diff --git a/examples/arbitrary-future-properties.ts b/examples/arbitrary-future-properties.ts index ebc5ffb..2c9675d 100755 --- a/examples/arbitrary-future-properties.ts +++ b/examples/arbitrary-future-properties.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S npx ts-node --transpileOnly -import { Substrate, Box, sb } from "substrate"; +import { Substrate, Box, ComputeText, ComputeJSON, sb } from "substrate"; async function main() { const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"]; @@ -9,37 +9,59 @@ async function main() { apiKey: SUBSTRATE_API_KEY, }); - const numbers = new Box( - { - value: [0, 1], - }, - ); + const numbers = new Box({ + value: [0, 1], + }); + + const latin = new Box({ + value: ["a", "b"], + }); - const latin = new Box( - { - value: ["a", "b"], + const greek = new Box({ + value: { + a: "α", + b: "β", }, - ); + }); + + const proxyAccessor = new Box({ value: { property: "text" } }); + + const d = new ComputeText({ + prompt: "What is the character for the Latin 'D' in the Cyrillic alphabet? (just tell me the character only)", + max_tokens: 1, + }); - const greek = new Box( - { - value: { - a: "α", - b: "β", + const three = new ComputeText({ + prompt: "What number comes after '2'? (just tell me the character only)", + max_tokens: 1, + }); + const number3 = sb.jq(three.future, ".text | tonumber"); + + const hebrew = new ComputeJSON({ + prompt: "what are the characters of the Hebrew alphabet (in unicode, eg. א )?", + json_schema: { + type: "object", + properties: { + characters: { + type: "array", + items: { + type: "string", + description: "single character", + } + } } - }, - ); - - const result = new Box( - { - value: { - x: latin.future.value.xyz, - a: latin.future.value[numbers.future.value[0]], - b: greek.future.value[latin.future.value[1]], - ab: sb.concat(greek.future.value.a, greek.future.value.b), - }, } - ); + }); + + const result = new Box({ + value: { + a: latin.future.value[numbers.future.value[0]], + b: greek.future.value[latin.future.value[1]], + c: hebrew.future.json_object.characters[number3 as any], + ab: sb.concat(greek.future.value.a, greek.future.value.b), + d: sb.get(d.future, proxyAccessor.future.value.property), + }, + }); const res = await substrate.run(result); console.log(res.get(result)); diff --git a/examples/bug.ts b/examples/bug.ts index c47484c..01060f8 100755 --- a/examples/bug.ts +++ b/examples/bug.ts @@ -16,11 +16,11 @@ async function main() { const selected = new Box({ value: { - example1: sb.get( - data.future.value.object, + example1: sb.concat( + data.future.value.letters[0], data.future.value.letters[1], ), - example2: data.future.value.letters[data.future.value.index], + // example2: data.future.value.letters[data.future.value.index], }, }); diff --git a/src/Future.ts b/src/Future.ts index 62f78b0..da166da 100644 --- a/src/Future.ts +++ b/src/Future.ts @@ -1,6 +1,6 @@ import { idGenerator } from "substrate/idGenerator"; import { Node } from "substrate/Node"; -import { unproxy } from "./ProxiedFuture"; +import { unproxy, proxyFactory } from "./ProxiedFuture"; type Accessor = "item" | "attr"; type TraceOperation = { @@ -222,6 +222,15 @@ export class Future { return this._directive.result(); } + [Symbol.toPrimitive]() { + // Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values + // we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when + // used with bracket-access are) we track a reference to the value and use a special ID that can be used + // later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the + // resulting proxied `Future`. + return proxyFactory.futureToPrimitive(this); + } + toJSON() { return { id: this._id, diff --git a/src/ProxiedFuture.ts b/src/ProxiedFuture.ts index fddc534..424073f 100644 --- a/src/ProxiedFuture.ts +++ b/src/ProxiedFuture.ts @@ -9,12 +9,22 @@ export const GET_TARGET = "$$GET_TARGET"; /** * @internal */ -export const makeProxyFactory = (futureTable: FutureTable = {}) => { +const makeProxyFactory = (futureTable: FutureTable = {}) => { + // Converts a future into an internal-ID that we use in the `FutureTable`. We're using this special + // format to mitigate potential conflicts with actual user-provided strings. const futureId = (future: Future): string => { // @ts-ignore (accessing protected property: _id) return `${ID_PREFIX}${future._id}`; }; + // Transforms a `Future` into an internal-ID to be used in the `FutureTable` and stores the `Future` + // in this table as a side-effect. Should be called when a `[Symbol.toPrimitive]()` is called on a `Future`. + const futureToPrimitive = (future: Future): string => { + const id = futureId(future); + futureTable[id] = future; // store in lookup table + return id; + } + const makeProxy = (future: Future): any => { return new Proxy(future, { has(target: Future, prop: any) { @@ -32,30 +42,19 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => { if (prop === GET_TARGET) return target; if (prop === Symbol.toPrimitive) { - // When the prop is not a primitive (number, string, Symbol) it will be attempted - // to be converted into one. This case will happen when the prop is a Future. - // - // Because only primitive types can be used as property accessors, what we're doing - // here is returning a Future ID, which is a string. We use this specially formatted - // ID to store the original Future in a lookup table we maintain as some hidden - // state in the SDK. - // - // When the prop (Future ID) is used as an accessor (eg. in the case of "bracket" - // access) we will use the returned Future ID here and look up the referenced Future - // when constructing new proxied Futures (in it's TraceProps). - return () => { - const utarget = unproxy(target); - const id = futureId(utarget); - futureTable[id] = utarget; // store in lookup table - return id; - }; + // Because we would like to use `Future` values as accessors with bracket-notation on proxied `Future` values + // we need to ensure that when a `Future` instance is being converted into a primitive (as all values are when + // used with bracket-access are) we track a reference to the value and use a special ID that can be used + // later on in the proxy to look up and use the original `Future` when constructing the `Trace` used in the + // resulting proxied `Future`. + return () => futureToPrimitive(unproxy(target)); } const nextProp = prop.startsWith(ID_PREFIX) ? // When the prop is a Future ID, we will lookup the corresponding Future // so that we can use it as a TraceProp in the resulting new Future. futureTable[prop]! - : // Otherwise the prop is not a future (always converted to string) + : // Otherwise the prop is not a future (converted to string at this point) (prop as string); // @ts-ignore (access protected prop: _directive) @@ -69,6 +68,7 @@ export const makeProxyFactory = (futureTable: FutureTable = {}) => { return { makeProxy, + futureToPrimitive, }; }; diff --git a/src/index.ts b/src/index.ts index 7d39e7a..9179c45 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,4 +52,8 @@ export { export { sb } from "substrate/sb"; export { Substrate }; import { Substrate } from "substrate/Substrate"; + + +export { Future, Trace } from "substrate/Future"; +export { Node } from "substrate/Node"; export default Substrate; diff --git a/tests/ProxiedFuture.test.ts b/tests/ProxiedFuture.test.ts index d6b1b45..8304058 100644 --- a/tests/ProxiedFuture.test.ts +++ b/tests/ProxiedFuture.test.ts @@ -1,7 +1,7 @@ import { expect, describe, test } from "vitest"; import { Future, Trace } from "substrate/Future"; import { Node } from "substrate/Node"; -import { makeProxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture"; +import { proxyFactory, isProxy, unproxy } from "substrate/ProxiedFuture"; class FooFuture extends Future {} class FooNode extends Node {} @@ -11,7 +11,6 @@ const node = (id: string = "") => new FooNode({}, { id }); describe("ProxiedFuture", () => { describe("ProxyFactory", () => { test("makeProxy", () => { - const proxyFactory = makeProxyFactory(); const f = new FooFuture(new Trace([], node())); const p = proxyFactory.makeProxy(f); // Proxy is an instance of Future @@ -19,7 +18,6 @@ describe("ProxiedFuture", () => { }); test("isProxy", () => { - const proxyFactory = makeProxyFactory(); const f = new FooFuture(new Trace([], node())); const p = proxyFactory.makeProxy(f); // We can detect whether the proxied Future is a proxy @@ -30,7 +28,6 @@ describe("ProxiedFuture", () => { describe("Proxy", () => { test("unproxy (returns unproxied Future)", () => { - const proxyFactory = makeProxyFactory(); const f = new FooFuture(new Trace([], node())); const p = proxyFactory.makeProxy(f); const up = unproxy(p); @@ -39,16 +36,13 @@ describe("ProxiedFuture", () => { }); test("arbitrary property access (. notation)", () => { - const proxyFactory = makeProxyFactory(); const f = new FooFuture(new Trace([], node("123"))); const p = proxyFactory.makeProxy(f); - // @ts-ignore (properties don't exist) const f1 = p.a.b.c; expect(f1).instanceof(Future); expect(isProxy(f1)).toEqual(true); - // @ts-ignore ("virtual property" doesn't exist) const up = unproxy(f1); const json = up.toJSON(); @@ -64,7 +58,6 @@ describe("ProxiedFuture", () => { }); test("arbitrary property and index access (brackets)", () => { - const proxyFactory = makeProxyFactory(); const f = new FooFuture(new Trace([], node("123"))); const p = proxyFactory.makeProxy(f); @@ -86,27 +79,28 @@ describe("ProxiedFuture", () => { }); }); - // TODO(liam): I'm not sure why this test doesn't work yet, but it does work in example code - test.skip("using Future values as proxy accessors", () => { - const proxyFactory = makeProxyFactory(); + test("using Future values as proxy accessors", () => { const f = new FooFuture(new Trace([], node("123"))); const p = proxyFactory.makeProxy(f); const a = new FooFuture(new Trace([], node("456"))); + const b = new FooFuture(new Trace([], node("789"))); - // non index-types are illegal in the type system, so we're casting to any here. - const f1 = p[a as any]; + const f1 = p[a as any][b as any]; expect(f1).instanceof(Future); expect(isProxy(f1)).toEqual(true); const up = unproxy(f1); + const json = up.toJSON(); expect(json.directive).toEqual({ op_stack: [ // @ts-ignore { accessor: "attr", future_id: a._id, key: null }, + // @ts-ignore + { accessor: "attr", future_id: b._id, key: null }, ], origin_node_id: "123", type: "trace",