Skip to content

Commit 4f1685c

Browse files
committed
Allow non-proxied Futures to be used a "bracket" index types
1 parent 3e146ba commit 4f1685c

File tree

6 files changed

+92
-63
lines changed

6 files changed

+92
-63
lines changed

examples/arbitrary-future-properties.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env -S npx ts-node --transpileOnly
22

3-
import { Substrate, Box, sb } from "substrate";
3+
import { Substrate, Box, ComputeText, ComputeJSON, sb } from "substrate";
44

55
async function main() {
66
const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
@@ -9,37 +9,59 @@ async function main() {
99
apiKey: SUBSTRATE_API_KEY,
1010
});
1111

12-
const numbers = new Box(
13-
{
14-
value: [0, 1],
15-
},
16-
);
12+
const numbers = new Box({
13+
value: [0, 1],
14+
});
15+
16+
const latin = new Box({
17+
value: ["a", "b"],
18+
});
1719

18-
const latin = new Box(
19-
{
20-
value: ["a", "b"],
20+
const greek = new Box({
21+
value: {
22+
a: "α",
23+
b: "β",
2124
},
22-
);
25+
});
26+
27+
const proxyAccessor = new Box({ value: { property: "text" } });
28+
29+
const d = new ComputeText({
30+
prompt: "What is the character for the Latin 'D' in the Cyrillic alphabet? (just tell me the character only)",
31+
max_tokens: 1,
32+
});
2333

24-
const greek = new Box(
25-
{
26-
value: {
27-
a: "α",
28-
b: "β",
34+
const three = new ComputeText({
35+
prompt: "What number comes after '2'? (just tell me the character only)",
36+
max_tokens: 1,
37+
});
38+
const number3 = sb.jq<number>(three.future, ".text | tonumber");
39+
40+
const hebrew = new ComputeJSON({
41+
prompt: "what are the characters of the Hebrew alphabet (in unicode, eg. א )?",
42+
json_schema: {
43+
type: "object",
44+
properties: {
45+
characters: {
46+
type: "array",
47+
items: {
48+
type: "string",
49+
description: "single character",
50+
}
51+
}
2952
}
30-
},
31-
);
32-
33-
const result = new Box(
34-
{
35-
value: {
36-
x: latin.future.value.xyz,
37-
a: latin.future.value[numbers.future.value[0]],
38-
b: greek.future.value[latin.future.value[1]],
39-
ab: sb.concat(greek.future.value.a, greek.future.value.b),
40-
},
4153
}
42-
);
54+
});
55+
56+
const result = new Box({
57+
value: {
58+
a: latin.future.value[numbers.future.value[0]],
59+
b: greek.future.value[latin.future.value[1]],
60+
c: hebrew.future.json_object.characters[number3 as any],
61+
ab: sb.concat(greek.future.value.a, greek.future.value.b),
62+
d: sb.get<string>(d.future, proxyAccessor.future.value.property),
63+
},
64+
});
4365

4466
const res = await substrate.run(result);
4567
console.log(res.get(result));

examples/bug.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ async function main() {
1616

1717
const selected = new Box({
1818
value: {
19-
example1: sb.get<string>(
20-
data.future.value.object,
19+
example1: sb.concat(
20+
data.future.value.letters[0],
2121
data.future.value.letters[1],
2222
),
23-
example2: data.future.value.letters[data.future.value.index],
23+
// example2: data.future.value.letters[data.future.value.index],
2424
},
2525
});
2626

src/Future.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { idGenerator } from "substrate/idGenerator";
22
import { Node } from "substrate/Node";
3-
import { unproxy } from "./ProxiedFuture";
3+
import { unproxy, proxyFactory } from "./ProxiedFuture";
44

55
type Accessor = "item" | "attr";
66
type TraceOperation = {
@@ -222,6 +222,15 @@ export class Future<T = unknown> {
222222
return this._directive.result();
223223
}
224224

225+
[Symbol.toPrimitive]() {
226+
// Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values
227+
// we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when
228+
// used with bracket-access are) we track a reference to the value and use a special ID that can be used
229+
// later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the
230+
// resulting proxied `Future`.
231+
return proxyFactory.futureToPrimitive(this);
232+
}
233+
225234
toJSON() {
226235
return {
227236
id: this._id,

src/ProxiedFuture.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,22 @@ export const GET_TARGET = "$$GET_TARGET";
99
/**
1010
* @internal
1111
*/
12-
export const makeProxyFactory = (futureTable: FutureTable = {}) => {
12+
const makeProxyFactory = (futureTable: FutureTable = {}) => {
13+
// Converts a future into an internal-ID that we use in the `FutureTable`. We're using this special
14+
// format to mitigate potential conflicts with actual user-provided strings.
1315
const futureId = (future: Future<any>): string => {
1416
// @ts-ignore (accessing protected property: _id)
1517
return `${ID_PREFIX}${future._id}`;
1618
};
1719

20+
// Transforms a `Future` into an internal-ID to be used in the `FutureTable` and stores the `Future`
21+
// in this table as a side-effect. Should be called when a `[Symbol.toPrimitive]()` is called on a `Future`.
22+
const futureToPrimitive = (future: Future<any>): string => {
23+
const id = futureId(future);
24+
futureTable[id] = future; // store in lookup table
25+
return id;
26+
}
27+
1828
const makeProxy = <T = unknown>(future: Future<T>): any => {
1929
return new Proxy(future, {
2030
has(target: Future<T>, prop: any) {
@@ -32,30 +42,19 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => {
3242
if (prop === GET_TARGET) return target;
3343

3444
if (prop === Symbol.toPrimitive) {
35-
// When the prop is not a primitive (number, string, Symbol) it will be attempted
36-
// to be converted into one. This case will happen when the prop is a Future.
37-
//
38-
// Because only primitive types can be used as property accessors, what we're doing
39-
// here is returning a Future ID, which is a string. We use this specially formatted
40-
// ID to store the original Future in a lookup table we maintain as some hidden
41-
// state in the SDK.
42-
//
43-
// When the prop (Future ID) is used as an accessor (eg. in the case of "bracket"
44-
// access) we will use the returned Future ID here and look up the referenced Future
45-
// when constructing new proxied Futures (in it's TraceProps).
46-
return () => {
47-
const utarget = unproxy(target);
48-
const id = futureId(utarget);
49-
futureTable[id] = utarget; // store in lookup table
50-
return id;
51-
};
45+
// Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values
46+
// we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when
47+
// used with bracket-access are) we track a reference to the value and use a special ID that can be used
48+
// later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the
49+
// resulting proxied `Future`.
50+
return () => futureToPrimitive(unproxy(target));
5251
}
5352

5453
const nextProp = prop.startsWith(ID_PREFIX)
5554
? // When the prop is a Future ID, we will lookup the corresponding Future
5655
// so that we can use it as a TraceProp in the resulting new Future.
5756
futureTable[prop]!
58-
: // Otherwise the prop is not a future (always converted to string)
57+
: // Otherwise the prop is not a future (converted to string at this point)
5958
(prop as string);
6059

6160
// @ts-ignore (access protected prop: _directive)
@@ -69,6 +68,7 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => {
6968

7069
return {
7170
makeProxy,
71+
futureToPrimitive,
7272
};
7373
};
7474

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,8 @@ export {
5252
export { sb } from "substrate/sb";
5353
export { Substrate };
5454
import { Substrate } from "substrate/Substrate";
55+
56+
57+
export { Future, Trace } from "substrate/Future";
58+
export { Node } from "substrate/Node";
5559
export default Substrate;

tests/ProxiedFuture.test.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { expect, describe, test } from "vitest";
22
import { Future, Trace } from "substrate/Future";
33
import { Node } from "substrate/Node";
4-
import { makeProxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture";
4+
import { proxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture";
55

66
class FooFuture extends Future<any> {}
77
class FooNode extends Node {}
@@ -11,15 +11,13 @@ const node = (id: string = "") => new FooNode({}, { id });
1111
describe("ProxiedFuture", () => {
1212
describe("ProxyFactory", () => {
1313
test("makeProxy", () => {
14-
const proxyFactory = makeProxyFactory();
1514
const f = new FooFuture(new Trace([], node()));
1615
const p = proxyFactory.makeProxy(f);
1716
// Proxy is an instance of Future
1817
expect(p).instanceof(Future);
1918
});
2019

2120
test("isProxy", () => {
22-
const proxyFactory = makeProxyFactory();
2321
const f = new FooFuture(new Trace([], node()));
2422
const p = proxyFactory.makeProxy(f);
2523
// We can detect whether the proxied Future is a proxy
@@ -30,7 +28,6 @@ describe("ProxiedFuture", () => {
3028

3129
describe("Proxy", () => {
3230
test("unproxy (returns unproxied Future)", () => {
33-
const proxyFactory = makeProxyFactory();
3431
const f = new FooFuture(new Trace([], node()));
3532
const p = proxyFactory.makeProxy(f);
3633
const up = unproxy(p);
@@ -39,16 +36,13 @@ describe("ProxiedFuture", () => {
3936
});
4037

4138
test("arbitrary property access (. notation)", () => {
42-
const proxyFactory = makeProxyFactory();
4339
const f = new FooFuture(new Trace([], node("123")));
4440
const p = proxyFactory.makeProxy(f);
4541

46-
// @ts-ignore (properties don't exist)
4742
const f1 = p.a.b.c;
4843
expect(f1).instanceof(Future);
4944
expect(isProxy(f1)).toEqual(true);
5045

51-
// @ts-ignore ("virtual property" doesn't exist)
5246
const up = unproxy(f1);
5347
const json = up.toJSON();
5448

@@ -64,7 +58,6 @@ describe("ProxiedFuture", () => {
6458
});
6559

6660
test("arbitrary property and index access (brackets)", () => {
67-
const proxyFactory = makeProxyFactory();
6861
const f = new FooFuture(new Trace([], node("123")));
6962
const p = proxyFactory.makeProxy(f);
7063

@@ -86,27 +79,28 @@ describe("ProxiedFuture", () => {
8679
});
8780
});
8881

89-
// TODO(liam): I'm not sure why this test doesn't work yet, but it does work in example code
90-
test.skip("using Future values as proxy accessors", () => {
91-
const proxyFactory = makeProxyFactory();
82+
test("using Future values as proxy accessors", () => {
9283
const f = new FooFuture(new Trace([], node("123")));
9384
const p = proxyFactory.makeProxy(f);
9485

9586
const a = new FooFuture(new Trace([], node("456")));
87+
const b = new FooFuture(new Trace([], node("789")));
9688

97-
// non index-types are illegal in the type system, so we're casting to any here.
98-
const f1 = p[a as any];
89+
const f1 = p[a as any][b as any];
9990

10091
expect(f1).instanceof(Future);
10192
expect(isProxy(f1)).toEqual(true);
10293

10394
const up = unproxy(f1);
95+
10496
const json = up.toJSON();
10597

10698
expect(json.directive).toEqual({
10799
op_stack: [
108100
// @ts-ignore
109101
{ accessor: "attr", future_id: a._id, key: null },
102+
// @ts-ignore
103+
{ accessor: "attr", future_id: b._id, key: null },
110104
],
111105
origin_node_id: "123",
112106
type: "trace",

0 commit comments

Comments
 (0)