experimental-a2 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +36 -0
- package/README.md +29 -0
- package/dist/actor-client.d.ts +46 -0
- package/dist/actor-client.d.ts.map +1 -0
- package/dist/actor-client.js +54 -0
- package/dist/actor-client.js.map +1 -0
- package/dist/actor-react.d.ts +54 -0
- package/dist/actor-react.d.ts.map +1 -0
- package/dist/actor-react.js +79 -0
- package/dist/actor-react.js.map +1 -0
- package/dist/actor-shared-BACubf4x.d.ts +136 -0
- package/dist/actor-shared-BACubf4x.d.ts.map +1 -0
- package/dist/actor-shared-DI7J5upy.js +127 -0
- package/dist/actor-shared-DI7J5upy.js.map +1 -0
- package/dist/actor.browser.d.ts +1 -0
- package/dist/actor.browser.js +13 -0
- package/dist/actor.browser.js.map +1 -0
- package/dist/actor.d.ts +176 -0
- package/dist/actor.d.ts.map +1 -0
- package/dist/actor.js +437 -0
- package/dist/actor.js.map +1 -0
- package/dist/ai-server.d.ts +2 -2
- package/dist/ai-server.js +2 -2
- package/dist/ai.d.ts +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/{errors-BQuJpe82.js → errors-DCk6ch5n.js} +16 -2
- package/dist/{errors-BQuJpe82.js.map → errors-DCk6ch5n.js.map} +1 -1
- package/dist/{idempotent-replay-DuqEkYA7.js → idempotent-replay-DVOlyYbx.js} +2 -2
- package/dist/{idempotent-replay-DuqEkYA7.js.map → idempotent-replay-DVOlyYbx.js.map} +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/react.d.ts +1 -1
- package/dist/{contract-jIfaR085.d.ts → reducer-DJKWm3cp.d.ts} +39 -39
- package/dist/reducer-DJKWm3cp.d.ts.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-B2XNevQA.js → server-CBET-jSz.js} +6 -6
- package/dist/server-CBET-jSz.js.map +1 -0
- package/dist/{server-DjPhHnbI.d.ts → server-CKY3_lbw.d.ts} +3 -3
- package/dist/{server-DjPhHnbI.d.ts.map → server-CKY3_lbw.d.ts.map} +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/{store-RJO35BMj.d.ts → store-DGHeBtIQ.d.ts} +2 -2
- package/dist/{store-RJO35BMj.d.ts.map → store-DGHeBtIQ.d.ts.map} +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +2 -2
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +2 -2
- package/dist/{store-redis-core-DT01r4GZ.js → store-redis-core-z-ykbyMg.js} +3 -3
- package/dist/{store-redis-core-DT01r4GZ.js.map → store-redis-core-z-ykbyMg.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -2
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +2 -2
- package/dist/{wire-B6te_wns.js → wire--yji6mO3.js} +2 -2
- package/dist/{wire-B6te_wns.js.map → wire--yji6mO3.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +189 -0
- package/docs/actors/02-concurrency.mdx +154 -0
- package/docs/actors/03-timers.mdx +120 -0
- package/docs/actors/04-routes.mdx +352 -0
- package/docs/actors/meta.ts +1 -0
- package/docs/concepts/meta.ts +1 -0
- package/docs/guides/07-examples.mdx +56 -0
- package/docs/guides/meta.ts +1 -0
- package/docs/index.mdx +16 -0
- package/docs/reference/02-errors.mdx +33 -0
- package/docs/reference/meta.ts +1 -0
- package/examples/README.md +15 -0
- package/examples/playground/AGENTS.md +11 -0
- package/examples/playground/DEPLOY.md +106 -0
- package/examples/playground/README.md +19 -0
- package/examples/playground/activity-feed.test.ts +10 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
- package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
- package/examples/playground/app/agent/events/route.ts +4 -0
- package/examples/playground/app/agent/model.ts +3 -0
- package/examples/playground/app/agent/new-agent-session.tsx +98 -0
- package/examples/playground/app/agent/page.tsx +25 -0
- package/examples/playground/app/agent/scheduler/route.ts +5 -0
- package/examples/playground/app/agent/server.ts +153 -0
- package/examples/playground/app/agent/session.ts +13 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
- package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
- package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
- package/examples/playground/app/canvas/events/route.ts +13 -0
- package/examples/playground/app/canvas/model.ts +94 -0
- package/examples/playground/app/canvas/open-canvas.tsx +40 -0
- package/examples/playground/app/canvas/page.tsx +20 -0
- package/examples/playground/app/canvas/server.ts +9 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
- package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
- package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
- package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
- package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
- package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
- package/examples/playground/app/chat/[chatId]/session.ts +7 -0
- package/examples/playground/app/chat/events/route.ts +7 -0
- package/examples/playground/app/chat/model.test.ts +155 -0
- package/examples/playground/app/chat/model.ts +310 -0
- package/examples/playground/app/chat/new-conversation.tsx +16 -0
- package/examples/playground/app/chat/page.tsx +25 -0
- package/examples/playground/app/chat/scheduler/route.ts +5 -0
- package/examples/playground/app/chat/server.ts +184 -0
- package/examples/playground/app/components/activity-feed.tsx +54 -0
- package/examples/playground/app/components/connection-pill.tsx +29 -0
- package/examples/playground/app/counter/counter-client.tsx +72 -0
- package/examples/playground/app/counter/events/route.ts +4 -0
- package/examples/playground/app/counter/model.test.ts +36 -0
- package/examples/playground/app/counter/model.ts +31 -0
- package/examples/playground/app/counter/page.tsx +24 -0
- package/examples/playground/app/counter/server.ts +9 -0
- package/examples/playground/app/counter/session.ts +13 -0
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
- package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
- package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
- package/examples/playground/app/documents/[documentId]/session.ts +7 -0
- package/examples/playground/app/documents/events/route.ts +4 -0
- package/examples/playground/app/documents/model.ts +55 -0
- package/examples/playground/app/documents/open-document.tsx +40 -0
- package/examples/playground/app/documents/page.tsx +22 -0
- package/examples/playground/app/documents/server.ts +9 -0
- package/examples/playground/app/globals.css +2078 -0
- package/examples/playground/app/layout.tsx +44 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
- package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
- package/examples/playground/app/orders/[orderId]/session.ts +11 -0
- package/examples/playground/app/orders/create/route.ts +30 -0
- package/examples/playground/app/orders/events/route.ts +4 -0
- package/examples/playground/app/orders/model.ts +79 -0
- package/examples/playground/app/orders/new-order-form.tsx +98 -0
- package/examples/playground/app/orders/page.tsx +22 -0
- package/examples/playground/app/orders/scheduler/route.ts +5 -0
- package/examples/playground/app/orders/server.ts +50 -0
- package/examples/playground/app/page.tsx +111 -0
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
- package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
- package/examples/playground/app/recovery/events/route.ts +3 -0
- package/examples/playground/app/recovery/model.ts +55 -0
- package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
- package/examples/playground/app/recovery/page.tsx +22 -0
- package/examples/playground/app/recovery/scheduler/route.ts +7 -0
- package/examples/playground/app/recovery/server.ts +53 -0
- package/examples/playground/app/recovery/start/route.ts +41 -0
- package/examples/playground/app/vault/[vaultId]/route.ts +19 -0
- package/examples/playground/app/vault/page.tsx +12 -0
- package/examples/playground/app/vault/server.ts +9 -0
- package/examples/playground/app/vault/vault-client.tsx +124 -0
- package/examples/playground/app/vault/vault.test.ts +147 -0
- package/examples/playground/app/vault/vault.ts +119 -0
- package/examples/playground/css.d.ts +4 -0
- package/examples/playground/lib/store.ts +15 -0
- package/examples/playground/next-env.d.ts +5 -0
- package/examples/playground/next.config.ts +10 -0
- package/examples/playground/package.json +46 -0
- package/examples/playground/tsconfig.json +37 -0
- package/examples/playground/vercel.json +40 -0
- package/package.json +11 -2
- package/src/actor-client.ts +132 -0
- package/src/actor-react.ts +143 -0
- package/src/actor-shared.ts +356 -0
- package/src/actor.browser.ts +12 -0
- package/src/actor.ts +914 -0
- package/src/client.ts +9 -1
- package/src/errors.ts +15 -0
- package/src/index.ts +1 -1
- package/src/server.ts +13 -3
- package/dist/contract-jIfaR085.d.ts.map +0 -1
- package/dist/server-B2XNevQA.js.map +0 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Using A2 from this package
|
|
2
|
+
|
|
3
|
+
When answering questions or editing applications that use A2:
|
|
4
|
+
|
|
5
|
+
1. Read `docs/` for the supported API and guarantees.
|
|
6
|
+
2. Read `examples/playground/` for complete application structure.
|
|
7
|
+
3. Read `src/` only when implementation details are relevant.
|
|
8
|
+
|
|
9
|
+
Do not infer application patterns from `dist/`. The playground uses public
|
|
10
|
+
package entry points and shows where contracts, reducers, servers, route
|
|
11
|
+
handlers, clients, authorization, WebSocket capabilities, and schedulers live.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a29bad2: New experimental actor module: durable-objects-shaped actors built on
|
|
8
|
+
sessions. Three entry points: `experimental-a2/actor` (server),
|
|
9
|
+
`experimental-a2/actor/client` (framework-agnostic typed calls), and
|
|
10
|
+
`experimental-a2/actor/react` (the `useActor` hook).
|
|
11
|
+
|
|
12
|
+
Define an actor over a protocol of types (`state`, `events`, optional
|
|
13
|
+
`presence`): handlers are serial per instance (one message at a time,
|
|
14
|
+
one atomic commit at return), `concurrent(...)` opts a handler off the
|
|
15
|
+
lane for slow work, `ctx.send` buffers typed self-sends into the
|
|
16
|
+
commit, and `ctx.schedule` arms protocol-typed durable timers. Handles
|
|
17
|
+
expose `call` (send and await the answer), `send`, `state`, and a
|
|
18
|
+
managed `fetch` door: GET streams state commits over SSE, POST serves
|
|
19
|
+
calls and presence envelopes, with per-operation `authorize` and
|
|
20
|
+
per-mount `view` projections. Presence is protocol-typed audience
|
|
21
|
+
state: `participant`, `presence`, and `setPresence` on `useActor`.
|
|
22
|
+
|
|
23
|
+
Core additions: `NonRetriableError` (a deterministic failure that
|
|
24
|
+
dead-letters on its first throw instead of burning the retry budget)
|
|
25
|
+
and a `'*'` catch-all entry in presence vocabularies (unknown fields
|
|
26
|
+
validate against it; object-plumbing names never do).
|
|
27
|
+
|
|
28
|
+
Docs: a new Actors section (introduction, concurrency, deadlines and
|
|
29
|
+
timers, routes and clients).
|
|
30
|
+
|
|
31
|
+
## 0.8.1
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 3bff761: Include a standalone snapshot of the complete Next.js playground in the npm
|
|
36
|
+
package, with package and documentation indexes that direct coding agents to
|
|
37
|
+
the supported docs and example structure.
|
|
38
|
+
|
|
3
39
|
## 0.8.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# experimental-a2
|
|
2
|
+
|
|
3
|
+
Durable sync and reactions for things with a lifecycle. Each session has an
|
|
4
|
+
event log, derived state, handlers, timers, and a live client.
|
|
5
|
+
|
|
6
|
+
## Read the package
|
|
7
|
+
|
|
8
|
+
The npm package includes more than compiled output:
|
|
9
|
+
|
|
10
|
+
- `docs/` defines the supported API.
|
|
11
|
+
- `examples/playground/` contains complete, standalone Next.js examples.
|
|
12
|
+
- `src/` contains the TypeScript implementation.
|
|
13
|
+
|
|
14
|
+
Coding agents should read `docs/` and `examples/playground/` before inferring
|
|
15
|
+
usage from `dist/` or internal implementation details. Every playground feature
|
|
16
|
+
owns its contract, reducer, server, route handlers, and scheduler when needed.
|
|
17
|
+
The package and copied playground both include `AGENTS.md` discovery files.
|
|
18
|
+
|
|
19
|
+
## Copy the playground
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
cp -R node_modules/experimental-a2/examples/playground ./a2-playground
|
|
23
|
+
cd a2-playground
|
|
24
|
+
pnpm install
|
|
25
|
+
pnpm dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The copied application has a standalone `package.json`. Its A2 dependency is
|
|
29
|
+
pinned to the package version that shipped the snapshot.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { c as ActorRefusedError } from "./actor-shared-BACubf4x.js";
|
|
2
|
+
//#region src/actor-client.d.ts
|
|
3
|
+
/** The loose shape of an `actor()` definition — carried type-only. */
|
|
4
|
+
type AnyActorDefinition = {
|
|
5
|
+
actor(id: string): {
|
|
6
|
+
state(): Promise<{
|
|
7
|
+
state: unknown;
|
|
8
|
+
index: number;
|
|
9
|
+
}>;
|
|
10
|
+
call: object;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
type HandleOf<D extends AnyActorDefinition> = ReturnType<D["actor"]>;
|
|
14
|
+
type ActorStateOf<D extends AnyActorDefinition> = Awaited<ReturnType<HandleOf<D>["state"]>>["state"];
|
|
15
|
+
/**
|
|
16
|
+
* The server handle's `call` record mirrored to the client, with
|
|
17
|
+
* answers carrying `View` — the projected shape when the route mounts
|
|
18
|
+
* `fetch(req, { view })`, the full state otherwise (the default).
|
|
19
|
+
*/
|
|
20
|
+
type ActorClientCall<D extends AnyActorDefinition, View = ActorStateOf<D>> = { readonly [K in keyof HandleOf<D>["call"]]: HandleOf<D>["call"][K] extends ((...args: infer P) => Promise<{
|
|
21
|
+
state: unknown;
|
|
22
|
+
index: number;
|
|
23
|
+
}>) ? (...args: P) => Promise<{
|
|
24
|
+
state: View;
|
|
25
|
+
index: number;
|
|
26
|
+
}> : never; };
|
|
27
|
+
type ActorClient<D extends AnyActorDefinition, View = ActorStateOf<D>> = {
|
|
28
|
+
/** Typed calls over POST — the response is the answer. */
|
|
29
|
+
readonly call: ActorClientCall<D, View>;
|
|
30
|
+
};
|
|
31
|
+
type CreateActorClientOptions = {
|
|
32
|
+
/** The route whose handler delegates to `handle.fetch` for this instance. */
|
|
33
|
+
api: string;
|
|
34
|
+
/** Injectable transport (tests, custom auth headers). */
|
|
35
|
+
fetch?: typeof globalThis.fetch;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* A typed call surface for one actor instance's route. When the route
|
|
39
|
+
* mounts a `view`, pass its shape as the second type argument so call
|
|
40
|
+
* answers carry the projected state:
|
|
41
|
+
* `createActorClient<typeof vault, PublicVault>({ api })`.
|
|
42
|
+
*/
|
|
43
|
+
declare function createActorClient<D extends AnyActorDefinition, View = ActorStateOf<D>>(options: CreateActorClientOptions): ActorClient<D, View>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { ActorClient, ActorClientCall, ActorRefusedError, ActorStateOf, AnyActorDefinition, CreateActorClientOptions, createActorClient };
|
|
46
|
+
//# sourceMappingURL=actor-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-client.d.ts","names":[],"sources":["../src/actor-client.ts"],"mappings":";;;KAyBY;EACV,MAAM;IACJ,SAAS;MAAU;MAAgB;;IACnC;;;KAIC,SAAS,UAAU,sBAAsB,WAAW;KAE7C,aAAa,UAAU,sBAAsB,QACvD,WAAW,SAAS;;;;;;KAQV,gBACV,UAAU,oBACV,OAAO,aAAa,kBAEV,WAAW,SAAS,aAAa,SAAS,WAAW,gBAC1D,YAAY,MACZ;EAAU;EAAgB;UACvB,MAAM,MAAM;EAAU,OAAO;EAAM;;KAIjC,YACV,UAAU,oBACV,OAAO,aAAa;;WAGX,MAAM,gBAAgB,GAAG;;KAGxB;;EAEV;;EAEA,eAAe,WAAW;;;;;;;;iBAiDZ,kBACd,UAAU,oBACV,OAAO,aAAa,IACpB,SAAS,2BAA2B,YAAY,GAAG"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { i as ActorRefusedError } from "./actor-shared-DI7J5upy.js";
|
|
2
|
+
//#region src/actor-client.ts
|
|
3
|
+
/**
|
|
4
|
+
* experimental-a2/actor/client — typed calls to an actor over HTTP.
|
|
5
|
+
* Framework-agnostic: no React, no framework machinery; works in any
|
|
6
|
+
* browser code and server-to-server.
|
|
7
|
+
*
|
|
8
|
+
* `client.call` mirrors the server handle's `call` record over the
|
|
9
|
+
* call lane `handle.fetch` serves — POST `{ event, input, messageId? }`
|
|
10
|
+
* → the answer (post-state), a 409 refusal (revived as
|
|
11
|
+
* `ActorRefusedError`), or a 400. Both ends of that wire are library
|
|
12
|
+
* code: the types here are backed by `handle.fetch`, not by a
|
|
13
|
+
* hand-written convention.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import type { vault } from '@/app/vault/server' // type-only, erased
|
|
17
|
+
* const client = createActorClient<typeof vault>({ api: `/vault/${id}` })
|
|
18
|
+
* await client.call.withdraw({ amount: 60 }) // ActorRefusedError on refusal
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
const CALL_TIMEOUT_MS = 3e4;
|
|
22
|
+
const callOverWire = async (fetchImpl, api, event, input, options) => {
|
|
23
|
+
const response = await fetchImpl(api, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json" },
|
|
26
|
+
body: JSON.stringify({
|
|
27
|
+
event,
|
|
28
|
+
...input === void 0 ? {} : { input },
|
|
29
|
+
...options?.id === void 0 ? {} : { messageId: options.id }
|
|
30
|
+
}),
|
|
31
|
+
signal: AbortSignal.timeout(options?.timeoutMs ?? CALL_TIMEOUT_MS)
|
|
32
|
+
});
|
|
33
|
+
const body = await response.json().catch(() => ({}));
|
|
34
|
+
if (response.status === 409 && typeof body["error"] === "string") throw new ActorRefusedError(typeof body["event"] === "string" ? body["event"] : event, typeof body["messageId"] === "string" ? body["messageId"] : "", body["error"]);
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const message = typeof body["error"] === "string" ? body["error"] : `actor call failed (${response.status})`;
|
|
37
|
+
throw new Error(message);
|
|
38
|
+
}
|
|
39
|
+
return body;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A typed call surface for one actor instance's route. When the route
|
|
43
|
+
* mounts a `view`, pass its shape as the second type argument so call
|
|
44
|
+
* answers carry the projected state:
|
|
45
|
+
* `createActorClient<typeof vault, PublicVault>({ api })`.
|
|
46
|
+
*/
|
|
47
|
+
function createActorClient(options) {
|
|
48
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
49
|
+
return { call: new Proxy({}, { get: (_target, event) => typeof event === "string" ? (input, callOptions) => callOverWire(fetchImpl, options.api, event, input, callOptions) : void 0 }) };
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { ActorRefusedError, createActorClient };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=actor-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-client.js","names":[],"sources":["../src/actor-client.ts"],"sourcesContent":["/**\n * experimental-a2/actor/client — typed calls to an actor over HTTP.\n * Framework-agnostic: no React, no framework machinery; works in any\n * browser code and server-to-server.\n *\n * `client.call` mirrors the server handle's `call` record over the\n * call lane `handle.fetch` serves — POST `{ event, input, messageId? }`\n * → the answer (post-state), a 409 refusal (revived as\n * `ActorRefusedError`), or a 400. Both ends of that wire are library\n * code: the types here are backed by `handle.fetch`, not by a\n * hand-written convention.\n *\n * ```ts\n * import type { vault } from '@/app/vault/server' // type-only, erased\n * const client = createActorClient<typeof vault>({ api: `/vault/${id}` })\n * await client.call.withdraw({ amount: 60 }) // ActorRefusedError on refusal\n * ```\n */\n\nimport { ActorRefusedError } from './actor-shared.ts'\nimport type { ActorCallOptions, ActorCallResult } from './actor.ts'\n\nexport { ActorRefusedError }\n\n/** The loose shape of an `actor()` definition — carried type-only. */\nexport type AnyActorDefinition = {\n actor(id: string): {\n state(): Promise<{ state: unknown; index: number }>\n call: object\n }\n}\n\ntype HandleOf<D extends AnyActorDefinition> = ReturnType<D['actor']>\n\nexport type ActorStateOf<D extends AnyActorDefinition> = Awaited<\n ReturnType<HandleOf<D>['state']>\n>['state']\n\n/**\n * The server handle's `call` record mirrored to the client, with\n * answers carrying `View` — the projected shape when the route mounts\n * `fetch(req, { view })`, the full state otherwise (the default).\n */\nexport type ActorClientCall<\n D extends AnyActorDefinition,\n View = ActorStateOf<D>,\n> = {\n readonly [K in keyof HandleOf<D>['call']]: HandleOf<D>['call'][K] extends (\n ...args: infer P\n ) => Promise<{ state: unknown; index: number }>\n ? (...args: P) => Promise<{ state: View; index: number }>\n : never\n}\n\nexport type ActorClient<\n D extends AnyActorDefinition,\n View = ActorStateOf<D>,\n> = {\n /** Typed calls over POST — the response is the answer. */\n readonly call: ActorClientCall<D, View>\n}\n\nexport type CreateActorClientOptions = {\n /** The route whose handler delegates to `handle.fetch` for this instance. */\n api: string\n /** Injectable transport (tests, custom auth headers). */\n fetch?: typeof globalThis.fetch\n}\n\nconst CALL_TIMEOUT_MS = 30_000\n\nconst callOverWire = async (\n fetchImpl: typeof globalThis.fetch,\n api: string,\n event: string,\n input: unknown,\n options?: ActorCallOptions,\n): Promise<ActorCallResult<unknown>> => {\n const response = await fetchImpl(api, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n event,\n ...(input === undefined ? {} : { input }),\n ...(options?.id === undefined ? {} : { messageId: options.id }),\n }),\n signal: AbortSignal.timeout(options?.timeoutMs ?? CALL_TIMEOUT_MS),\n })\n const body = (await response.json().catch(() => ({}))) as Record<\n string,\n unknown\n >\n if (response.status === 409 && typeof body['error'] === 'string') {\n throw new ActorRefusedError(\n typeof body['event'] === 'string' ? body['event'] : event,\n typeof body['messageId'] === 'string' ? body['messageId'] : '',\n body['error'],\n )\n }\n if (!response.ok) {\n const message =\n typeof body['error'] === 'string'\n ? body['error']\n : `actor call failed (${response.status})`\n throw new Error(message)\n }\n return body as ActorCallResult<unknown>\n}\n\n/**\n * A typed call surface for one actor instance's route. When the route\n * mounts a `view`, pass its shape as the second type argument so call\n * answers carry the projected state:\n * `createActorClient<typeof vault, PublicVault>({ api })`.\n */\nexport function createActorClient<\n D extends AnyActorDefinition,\n View = ActorStateOf<D>,\n>(options: CreateActorClientOptions): ActorClient<D, View> {\n const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis)\n const call = new Proxy(\n {},\n {\n get: (_target, event) =>\n typeof event === 'string'\n ? (input?: unknown, callOptions?: ActorCallOptions) =>\n callOverWire(fetchImpl, options.api, event, input, callOptions)\n : undefined,\n },\n ) as ActorClientCall<D, View>\n return { call }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqEA,MAAM,kBAAkB;AAExB,MAAM,eAAe,OACnB,WACA,KACA,OACA,OACA,YACsC;CACtC,MAAM,WAAW,MAAM,UAAU,KAAK;EACpC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,KAAK,UAAU;GACnB;GACA,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;GACvC,GAAI,SAAS,OAAO,KAAA,IAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,GAAG;EAC/D,CAAC;EACD,QAAQ,YAAY,QAAQ,SAAS,aAAa,eAAe;CACnE,CAAC;CACD,MAAM,OAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,aAAa,CAAC,EAAE;CAIpD,IAAI,SAAS,WAAW,OAAO,OAAO,KAAK,aAAa,UACtD,MAAM,IAAI,kBACR,OAAO,KAAK,aAAa,WAAW,KAAK,WAAW,OACpD,OAAO,KAAK,iBAAiB,WAAW,KAAK,eAAe,IAC5D,KAAK,QACP;CAEF,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,UACJ,OAAO,KAAK,aAAa,WACrB,KAAK,WACL,sBAAsB,SAAS,OAAO;EAC5C,MAAM,IAAI,MAAM,OAAO;CACzB;CACA,OAAO;AACT;;;;;;;AAQA,SAAgB,kBAGd,SAAyD;CACzD,MAAM,YAAY,QAAQ,SAAS,WAAW,MAAM,KAAK,UAAU;CAWnE,OAAO,EAAE,MAAA,IAVQ,MACf,CAAC,GACD,EACE,MAAM,SAAS,UACb,OAAO,UAAU,YACZ,OAAiB,gBAChB,aAAa,WAAW,QAAQ,KAAK,OAAO,OAAO,WAAW,IAChE,KAAA,EACR,CAEU,EAAE;AAChB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { a as ContractEvent, s as EventDefs } from "./reducer-DJKWm3cp.js";
|
|
2
|
+
import { i as ActorPresenceMap, o as ActorPresenceValues } from "./actor-shared-BACubf4x.js";
|
|
3
|
+
import { ActorClientCall, ActorStateOf, AnyActorDefinition } from "./actor-client.js";
|
|
4
|
+
import { Connection } from "./client.js";
|
|
5
|
+
//#region src/actor-react.d.ts
|
|
6
|
+
/** The server-to-client handoff: `await def.actor(id).state()`. */
|
|
7
|
+
type ActorSnapshot<S> = {
|
|
8
|
+
state: S;
|
|
9
|
+
index: number;
|
|
10
|
+
};
|
|
11
|
+
type UseActorOptions<View> = {
|
|
12
|
+
/** The route whose handler delegates to `handle.fetch` for this instance. */
|
|
13
|
+
api: string;
|
|
14
|
+
/** The instance id. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** The server-rendered fold — the stream resumes after `index`. */
|
|
17
|
+
initial: ActorSnapshot<View>;
|
|
18
|
+
/** This client's presence identity — required to call `setPresence`. */
|
|
19
|
+
participant?: string;
|
|
20
|
+
};
|
|
21
|
+
/** The definition's presence vocabulary, `never` when undeclared. */
|
|
22
|
+
type PresenceOf<D extends AnyActorDefinition> = D extends {
|
|
23
|
+
protocol?: infer P;
|
|
24
|
+
} ? NonNullable<P> extends {
|
|
25
|
+
presence: infer Values extends object;
|
|
26
|
+
} ? Values : never : never;
|
|
27
|
+
type UseActorResult<D extends AnyActorDefinition, View> = {
|
|
28
|
+
/** The actor's state, live: every commit streams in and replaces it. */
|
|
29
|
+
state: View;
|
|
30
|
+
/** The stream frontier. */
|
|
31
|
+
index: number;
|
|
32
|
+
connection: Connection;
|
|
33
|
+
/** Typed calls over POST — the response is the answer. */
|
|
34
|
+
call: ActorClientCall<D, View>;
|
|
35
|
+
/** The state-commit feed this browser has observed. */
|
|
36
|
+
events: ContractEvent<EventDefs>[];
|
|
37
|
+
/**
|
|
38
|
+
* The replicated presence map: participant → field → latest value.
|
|
39
|
+
* Peer-authored — render values like user input. Empty (and
|
|
40
|
+
* `setPresence` untyped-callable-with-nothing) when the protocol
|
|
41
|
+
* declares no presence.
|
|
42
|
+
*/
|
|
43
|
+
presence: ActorPresenceMap<PresenceOf<D>>;
|
|
44
|
+
/**
|
|
45
|
+
* Announce this client's presence fields (throttled, resent on
|
|
46
|
+
* reconnect; `null` clears a field). Requires `participant`.
|
|
47
|
+
*/
|
|
48
|
+
setPresence: (values: ActorPresenceValues<PresenceOf<D>>) => void;
|
|
49
|
+
};
|
|
50
|
+
/** Use one actor instance: live state plus typed calls. No provider. */
|
|
51
|
+
declare function useActor<D extends AnyActorDefinition, View = ActorStateOf<D>>(options: UseActorOptions<View>): UseActorResult<D, View>;
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ActorSnapshot, UseActorOptions, UseActorResult, useActor };
|
|
54
|
+
//# sourceMappingURL=actor-react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-react.d.ts","names":[],"sources":["../src/actor-react.ts"],"mappings":";;;;;;KA6CY,cAAc;EAAO,OAAO;EAAG;;KAE/B,gBAAgB;;EAE1B;;EAEA;;EAEA,SAAS,cAAc;;EAEvB;;;KAIG,WAAW,UAAU,sBAAsB;EAC9C,iBAAiB;IAEf,YAAY;EAAa,gBAAgB;IACvC;KAIM,eAAe,UAAU,oBAAoB;;EAEvD,OAAO;;EAEP;EACA,YAAY;;EAEZ,MAAM,gBAAgB,GAAG;;EAEzB,QAAQ,cAAc;;;;;;;EAOtB,UAAU,iBAAiB,WAAW;;;;;EAKtC,cAAc,QAAQ,oBAAoB,WAAW;;;iBA2BvC,SAAS,UAAU,oBAAoB,OAAO,aAAa,IACzE,SAAS,gBAAgB,QACxB,eAAe,GAAG"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createClient } from "./client.js";
|
|
3
|
+
import { useSession } from "./react.js";
|
|
4
|
+
import { l as openPresenceDefs } from "./actor-shared-DI7J5upy.js";
|
|
5
|
+
import { createActorClient } from "./actor-client.js";
|
|
6
|
+
//#region src/actor-react.ts
|
|
7
|
+
/**
|
|
8
|
+
* experimental-a2/actor/react — use an actor from React: live state
|
|
9
|
+
* plus typed calls, one hook. A thin wrapper: the state pump follows
|
|
10
|
+
* the SSE that `handle.fetch` serves (frontier resume, reconnection,
|
|
11
|
+
* heartbeat watchdog — both ends library code), and `call` is
|
|
12
|
+
* `createActorClient`'s typed surface (the framework-agnostic
|
|
13
|
+
* primitive for everything that isn't React).
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* // server component
|
|
17
|
+
* const initial = await vault.actor(id).state()
|
|
18
|
+
* // client component — typed by the server module, erased at build
|
|
19
|
+
* const { state, call, connection } = useActor<typeof vault>({
|
|
20
|
+
* api: `/vault/${id}`, // your route: auth, then handle.fetch
|
|
21
|
+
* id,
|
|
22
|
+
* initial,
|
|
23
|
+
* })
|
|
24
|
+
* await call.withdraw({ amount: 60 }) // ActorRefusedError on refusal
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The fold is library code (last write wins over state commits), so no
|
|
28
|
+
* reducers, schemas, or contracts appear in browser code. Live, not
|
|
29
|
+
* optimistic: handlers are server code deciding against serialized
|
|
30
|
+
* fresh state, so a local guess would be wrong exactly when the actor
|
|
31
|
+
* matters. On a view-projected mount, pass the projected shape as the
|
|
32
|
+
* second type argument — `useActor<typeof vault, PublicVault>` — and
|
|
33
|
+
* `initial`, `state`, and call answers all carry it.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The follower fold, untyped on purpose: the state type comes from the
|
|
37
|
+
* caller's `initial` handoff, and the fold never inspects state.
|
|
38
|
+
* `initialState` is never rendered — `initial` is required, so every
|
|
39
|
+
* session hydrates before it connects.
|
|
40
|
+
*/
|
|
41
|
+
const followerReducer = {
|
|
42
|
+
name: "a2.actor.follower.v1",
|
|
43
|
+
events: {},
|
|
44
|
+
presence: openPresenceDefs,
|
|
45
|
+
fold: (state, event) => event.type === "a2.actor.state" ? event.payload.state : state,
|
|
46
|
+
initialState: void 0,
|
|
47
|
+
stateSchema: void 0
|
|
48
|
+
};
|
|
49
|
+
const clients = /* @__PURE__ */ new Map();
|
|
50
|
+
/** Use one actor instance: live state plus typed calls. No provider. */
|
|
51
|
+
function useActor(options) {
|
|
52
|
+
const { api } = options;
|
|
53
|
+
let client = clients.get(api);
|
|
54
|
+
if (!client) {
|
|
55
|
+
client = createClient({
|
|
56
|
+
reducer: followerReducer,
|
|
57
|
+
api
|
|
58
|
+
});
|
|
59
|
+
clients.set(api, client);
|
|
60
|
+
}
|
|
61
|
+
const session = useSession(client, options.id, {
|
|
62
|
+
hydrate: options.initial,
|
|
63
|
+
...options.participant === void 0 ? {} : { participant: options.participant }
|
|
64
|
+
});
|
|
65
|
+
const { call } = createActorClient({ api });
|
|
66
|
+
return {
|
|
67
|
+
state: session.state,
|
|
68
|
+
index: session.index,
|
|
69
|
+
connection: session.connection,
|
|
70
|
+
call,
|
|
71
|
+
events: session.events,
|
|
72
|
+
presence: session.presence,
|
|
73
|
+
setPresence: session.setPresence
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { useActor };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=actor-react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-react.js","names":[],"sources":["../src/actor-react.ts"],"sourcesContent":["'use client'\n/**\n * experimental-a2/actor/react — use an actor from React: live state\n * plus typed calls, one hook. A thin wrapper: the state pump follows\n * the SSE that `handle.fetch` serves (frontier resume, reconnection,\n * heartbeat watchdog — both ends library code), and `call` is\n * `createActorClient`'s typed surface (the framework-agnostic\n * primitive for everything that isn't React).\n *\n * ```tsx\n * // server component\n * const initial = await vault.actor(id).state()\n * // client component — typed by the server module, erased at build\n * const { state, call, connection } = useActor<typeof vault>({\n * api: `/vault/${id}`, // your route: auth, then handle.fetch\n * id,\n * initial,\n * })\n * await call.withdraw({ amount: 60 }) // ActorRefusedError on refusal\n * ```\n *\n * The fold is library code (last write wins over state commits), so no\n * reducers, schemas, or contracts appear in browser code. Live, not\n * optimistic: handlers are server code deciding against serialized\n * fresh state, so a local guess would be wrong exactly when the actor\n * matters. On a view-projected mount, pass the projected shape as the\n * second type argument — `useActor<typeof vault, PublicVault>` — and\n * `initial`, `state`, and call answers all carry it.\n */\n\nimport { ACTOR_STATE_EVENT, openPresenceDefs } from './actor-shared.ts'\nimport type { ActorPresenceMap, ActorPresenceValues } from './actor-shared.ts'\nimport {\n createActorClient,\n type ActorClientCall,\n type ActorStateOf,\n type AnyActorDefinition,\n} from './actor-client.ts'\nimport { createClient } from './client.ts'\nimport type { A2Client, Connection } from './client.ts'\nimport type { ContractEvent, EventDefs, PresenceDefs } from './contract.ts'\nimport type { Reducer } from './reducer.ts'\nimport { useSession } from './react.ts'\n\n/** The server-to-client handoff: `await def.actor(id).state()`. */\nexport type ActorSnapshot<S> = { state: S; index: number }\n\nexport type UseActorOptions<View> = {\n /** The route whose handler delegates to `handle.fetch` for this instance. */\n api: string\n /** The instance id. */\n id: string\n /** The server-rendered fold — the stream resumes after `index`. */\n initial: ActorSnapshot<View>\n /** This client's presence identity — required to call `setPresence`. */\n participant?: string\n}\n\n/** The definition's presence vocabulary, `never` when undeclared. */\ntype PresenceOf<D extends AnyActorDefinition> = D extends {\n protocol?: infer P\n}\n ? NonNullable<P> extends { presence: infer Values extends object }\n ? Values\n : never\n : never\n\nexport type UseActorResult<D extends AnyActorDefinition, View> = {\n /** The actor's state, live: every commit streams in and replaces it. */\n state: View\n /** The stream frontier. */\n index: number\n connection: Connection\n /** Typed calls over POST — the response is the answer. */\n call: ActorClientCall<D, View>\n /** The state-commit feed this browser has observed. */\n events: ContractEvent<EventDefs>[]\n /**\n * The replicated presence map: participant → field → latest value.\n * Peer-authored — render values like user input. Empty (and\n * `setPresence` untyped-callable-with-nothing) when the protocol\n * declares no presence.\n */\n presence: ActorPresenceMap<PresenceOf<D>>\n /**\n * Announce this client's presence fields (throttled, resent on\n * reconnect; `null` clears a field). Requires `participant`.\n */\n setPresence: (values: ActorPresenceValues<PresenceOf<D>>) => void\n}\n\n/**\n * The follower fold, untyped on purpose: the state type comes from the\n * caller's `initial` handoff, and the fold never inspects state.\n * `initialState` is never rendered — `initial` is required, so every\n * session hydrates before it connects.\n */\nconst followerReducer: Reducer<EventDefs, unknown, PresenceDefs> = {\n name: 'a2.actor.follower.v1',\n events: {},\n // The open vocabulary ('*' catch-all): the browser cannot know a\n // server-defined field list, so the server stays the authority and\n // the protocol types honest callers at compile time.\n presence: openPresenceDefs,\n fold: (state, event) =>\n event.type === ACTOR_STATE_EVENT\n ? (event.payload as { state: unknown }).state\n : state,\n initialState: undefined,\n stateSchema: undefined,\n}\n\nconst clients = new Map<string, A2Client<EventDefs, unknown, PresenceDefs>>()\n\n/** Use one actor instance: live state plus typed calls. No provider. */\nexport function useActor<D extends AnyActorDefinition, View = ActorStateOf<D>>(\n options: UseActorOptions<View>,\n): UseActorResult<D, View> {\n const { api } = options\n let client = clients.get(api)\n if (!client) {\n client = createClient({ reducer: followerReducer, api })\n clients.set(api, client)\n }\n const session = useSession(client, options.id, {\n hydrate: options.initial,\n ...(options.participant === undefined\n ? {}\n : { participant: options.participant }),\n })\n const { call } = createActorClient<D, View>({ api })\n return {\n state: session.state as View,\n index: session.index,\n connection: session.connection,\n call,\n events: session.events,\n presence: session.presence as ActorPresenceMap<PresenceOf<D>>,\n setPresence: session.setPresence as (\n values: ActorPresenceValues<PresenceOf<D>>,\n ) => void,\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiGA,MAAM,kBAA6D;CACjE,MAAM;CACN,QAAQ,CAAC;CAIT,UAAU;CACV,OAAO,OAAO,UACZ,MAAM,SAAA,mBACD,MAAM,QAA+B,QACtC;CACN,cAAc,KAAA;CACd,aAAa,KAAA;AACf;AAEA,MAAM,0BAAU,IAAI,IAAwD;;AAG5E,SAAgB,SACd,SACyB;CACzB,MAAM,EAAE,QAAQ;CAChB,IAAI,SAAS,QAAQ,IAAI,GAAG;CAC5B,IAAI,CAAC,QAAQ;EACX,SAAS,aAAa;GAAE,SAAS;GAAiB;EAAI,CAAC;EACvD,QAAQ,IAAI,KAAK,MAAM;CACzB;CACA,MAAM,UAAU,WAAW,QAAQ,QAAQ,IAAI;EAC7C,SAAS,QAAQ;EACjB,GAAI,QAAQ,gBAAgB,KAAA,IACxB,CAAC,IACD,EAAE,aAAa,QAAQ,YAAY;CACzC,CAAC;CACD,MAAM,EAAE,SAAS,kBAA2B,EAAE,IAAI,CAAC;CACnD,OAAO;EACL,OAAO,QAAQ;EACf,OAAO,QAAQ;EACf,YAAY,QAAQ;EACpB;EACA,QAAQ,QAAQ;EAChB,UAAU,QAAQ;EAClB,aAAa,QAAQ;CAGvB;AACF"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { h as StandardSchemaV1 } from "./reducer-DJKWm3cp.js";
|
|
2
|
+
import { f as ScheduleTiming } from "./server-CKY3_lbw.js";
|
|
3
|
+
//#region src/actor-shared.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* A handler answered by throwing `NonRetriableError`: the event
|
|
6
|
+
* settled, the state did not change. Isomorphic — the server raises it
|
|
7
|
+
* from calls and `fetch()` serializes it as a 409; the client's `call`
|
|
8
|
+
* proxy revives it from that response.
|
|
9
|
+
*/
|
|
10
|
+
declare class ActorRefusedError extends Error {
|
|
11
|
+
/** The refused event's name. */
|
|
12
|
+
readonly event: string;
|
|
13
|
+
/** The refused event's message id. */
|
|
14
|
+
readonly messageId: string;
|
|
15
|
+
constructor(event: string, messageId: string, message: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The declaration an actor is defined over: its state shape and its
|
|
19
|
+
* event vocabulary, as types. Core A2 contracts are schemas because
|
|
20
|
+
* the wire is untrusted; actor protocols are types because your
|
|
21
|
+
* server is trusted — same contract-first design language, dialed to
|
|
22
|
+
* the trust level.
|
|
23
|
+
*/
|
|
24
|
+
type ActorProtocol = {
|
|
25
|
+
state: object;
|
|
26
|
+
events: object;
|
|
27
|
+
/**
|
|
28
|
+
* Optional third vocabulary: presence field → value type. Ephemeral
|
|
29
|
+
* audience state (who is here, cursors), replicated to subscribers
|
|
30
|
+
* and never stored in the log. Declaring it requires `presence:
|
|
31
|
+
* true` in the actor options — the wire bit types cannot carry.
|
|
32
|
+
*/
|
|
33
|
+
presence?: object;
|
|
34
|
+
};
|
|
35
|
+
/** The protocol's presence vocabulary, `never` when undeclared. */
|
|
36
|
+
type ActorPresenceOf<D extends ActorProtocol> = D extends {
|
|
37
|
+
presence: infer P extends object;
|
|
38
|
+
} ? P : never;
|
|
39
|
+
/** A `setPresence` patch: changed fields, `null` clears one. */
|
|
40
|
+
type ActorPresenceValues<P> = { [F in keyof P & string]?: P[F] | null; };
|
|
41
|
+
/**
|
|
42
|
+
* The replicated presence map: participant → field → latest value,
|
|
43
|
+
* last write wins per field. Values are peer-authored — render them
|
|
44
|
+
* like user input.
|
|
45
|
+
*/
|
|
46
|
+
type ActorPresenceMap<P> = {
|
|
47
|
+
[participant: string]: { [F in keyof P & string]?: {
|
|
48
|
+
value: P[F];
|
|
49
|
+
seen: number;
|
|
50
|
+
at: Date;
|
|
51
|
+
}; };
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* The typed self-send surface: one method per declared event —
|
|
55
|
+
* `ctx.send.transfer({ ref, amount })`. Buffered, not immediate: every
|
|
56
|
+
* send requested during a handler commits atomically with that
|
|
57
|
+
* handler's completion (a serial handler's state commit, a concurrent
|
|
58
|
+
* handler's settlement), riding core's returned-events semantics — so
|
|
59
|
+
* a reserve-and-trigger can never half-happen, and re-runs converge on
|
|
60
|
+
* the same deterministic message ids.
|
|
61
|
+
*/
|
|
62
|
+
type ActorSend<E> = { readonly [K in keyof E]: {} extends E[K] ? (input?: E[K]) => void : (input: E[K]) => void; };
|
|
63
|
+
/**
|
|
64
|
+
* Timing for a scheduled event: exactly `{ delay: '5d' }` or
|
|
65
|
+
* `{ at: Date }` (core §6 semantics — relative delays anchor to the
|
|
66
|
+
* triggering message's durable `createdAt`, so re-runs resolve the
|
|
67
|
+
* same due time). `name` overrides the timer's identity — it defaults
|
|
68
|
+
* to the target event name, scoped to the triggering message, so one
|
|
69
|
+
* handler run gets one timer per target event unless named apart.
|
|
70
|
+
*/
|
|
71
|
+
type ActorScheduleOptions = ScheduleTiming & {
|
|
72
|
+
name?: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The typed durable-timer surface: one method per declared event —
|
|
76
|
+
* `ctx.schedule.refund({ ref: ctx.id }, { delay: '1h' })`. Unlike
|
|
77
|
+
* `send`, scheduling is immediate, not buffered: it awaits provider
|
|
78
|
+
* acceptance at call time (a timer is provider-side, not a log row),
|
|
79
|
+
* so a handler that schedules and then refuses has still armed the
|
|
80
|
+
* timer. That is safe by the guarded-delivery idiom — the stale timer
|
|
81
|
+
* fires into an idempotent no-op — but it is the one exception to
|
|
82
|
+
* "refusals are total". Requires a configured `scheduler`.
|
|
83
|
+
*/
|
|
84
|
+
type ActorScheduleSend<E> = { readonly [K in keyof E]: {} extends E[K] ? (input: E[K] | undefined, options: ActorScheduleOptions) => Promise<void> : (input: E[K], options: ActorScheduleOptions) => Promise<void>; };
|
|
85
|
+
/** What a serial handler receives alongside its typed input. */
|
|
86
|
+
type ActorContext<D extends ActorProtocol> = {
|
|
87
|
+
/** Mutable draft, committed atomically with the handler's completion. */
|
|
88
|
+
state: D["state"];
|
|
89
|
+
/** The message id — stable across re-runs; the idempotency key for external I/O. */
|
|
90
|
+
id: string;
|
|
91
|
+
/** Durable 1-based dispatch ordinal of this message. */
|
|
92
|
+
attempt: number;
|
|
93
|
+
/** The ordinary A2 handler signal — fires on claim expiry or supersession. */
|
|
94
|
+
signal: AbortSignal;
|
|
95
|
+
/** Typed buffered self-send — committed atomically with the state commit. */
|
|
96
|
+
send: ActorSend<D["events"]>;
|
|
97
|
+
/** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
|
|
98
|
+
schedule: ActorScheduleSend<D["events"]>;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* What a `concurrent: true` handler receives. No draft — concurrent
|
|
102
|
+
* handlers run off the lane, in parallel, so state is a snapshot read
|
|
103
|
+
* (honest about staleness) and mutations happen by sending events
|
|
104
|
+
* whose serial handlers decide against fresh state.
|
|
105
|
+
*/
|
|
106
|
+
type ActorConcurrentContext<D extends ActorProtocol> = {
|
|
107
|
+
/** The message id — stable across re-runs; the idempotency key for external I/O. */
|
|
108
|
+
id: string;
|
|
109
|
+
/** Durable 1-based dispatch ordinal of this message. */
|
|
110
|
+
attempt: number;
|
|
111
|
+
/** The ordinary A2 handler signal — fires on claim expiry or supersession. */
|
|
112
|
+
signal: AbortSignal;
|
|
113
|
+
/** Snapshot read — observational; the world moves while this runs. */
|
|
114
|
+
state(): Promise<{
|
|
115
|
+
state: D["state"];
|
|
116
|
+
index: number;
|
|
117
|
+
}>;
|
|
118
|
+
/** Typed buffered self-send — committed atomically with settlement. */
|
|
119
|
+
send: ActorSend<D["events"]>;
|
|
120
|
+
/** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
|
|
121
|
+
schedule: ActorScheduleSend<D["events"]>;
|
|
122
|
+
};
|
|
123
|
+
type ActorStatePayload<S> = {
|
|
124
|
+
state: S;
|
|
125
|
+
/** The event whose handler committed this state. */
|
|
126
|
+
event: string;
|
|
127
|
+
/** The message (invocation event) id this state answers. */
|
|
128
|
+
message: string;
|
|
129
|
+
};
|
|
130
|
+
/** The event vocabulary a client needs to follow an actor's state. */
|
|
131
|
+
type ActorClientEventDefs<S> = {
|
|
132
|
+
"a2.actor.state": StandardSchemaV1<ActorStatePayload<S>>;
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
export { ActorPresenceOf as a, ActorRefusedError as c, ActorSend as d, ActorPresenceMap as i, ActorScheduleOptions as l, ActorConcurrentContext as n, ActorPresenceValues as o, ActorContext as r, ActorProtocol as s, ActorClientEventDefs as t, ActorScheduleSend as u };
|
|
136
|
+
//# sourceMappingURL=actor-shared-BACubf4x.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-shared-BACubf4x.d.ts","names":[],"sources":["../src/actor-shared.ts"],"mappings":";;;;;;;;;cAgCa,0BAA0B;;WAE5B;;WAEA;EACT,YAAY,eAAe,mBAAmB;;;;;;;;;KAepC;EACV;EACA;;;;;;;EAOA;;;KAIU,gBAAgB,UAAU,iBAAiB;EACrD,gBAAgB;IAEd;;KAIQ,oBAAoB,QAC7B,WAAW,cAAc,EAAE;;;;;;KAQlB,iBAAiB;GAExB,yBAAA,WAAW;IAAgB,OAAO,EAAE;IAAI;IAAc,IAAI;;;;;;;;;;;;KAanD,UAAU,iBACV,WAAW,eAAe,EAAE,MACjC,QAAQ,EAAE,eACV,OAAO,EAAE;;;;;;;;;KAWJ,uBAAuB;EAAmB;;;;;;;;;;;;KAY1C,kBAAkB,iBAClB,WAAW,eAAe,EAAE,MACjC,OAAO,EAAE,gBAAgB,SAAS,yBAAyB,iBAC3D,OAAO,EAAE,IAAI,SAAS,yBAAyB;;KAI1C,aAAa,UAAU;;EAEjC,OAAO;;EAEP;;EAEA;;EAEA,QAAQ;;EAER,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;;;;;;;KASlB,uBAAuB,UAAU;;EAE3C;;EAEA;;EAEA,QAAQ;;EAER,SAAS;IAAU,OAAO;IAAY;;;EAEtC,MAAM,UAAU;;EAEhB,UAAU,kBAAkB;;KAGlB,kBAAkB;EAC5B,OAAO;;EAEP;;EAEA;;;KAWU,qBAAqB;EAC/B,kBAAkB,iBAAiB,kBAAkB"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { t as contract } from "./contract-48bUMgcL.js";
|
|
2
|
+
//#region src/actor-shared.ts
|
|
3
|
+
/**
|
|
4
|
+
* Internal shared vocabulary of the actor module — the reserved event
|
|
5
|
+
* envelopes, the protocol and context types, the state fold. Not an
|
|
6
|
+
* entry point: the public surfaces are experimental-a2/actor (server),
|
|
7
|
+
* /actor/client, and /actor/react.
|
|
8
|
+
*
|
|
9
|
+
* An actor is a durable mailbox with memory: one session per instance,
|
|
10
|
+
* events processed one at a time (a lane) unless a handler opts out
|
|
11
|
+
* with `concurrent: true`, state materialized as `a2.actor.state`
|
|
12
|
+
* events committed atomically with each handler's completion.
|
|
13
|
+
*/
|
|
14
|
+
/** Event type of a committed state change — one per completed serial handler. */
|
|
15
|
+
const ACTOR_STATE_EVENT = "a2.actor.state";
|
|
16
|
+
/** Event type of a refusal — a handler that answered by throwing NonRetriableError. */
|
|
17
|
+
const ACTOR_FAILED_EVENT = "a2.actor.failed";
|
|
18
|
+
/** The per-instance lane every serial handler shares — the state's write lock. */
|
|
19
|
+
const ACTOR_LANE = "a2.actor";
|
|
20
|
+
/** Reducer identity — bump when the fold's meaning changes. */
|
|
21
|
+
const ACTOR_REDUCER_NAME = "a2.actor.v1";
|
|
22
|
+
/**
|
|
23
|
+
* A handler answered by throwing `NonRetriableError`: the event
|
|
24
|
+
* settled, the state did not change. Isomorphic — the server raises it
|
|
25
|
+
* from calls and `fetch()` serializes it as a 409; the client's `call`
|
|
26
|
+
* proxy revives it from that response.
|
|
27
|
+
*/
|
|
28
|
+
var ActorRefusedError = class extends Error {
|
|
29
|
+
/** The refused event's name. */
|
|
30
|
+
event;
|
|
31
|
+
/** The refused event's message id. */
|
|
32
|
+
messageId;
|
|
33
|
+
constructor(event, messageId, message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "ActorRefusedError";
|
|
36
|
+
this.event = event;
|
|
37
|
+
this.messageId = messageId;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const issue = (message) => ({ issues: [{ message }] });
|
|
41
|
+
const schema = (label, parse) => ({ "~standard": {
|
|
42
|
+
version: 1,
|
|
43
|
+
vendor: "a2",
|
|
44
|
+
validate(value) {
|
|
45
|
+
try {
|
|
46
|
+
return parse(value);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return issue(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} });
|
|
52
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
53
|
+
/** True for plain JSON trees — what the log can store without loss. */
|
|
54
|
+
function isJsonTree(value, seen = /* @__PURE__ */ new Set()) {
|
|
55
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
56
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
57
|
+
if (typeof value !== "object") return false;
|
|
58
|
+
if (seen.has(value)) return false;
|
|
59
|
+
seen.add(value);
|
|
60
|
+
const valid = Array.isArray(value) ? value.every((item) => isJsonTree(item, seen)) : Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every((item) => item === void 0 || isJsonTree(item, seen));
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
return valid;
|
|
63
|
+
}
|
|
64
|
+
/** Per-field ceiling for presence values — presence is a cursor, not a document. */
|
|
65
|
+
const PRESENCE_VALUE_MAX_BYTES = 8192;
|
|
66
|
+
/**
|
|
67
|
+
* The open presence vocabulary: the '*' catch-all validates any field
|
|
68
|
+
* name against the JSON floor. The protocol types the vocabulary at
|
|
69
|
+
* compile time; the route's `authorize` sees every set before
|
|
70
|
+
* acceptance; renderers treat values as user input. Meaning has three
|
|
71
|
+
* guards — this schema only owns the bytes.
|
|
72
|
+
*/
|
|
73
|
+
const openPresenceDefs = Object.freeze({ "*": { "~standard": {
|
|
74
|
+
version: 1,
|
|
75
|
+
vendor: "a2",
|
|
76
|
+
validate: (value) => {
|
|
77
|
+
if (!isJsonTree(value)) return { issues: [{ message: "presence values must be plain JSON trees" }] };
|
|
78
|
+
if (JSON.stringify(value).length > 8192) return { issues: [{ message: `presence values are capped at ${PRESENCE_VALUE_MAX_BYTES} bytes` }] };
|
|
79
|
+
return { value };
|
|
80
|
+
}
|
|
81
|
+
} } });
|
|
82
|
+
/**
|
|
83
|
+
* The `a2.actor.state` envelope: provenance plus the state, held to a
|
|
84
|
+
* plain JSON tree (a `Date` or `Map` in state fails loudly at commit,
|
|
85
|
+
* never silently coerces in the log).
|
|
86
|
+
*/
|
|
87
|
+
function actorStateSchema() {
|
|
88
|
+
return schema(ACTOR_STATE_EVENT, (value) => {
|
|
89
|
+
if (!isRecord(value) || typeof value["event"] !== "string" || typeof value["message"] !== "string") return issue(`invalid ${ACTOR_STATE_EVENT} payload`);
|
|
90
|
+
const state = value["state"];
|
|
91
|
+
if (!isJsonTree(state)) return issue("the actor state must be a plain JSON tree");
|
|
92
|
+
return { value: {
|
|
93
|
+
state,
|
|
94
|
+
event: value["event"],
|
|
95
|
+
message: value["message"]
|
|
96
|
+
} };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const actorFailedSchema = schema(ACTOR_FAILED_EVENT, (value) => {
|
|
100
|
+
if (!isRecord(value) || typeof value["message"] !== "string" || typeof value["event"] !== "string" || typeof value["error"] !== "string") return issue(`invalid ${ACTOR_FAILED_EVENT} payload`);
|
|
101
|
+
return { value: {
|
|
102
|
+
message: value["message"],
|
|
103
|
+
event: value["event"],
|
|
104
|
+
error: value["error"]
|
|
105
|
+
} };
|
|
106
|
+
});
|
|
107
|
+
/**
|
|
108
|
+
* The library fold over an actor's state commits — last write wins.
|
|
109
|
+
* Pure library code: a browser bundle folds an actor's live state with
|
|
110
|
+
* no user code, so the actor definition itself stays server-only
|
|
111
|
+
* (import the server module's type for typing, this reducer for data).
|
|
112
|
+
*/
|
|
113
|
+
function actorReducer(options) {
|
|
114
|
+
if (!isJsonTree(options.state)) throw new TypeError(`actor '${options.name}': the initial state must be a plain JSON tree`);
|
|
115
|
+
const events = { [ACTOR_STATE_EVENT]: actorStateSchema() };
|
|
116
|
+
return contract({
|
|
117
|
+
name: options.name,
|
|
118
|
+
events
|
|
119
|
+
}).reducer({
|
|
120
|
+
name: ACTOR_REDUCER_NAME,
|
|
121
|
+
initialState: options.state
|
|
122
|
+
}).fold((state, event) => event.type === "a2.actor.state" ? event.payload.state : state);
|
|
123
|
+
}
|
|
124
|
+
//#endregion
|
|
125
|
+
export { actorFailedSchema as a, isJsonTree as c, ActorRefusedError as i, openPresenceDefs as l, ACTOR_LANE as n, actorReducer as o, ACTOR_STATE_EVENT as r, actorStateSchema as s, ACTOR_FAILED_EVENT as t };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=actor-shared-DI7J5upy.js.map
|