diff --git a/packages/api/index.ts b/packages/api/index.ts index 3cf5b4459..bc7466fb9 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -218,6 +218,10 @@ export interface ICreatable> { */ export interface IResolvable> { resolveAsCell(): C; + getArgumentCell( + schema?: S, + ): Cell> | undefined; + getArgumentCell(): Cell | undefined; } /** diff --git a/packages/background-charm-service/cast-admin.ts b/packages/background-charm-service/cast-admin.ts index cc9122148..d83c9a45d 100644 --- a/packages/background-charm-service/cast-admin.ts +++ b/packages/background-charm-service/cast-admin.ts @@ -3,7 +3,7 @@ import { CharmManager, compileRecipe } from "@commontools/charm"; import { Runtime } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { type DID } from "@commontools/identity"; -import { createAdminSession } from "@commontools/identity"; +import { createSessionFromDid } from "@commontools/identity"; import { BG_CELL_CAUSE, BG_SYSTEM_SPACE_ID, @@ -87,9 +87,9 @@ async function castRecipe() { console.log("Casting recipe..."); // Create session and charm manager (matching main.ts pattern) - const session = await createAdminSession({ + const session = await createSessionFromDid({ identity, - name: "recipe-caster", + spaceName: "recipe-caster", space: spaceId as DID, }); diff --git a/packages/background-charm-service/src/worker.ts b/packages/background-charm-service/src/worker.ts index 093b6d455..d47a7f3d9 100644 --- a/packages/background-charm-service/src/worker.ts +++ b/packages/background-charm-service/src/worker.ts @@ -12,7 +12,7 @@ import { import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { - createAdminSession, + createSessionFromDid, type DID, Identity, Session, @@ -93,9 +93,9 @@ async function initialize( // Initialize session spaceId = did as DID; - currentSession = await createAdminSession({ + currentSession = await createSessionFromDid({ identity, - name: "~background-service-worker", + spaceName: "~background-service-worker", space: spaceId, }); diff --git a/packages/charm/src/manager.ts b/packages/charm/src/manager.ts index 43549210d..40561ca3a 100644 --- a/packages/charm/src/manager.ts +++ b/packages/charm/src/manager.ts @@ -226,7 +226,7 @@ export class CharmManager { } getSpaceName(): string { - return this.session.name; + return this.session.spaceName; } async synced(): Promise { diff --git a/packages/charm/src/ops/charms-controller.ts b/packages/charm/src/ops/charms-controller.ts index f9ce7c473..1bffb51d5 100644 --- a/packages/charm/src/ops/charms-controller.ts +++ b/packages/charm/src/ops/charms-controller.ts @@ -8,7 +8,7 @@ import { StorageManager } from "@commontools/runner/storage/cache"; import { CharmManager } from "../index.ts"; import { CharmController } from "./charm-controller.ts"; import { compileProgram } from "./utils.ts"; -import { ANYONE, Identity } from "@commontools/identity"; +import { createSession, Identity } from "@commontools/identity"; export interface CreateCharmOptions { input?: object; @@ -114,17 +114,7 @@ export class CharmsController { identity: Identity; spaceName: string; }): Promise { - const account = spaceName.startsWith("~") - ? identity - : await Identity.fromPassphrase(ANYONE); - const user = await account.derive(spaceName); - const session = { - private: account.did() === identity.did(), - name: spaceName, - space: user.did(), - as: user, - }; - + const session = await createSession({ identity, spaceName }); const runtime = new Runtime({ apiUrl: new URL(apiUrl), storageManager: StorageManager.open({ diff --git a/packages/cli/lib/charm.ts b/packages/cli/lib/charm.ts index 7481c98a3..da25c1f11 100644 --- a/packages/cli/lib/charm.ts +++ b/packages/cli/lib/charm.ts @@ -1,4 +1,4 @@ -import { ANYONE, Identity, Session } from "@commontools/identity"; +import { createSession, Session } from "@commontools/identity"; import { ensureDir } from "@std/fs"; import { loadIdentity } from "./identity.ts"; import { @@ -35,17 +35,8 @@ async function makeSession(config: SpaceConfig): Promise { if (config.space.startsWith("did:key")) { throw new Error("DID key spaces not yet supported."); } - const root = await loadIdentity(config.identity); - const account = config.space.startsWith("~") - ? root - : await Identity.fromPassphrase(ANYONE); - const user = await account.derive(config.space); - return { - private: account.did() === root.did(), - name: config.space, - space: user.did(), - as: user, - }; + const identity = await loadIdentity(config.identity); + return createSession({ identity, spaceName: config.space }); } export async function loadManager(config: SpaceConfig): Promise { diff --git a/packages/identity/src/index.ts b/packages/identity/src/index.ts index dda6a9046..59016b57b 100644 --- a/packages/identity/src/index.ts +++ b/packages/identity/src/index.ts @@ -8,7 +8,7 @@ export { KeyStore } from "./key-store.ts"; export * from "./interface.ts"; export { ANYONE, - createAdminSession, createSession, + createSessionFromDid, type Session, } from "./session.ts"; diff --git a/packages/identity/src/session.ts b/packages/identity/src/session.ts index fec994e5b..694836448 100644 --- a/packages/identity/src/session.ts +++ b/packages/identity/src/session.ts @@ -3,37 +3,41 @@ import { type DID } from "./interface.ts"; export const ANYONE = "common user"; export type Session = { - private: boolean; - name: string; + isPrivate: boolean; + spaceName: string; space: DID; as: Identity; }; // Create a session where `Identity` is used directly and not derived. -export const createAdminSession = async ( - { identity, space, name }: { +export const createSessionFromDid = ( + { identity, space, spaceName }: { identity: Identity; space: DID; - name: string; + spaceName: string; }, -) => - await ({ - private: name.startsWith("~"), - name, +): Promise => { + const isPrivate = spaceName.startsWith("~"); + return Promise.resolve({ + isPrivate, + spaceName, space, as: identity, }); +}; // Create a session where `Identity` is used to derive a space key. export const createSession = async ( - { identity, name }: { identity: Identity; name: string }, -) => { - const space = await identity.derive(name); + { identity, spaceName }: { identity: Identity; spaceName: string }, +): Promise => { + const isPrivate = spaceName.startsWith("~"); + const account = isPrivate ? identity : await Identity.fromPassphrase(ANYONE); + const user = await account.derive(spaceName); return { - private: name.startsWith("~"), - name, - space: space.did(), - as: space, + isPrivate, + spaceName, + space: user.did(), + as: user, }; }; diff --git a/packages/runner/integration/array_push.test.ts b/packages/runner/integration/array_push.test.ts index 8700280e0..9a4e605db 100644 --- a/packages/runner/integration/array_push.test.ts +++ b/packages/runner/integration/array_push.test.ts @@ -33,8 +33,8 @@ async function runTest() { const space_thingy = await account.derive(SPACE_NAME); const space_thingy_space = space_thingy.did(); const session = { - private: false, - name: SPACE_NAME, + isPrivate: false, + spaceName: SPACE_NAME, space: space_thingy_space, as: space_thingy, } as Session; diff --git a/packages/runner/integration/derive_array_leak.test.ts b/packages/runner/integration/derive_array_leak.test.ts index a2b4cd66d..2156b556c 100644 --- a/packages/runner/integration/derive_array_leak.test.ts +++ b/packages/runner/integration/derive_array_leak.test.ts @@ -85,8 +85,8 @@ async function runTest() { const space_thingy = await account.derive(SPACE_NAME); const space_thingy_space = space_thingy.did(); const session = { - private: false, - name: SPACE_NAME, + isPrivate: false, + spaceName: SPACE_NAME, space: space_thingy_space, as: space_thingy, } as Session; diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 792d6b77c..2cdde07c5 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -225,6 +225,7 @@ const cellMethods = new Set>([ "setRaw", "getSourceCell", "setSourceCell", + "getArgumentCell", "freeze", "isFrozen", "setSchema", @@ -916,6 +917,18 @@ export class CellImpl implements ICell, IStreamable { ); } + getArgumentCell(schema?: JSONSchema): Cell | undefined { + const sourceCell = this.getSourceCell(); + if (!sourceCell) return undefined; + // Kick off sync, since when used in a pattern, this wasn't automatically + // subscribed to yet. So we might still get a conflict on first write, but will + // get the correct version on retry. + sourceCell.sync(); + // TODO(seefeld): Ideally we intersect this schema with the actual argument + // schema, so that get isn't for any. + return sourceCell.key("argument").asSchema(schema); + } + freeze(reason: string): void { this.readOnlyReason = reason; } diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index fb1e96cf7..d66d32736 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -8,7 +8,7 @@ import { isCell } from "../src/cell.ts"; import { LINK_V1_TAG } from "../src/sigil-types.ts"; import { isCellResult } from "../src/query-result-proxy.ts"; import { toCell } from "../src/back-to-cell.ts"; -import { ID, JSONSchema } from "../src/builder/types.ts"; +import { ID, JSONSchema, type Recipe } from "../src/builder/types.ts"; import { popFrame, pushFrame } from "../src/builder/recipe.ts"; import { Runtime } from "../src/runtime.ts"; import { txToReactivityLog } from "../src/scheduler.ts"; @@ -225,6 +225,63 @@ describe("Cell", () => { expect(retrievedSource?.get()).toEqual({ foo: 456 }); }); + it("should update recipe output when argument is changed via getArgumentCell", async () => { + // Create a simple doubling recipe + const doubleRecipe: Recipe = { + argumentSchema: { + type: "object", + properties: { input: { type: "number" } }, + required: ["input"], + }, + resultSchema: { + type: "object", + properties: { output: { type: "number" } }, + }, + result: { output: { $alias: { path: ["internal", "doubled"] } } }, + nodes: [ + { + module: { + type: "javascript", + implementation: (args: { input: number }) => (args.input * 2), + }, + inputs: { input: { $alias: { path: ["argument", "input"] } } }, + outputs: { $alias: { path: ["internal", "doubled"] } }, + }, + ], + }; + + // Instantiate the recipe with initial argument + const resultCell = runtime.getCell(space, "doubling recipe instance"); + runtime.setup(undefined, doubleRecipe, { input: 5 }, resultCell); + runtime.start(resultCell); + await runtime.idle(); + + // Verify initial output + expect(resultCell.getAsQueryResult().output).toEqual(10); + + // Get the argument cell and update it + const argumentCell = resultCell.getArgumentCell<{ input: number }>(); + expect(argumentCell).toBeDefined(); + expect(argumentCell?.get()).toEqual({ input: 5 }); + + // Update the argument via the argument cell + const updateTx = runtime.edit(); + argumentCell!.withTx(updateTx).set({ input: 7 }); + updateTx.commit(); + await runtime.idle(); + + // Verify the output has changed + expect(resultCell.getAsQueryResult()).toEqual({ output: 14 }); + + // Update again to verify reactivity + const updateTx2 = runtime.edit(); + argumentCell!.withTx(updateTx2).set({ input: 100 }); + updateTx2.commit(); + await runtime.idle(); + + expect(resultCell.getAsQueryResult()).toEqual({ output: 200 }); + }); + it("should translate circular references into links", () => { const c = runtime.getCell( space, diff --git a/packages/seeder/cli.ts b/packages/seeder/cli.ts index eb235d73e..3875303b1 100644 --- a/packages/seeder/cli.ts +++ b/packages/seeder/cli.ts @@ -46,7 +46,7 @@ setLLMUrl(apiUrl); const identity = await Identity.fromPassphrase("common user"); const session = await createSession({ identity, - name, + spaceName: name, }); const runtime = new Runtime({ diff --git a/packages/shell/src/lib/runtime.ts b/packages/shell/src/lib/runtime.ts index 2b0b63ce5..6228f5483 100644 --- a/packages/shell/src/lib/runtime.ts +++ b/packages/shell/src/lib/runtime.ts @@ -1,4 +1,4 @@ -import { ANYONE, Identity, Session } from "@commontools/identity"; +import { createSession, Identity } from "@commontools/identity"; import { Runtime, RuntimeTelemetry, @@ -18,25 +18,6 @@ const logger = getLogger("shell.telemetry", { level: "debug", }); -async function createSession( - root: Identity, - spaceName: string, -): Promise { - const account = spaceName.startsWith("~") - ? root - : await Identity.fromPassphrase(ANYONE); - - const user = await account.derive(spaceName); - const session = { - private: account.did() === root.did(), - name: spaceName, - space: user.did(), - as: user, - }; - - return session; -} - // RuntimeInternals bundles all of the lifetimes // of resources bound to an identity,host,space triplet, // containing runtime, inspector, and charm references. @@ -103,7 +84,7 @@ export class RuntimeInternals extends EventTarget { apiUrl: URL; }, ): Promise { - const session = await createSession(identity, spaceName); + const session = await createSession({ identity, spaceName }); // We're hoisting CharmManager so that // we can create it after the runtime, but still reference diff --git a/scripts/main.ts b/scripts/main.ts index 7b3444b68..9dc421c76 100755 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -10,7 +10,7 @@ import { } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache"; import { - createAdminSession, + createSessionFromDid, type DID, Identity, type Session, @@ -85,10 +85,10 @@ async function main() { const space: DID = spaceDID as DID ?? identity.did(); - const session = await createAdminSession({ + const session = await createSessionFromDid({ identity, space, - name: spaceName ?? "unknown", + spaceName: spaceName ?? "unknown", }) satisfies Session; // TODO(seefeld): It only wants the space, so maybe we simplify the above and just space the space did? diff --git a/scripts/restart-local-dev.sh b/scripts/restart-local-dev.sh new file mode 100755 index 000000000..c60390793 --- /dev/null +++ b/scripts/restart-local-dev.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Change to repository root (parent of scripts directory) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR/.." + +# Parse command line arguments +CLEAR_CACHE=false +FORCE=false +while [[ $# -gt 0 ]]; do + case $1 in + --clear-cache) + CLEAR_CACHE=true + shift + ;; + --force) + FORCE=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--clear-cache] [--force]" + exit 1 + ;; + esac +done + +echo "Stopping local dev servers..." +./scripts/stop-local-dev.sh + +if [[ "$CLEAR_CACHE" == "true" ]]; then + echo "Clearing cache..." + rm -rf packages/toolshed/cache/* + echo "Cache cleared." +fi + +echo "Starting local dev servers..." +if [[ "$FORCE" == "true" ]]; then + ./scripts/start-local-dev.sh --force +else + ./scripts/start-local-dev.sh +fi