experimental-a2 0.10.0 → 0.11.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/CHANGELOG.md +39 -0
- package/dist/actor-D_54lz_1.d.ts +310 -0
- package/dist/actor-D_54lz_1.d.ts.map +1 -0
- package/dist/actor-client.d.ts +13 -4
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +63 -7
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -4
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +16 -2
- package/dist/actor-react.js.map +1 -1
- package/dist/actor.d.ts +2 -176
- package/dist/actor.js +13 -2
- package/dist/actor.js.map +1 -1
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.js +2 -2
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/client-Bf6uSEAk.js +1342 -0
- package/dist/client-Bf6uSEAk.js.map +1 -0
- package/dist/client-P_NNNRM-.d.ts +243 -0
- package/dist/client-P_NNNRM-.d.ts.map +1 -0
- package/dist/client.d.ts +2 -208
- package/dist/client.js +2 -1206
- package/dist/errors-DCk6ch5n.js.map +1 -1
- package/dist/errors-DvhSXnxk.d.ts +28 -0
- package/dist/errors-DvhSXnxk.d.ts.map +1 -0
- package/dist/index.d.ts +3 -35
- package/dist/{internal-DRXJ56EI.js → internal-Dq2qYxou.js} +2 -2
- package/dist/{internal-DRXJ56EI.js.map → internal-Dq2qYxou.js.map} +1 -1
- package/dist/platform-B4TnJtWu.js +34 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +3 -1
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -2
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DlLyvaSH.js → server-Dkz2a84E.js} +165 -62
- package/dist/server-Dkz2a84E.js.map +1 -0
- package/dist/{server-DgCrSuhB.d.ts → server-DwPrMqHB.d.ts} +2 -2
- package/dist/{server-DgCrSuhB.d.ts.map → server-DwPrMqHB.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{store-DGHeBtIQ.d.ts → store-DtDOWLSn.d.ts} +4 -5
- package/dist/{store-DGHeBtIQ.d.ts.map → store-DtDOWLSn.d.ts.map} +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +1 -1
- package/dist/{store-polling-6DW7F1DT.js → store-polling-CmxUbV93.js} +56 -7
- package/dist/store-polling-CmxUbV93.js.map +1 -0
- package/dist/store-postgres.d.ts +6 -4
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +701 -53
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-C7-XZyW9.js +94 -0
- package/dist/store-presence-polling-C7-XZyW9.js.map +1 -0
- package/dist/store-redis-http.d.ts +2 -2
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +184 -38
- package/dist/store-redis-http.js.map +1 -1
- package/dist/{store-redis-core-z-ykbyMg.js → store-redis-notify-BUCyXOn0.js} +491 -27
- package/dist/store-redis-notify-BUCyXOn0.js.map +1 -0
- package/dist/store-redis.d.ts +5 -13
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +27 -271
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +1 -1
- package/dist/{wire--yji6mO3.js → wire-BO5wWCb1.js} +18 -16
- package/dist/wire-BO5wWCb1.js.map +1 -0
- package/docs/actors/04-routes.mdx +40 -8
- package/docs/guides/03-react.mdx +52 -10
- package/docs/guides/05-production.mdx +54 -5
- package/docs/guides/09-presence.mdx +12 -7
- package/docs/guides/10-transports.mdx +15 -2
- package/docs/reference/01-api.mdx +60 -16
- package/docs/reference/02-errors.mdx +45 -11
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/actor-client.ts +114 -14
- package/src/actor-react.ts +18 -3
- package/src/actor.ts +16 -1
- package/src/client-errors.ts +51 -0
- package/src/client.ts +253 -96
- package/src/errors.ts +4 -9
- package/src/index.ts +1 -1
- package/src/internal.ts +1 -1
- package/src/postgres-notification-scope.ts +134 -0
- package/src/postgres-notifications.ts +244 -0
- package/src/postgres-pool.ts +65 -0
- package/src/postgres-resources.ts +185 -0
- package/src/presence-recovery.ts +120 -0
- package/src/react.ts +3 -0
- package/src/redis-http-subscriptions.ts +199 -0
- package/src/server.ts +146 -19
- package/src/session-socket.ts +25 -7
- package/src/sse.ts +17 -0
- package/src/store-polling.ts +32 -12
- package/src/store-postgres.ts +292 -75
- package/src/store-presence-polling.ts +125 -0
- package/src/store-redis-core.ts +37 -30
- package/src/store-redis-http.ts +51 -45
- package/src/store-redis-notify.ts +464 -0
- package/src/store-redis.ts +9 -364
- package/src/store.ts +3 -4
- package/src/stream-activity.ts +32 -0
- package/src/wire.ts +39 -15
- package/dist/actor-shared-USo5MyuF.d.ts +0 -136
- package/dist/actor-shared-USo5MyuF.d.ts.map +0 -1
- package/dist/actor.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server-DlLyvaSH.js.map +0 -1
- package/dist/store-polling-6DW7F1DT.js.map +0 -1
- package/dist/store-redis-core-z-ykbyMg.js.map +0 -1
- package/dist/wire--yji6mO3.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bcd0782: Adopt actor call answers into React's live state before resolving, without
|
|
8
|
+
waiting for the stream. Add authorized, view-projected snapshot reads,
|
|
9
|
+
`client.state()`, `onSnapshot`, and `useActor().refresh()`. Older snapshots
|
|
10
|
+
cannot overwrite newer state. Track stream progress separately so delayed
|
|
11
|
+
commits still enter the event feed and reconnects resume without skipping them.
|
|
12
|
+
Expose HTTP status through `ActorRequestError`
|
|
13
|
+
so clients can distinguish request failures from handler refusals.
|
|
14
|
+
- 7bfab77: Add a caller-configurable reconnection policy and a paused connection state. Paused sessions retain their state, optimistic writes, and connection leases; `session.reconnect()` resumes them explicitly.
|
|
15
|
+
|
|
16
|
+
Expose `A2ClientError extends A2Error` from the client entry point for server and client failures, preserving available HTTP, WebSocket, and timeout details. Multiplexed subscription failures carry structured errors.
|
|
17
|
+
|
|
18
|
+
- 853fa79: Back idle event and presence polling off to two seconds. Local pushes reactivate idle streams: SSE refreshes from the current cursor without reconnect backoff, while WebSockets wake the attached session iterator in place. Pending writes keep their stream active until their acknowledged events arrive.
|
|
19
|
+
|
|
20
|
+
Add `session.reconnect()` and `useSession().reconnect()` to restart a leased live subscription after custom REST mutations. Reconnection preserves state, pending writes, and leases. Multiplexed WebSockets re-subscribe the selected session without replacing the shared socket.
|
|
21
|
+
|
|
22
|
+
Stop the previous session subscription before authorizing its replacement, so denied or failed reconnects cannot retain access through the old lane.
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- f92e00e: Reduce live-stream read overhead: Postgres selects only the six public event columns, and Redis HTTP reads public events directly without fetching dispatch metadata. Redis live reads use bounded pages while preserving cursor-based replay and delivery.
|
|
27
|
+
- 6080ac2: Add an optional listenConnectionString to the Postgres store for a direct LISTEN connection. Stores share query pools and listeners across module copies in the same runtime, without sharing application state. Transactional triggers notify event and presence subscribers across writers. Reconnects catch up, and the last subscriber releases the listener.
|
|
28
|
+
|
|
29
|
+
Pools created by A2 use ambient waitUntil on Vercel to let idle connections close before suspension while preserving reuse between requests.
|
|
30
|
+
|
|
31
|
+
Keep LISTEN startup errors visible while allowing transient presence seed-read failures to recover.
|
|
32
|
+
|
|
33
|
+
Keep requested presence snapshots independent of listener unsubscribe, returning the real database state while cancelled streams discard late results.
|
|
34
|
+
|
|
35
|
+
- f13c2cb: Stop idle event and presence subscriptions when clients disconnect or streams reach their invocation deadline. Cancellation now reaches the underlying store iterator while a session is waiting for its next event, preventing abandoned streams from continuing to poll the database.
|
|
36
|
+
- e65dc9a: Use Upstash HTTP subscriptions for live events and presence without extra configuration. Share subscriptions across local viewers, close them on disconnect, and catch up after reconnect. Retain ten-second event safety reads for missed notifications. Publish wake-ups inside Redis write scripts, removing the separate request and application crash gap. Fall back to adaptive event and presence polling when an HTTP endpoint explicitly lacks subscription support.
|
|
37
|
+
|
|
38
|
+
Reconcile presence deletions after subscription reconnects while preserving newer patches and discarding superseded initial snapshots.
|
|
39
|
+
|
|
40
|
+
Recover from initial presence seed-read failures without suppressing foreground snapshot errors.
|
|
41
|
+
|
|
3
42
|
## 0.10.0
|
|
4
43
|
|
|
5
44
|
### Minor Changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { h as StandardSchemaV1, i as Contract, s as EventDefs, t as Reducer } from "./reducer-DJKWm3cp.js";
|
|
2
|
+
import { t as A2Store } from "./store-DtDOWLSn.js";
|
|
3
|
+
import { f as ScheduleTiming, m as Session, n as A2Server, t as A2Scheduler } from "./server-DwPrMqHB.js";
|
|
4
|
+
import { i as A2Telemetry } from "./telemetry-CpeclqB2.js";
|
|
5
|
+
//#region src/actor-shared.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* A handler answered by throwing `NonRetriableError`: the event
|
|
8
|
+
* settled, the state did not change. Isomorphic — the server raises it
|
|
9
|
+
* from calls and `fetch()` serializes it as a 409; the client's `call`
|
|
10
|
+
* proxy revives it from that response.
|
|
11
|
+
*/
|
|
12
|
+
declare class ActorRefusedError extends Error {
|
|
13
|
+
/** The refused event's name. */
|
|
14
|
+
readonly event: string;
|
|
15
|
+
/** The refused event's message id. */
|
|
16
|
+
readonly messageId: string;
|
|
17
|
+
constructor(event: string, messageId: string, message: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The declaration an actor is defined over: its state shape and its
|
|
21
|
+
* event vocabulary, as types. Core A2 contracts are schemas because
|
|
22
|
+
* the wire is untrusted; actor protocols are types because your
|
|
23
|
+
* server is trusted — same contract-first design language, dialed to
|
|
24
|
+
* the trust level.
|
|
25
|
+
*/
|
|
26
|
+
type ActorProtocol = {
|
|
27
|
+
state: object;
|
|
28
|
+
events: object;
|
|
29
|
+
/**
|
|
30
|
+
* Optional third vocabulary: presence field → value type. Ephemeral
|
|
31
|
+
* audience state (who is here, cursors), replicated to subscribers
|
|
32
|
+
* and never stored in the log. Declaring it requires `presence:
|
|
33
|
+
* true` in the actor options — the wire bit types cannot carry.
|
|
34
|
+
*/
|
|
35
|
+
presence?: object;
|
|
36
|
+
};
|
|
37
|
+
/** The protocol's presence vocabulary, `never` when undeclared. */
|
|
38
|
+
type ActorPresenceOf<D extends ActorProtocol> = D extends {
|
|
39
|
+
presence: infer P extends object;
|
|
40
|
+
} ? P : never;
|
|
41
|
+
/** A `setPresence` patch: changed fields, `null` clears one. */
|
|
42
|
+
type ActorPresenceValues<P> = { [F in keyof P & string]?: P[F] | null; };
|
|
43
|
+
/**
|
|
44
|
+
* The replicated presence map: participant → field → latest value,
|
|
45
|
+
* last write wins per field. Values are peer-authored — render them
|
|
46
|
+
* like user input.
|
|
47
|
+
*/
|
|
48
|
+
type ActorPresenceMap<P> = {
|
|
49
|
+
[participant: string]: { [F in keyof P & string]?: {
|
|
50
|
+
value: P[F];
|
|
51
|
+
seen: number;
|
|
52
|
+
at: Date;
|
|
53
|
+
}; };
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The typed self-send surface: one method per declared event —
|
|
57
|
+
* `ctx.send.transfer({ ref, amount })`. Buffered, not immediate: every
|
|
58
|
+
* send requested during a handler commits atomically with that
|
|
59
|
+
* handler's completion (a serial handler's state commit, a concurrent
|
|
60
|
+
* handler's settlement), riding core's returned-events semantics — so
|
|
61
|
+
* a reserve-and-trigger can never half-happen, and re-runs converge on
|
|
62
|
+
* the same deterministic message ids.
|
|
63
|
+
*/
|
|
64
|
+
type ActorSend<E> = { readonly [K in keyof E]: {} extends E[K] ? (input?: E[K]) => void : (input: E[K]) => void; };
|
|
65
|
+
/**
|
|
66
|
+
* Timing for a scheduled event: exactly `{ delay: '5d' }` or
|
|
67
|
+
* `{ at: Date }` (core §6 semantics — relative delays anchor to the
|
|
68
|
+
* triggering message's durable `createdAt`, so re-runs resolve the
|
|
69
|
+
* same due time). `name` overrides the timer's identity — it defaults
|
|
70
|
+
* to the target event name, scoped to the triggering message, so one
|
|
71
|
+
* handler run gets one timer per target event unless named apart.
|
|
72
|
+
*/
|
|
73
|
+
type ActorScheduleOptions = ScheduleTiming & {
|
|
74
|
+
name?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The typed durable-timer surface: one method per declared event —
|
|
78
|
+
* `ctx.schedule.refund({ ref: ctx.id }, { delay: '1h' })`. Unlike
|
|
79
|
+
* `send`, scheduling is immediate, not buffered: it awaits provider
|
|
80
|
+
* acceptance at call time (a timer is provider-side, not a log row),
|
|
81
|
+
* so a handler that schedules and then refuses has still armed the
|
|
82
|
+
* timer. That is safe by the guarded-delivery idiom — the stale timer
|
|
83
|
+
* fires into an idempotent no-op — but it is the one exception to
|
|
84
|
+
* "refusals are total". Requires a configured `scheduler`.
|
|
85
|
+
*/
|
|
86
|
+
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>; };
|
|
87
|
+
/** What a serial handler receives alongside its typed input. */
|
|
88
|
+
type ActorContext<D extends ActorProtocol> = {
|
|
89
|
+
/** Mutable draft, committed atomically with the handler's completion. */
|
|
90
|
+
state: D["state"];
|
|
91
|
+
/** The message id — stable across re-runs; the idempotency key for external I/O. */
|
|
92
|
+
id: string;
|
|
93
|
+
/** Durable 1-based dispatch ordinal of this message. */
|
|
94
|
+
attempt: number;
|
|
95
|
+
/** The ordinary A2 handler signal — fires on claim expiry or supersession. */
|
|
96
|
+
signal: AbortSignal;
|
|
97
|
+
/** Typed buffered self-send — committed atomically with the state commit. */
|
|
98
|
+
send: ActorSend<D["events"]>;
|
|
99
|
+
/** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
|
|
100
|
+
schedule: ActorScheduleSend<D["events"]>;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* What a `concurrent: true` handler receives. No draft — concurrent
|
|
104
|
+
* handlers run off the lane, in parallel, so state is a snapshot read
|
|
105
|
+
* (honest about staleness) and mutations happen by sending events
|
|
106
|
+
* whose serial handlers decide against fresh state.
|
|
107
|
+
*/
|
|
108
|
+
type ActorConcurrentContext<D extends ActorProtocol> = {
|
|
109
|
+
/** The message id — stable across re-runs; the idempotency key for external I/O. */
|
|
110
|
+
id: string;
|
|
111
|
+
/** Durable 1-based dispatch ordinal of this message. */
|
|
112
|
+
attempt: number;
|
|
113
|
+
/** The ordinary A2 handler signal — fires on claim expiry or supersession. */
|
|
114
|
+
signal: AbortSignal;
|
|
115
|
+
/** Snapshot read — observational; the world moves while this runs. */
|
|
116
|
+
state(): Promise<{
|
|
117
|
+
state: D["state"];
|
|
118
|
+
index: number;
|
|
119
|
+
}>;
|
|
120
|
+
/** Typed buffered self-send — committed atomically with settlement. */
|
|
121
|
+
send: ActorSend<D["events"]>;
|
|
122
|
+
/** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
|
|
123
|
+
schedule: ActorScheduleSend<D["events"]>;
|
|
124
|
+
};
|
|
125
|
+
type ActorStatePayload<S> = {
|
|
126
|
+
state: S;
|
|
127
|
+
/** The event whose handler committed this state. */
|
|
128
|
+
event: string;
|
|
129
|
+
/** The message (invocation event) id this state answers. */
|
|
130
|
+
message: string;
|
|
131
|
+
};
|
|
132
|
+
/** The event vocabulary a client needs to follow an actor's state. */
|
|
133
|
+
type ActorClientEventDefs<S> = {
|
|
134
|
+
"a2.actor.state": StandardSchemaV1<ActorStatePayload<S>>;
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/actor.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* One event's handler: ordinary server code. Serial by default — read
|
|
140
|
+
* `ctx.state`, perform I/O, mutate `ctx.state`, one atomic commit at
|
|
141
|
+
* return. `{ concurrent: true, handle }` opts the handler out of the
|
|
142
|
+
* lane for slow I/O: state becomes a snapshot read and mutations
|
|
143
|
+
* happen by sending events. The input is typed by the protocol; the
|
|
144
|
+
* wire hands handlers parsed JSON, so a wire-exposed event guards its
|
|
145
|
+
* input in its first line (`schema.parse(input)` if you like schemas —
|
|
146
|
+
* userland either way).
|
|
147
|
+
*/
|
|
148
|
+
type ActorHandler<D extends ActorProtocol, K extends keyof D["events"]> = ((ctx: ActorContext<D>, input: D["events"][K]) => void | Promise<void>) | {
|
|
149
|
+
concurrent: true;
|
|
150
|
+
handle: (ctx: ActorConcurrentContext<D>, input: D["events"][K]) => void | Promise<void>;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Mark a handler concurrent — off the lane, in parallel with the lane
|
|
154
|
+
* and with other concurrent handlers, `ctx.state()` a snapshot read.
|
|
155
|
+
* Sugar for the structural `{ concurrent: true, handle }` form:
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* handlers: {
|
|
159
|
+
* transfer: concurrent(async (ctx, input) => {
|
|
160
|
+
* await bank.transfer(input, { idempotencyKey: input.ref })
|
|
161
|
+
* ctx.send.settle({ ref: input.ref })
|
|
162
|
+
* }),
|
|
163
|
+
* }
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
declare function concurrent<Ctx, I>(handle: (ctx: Ctx, input: I) => void | Promise<void>): {
|
|
167
|
+
concurrent: true;
|
|
168
|
+
handle: (ctx: Ctx, input: I) => void | Promise<void>;
|
|
169
|
+
};
|
|
170
|
+
type ActorOptions<D extends ActorProtocol> = {
|
|
171
|
+
/** Identity — prefixes storage keys; one contract per actor definition. */
|
|
172
|
+
name: string;
|
|
173
|
+
/** The initial state — checked against the protocol, a plain JSON tree. */
|
|
174
|
+
state: D["state"];
|
|
175
|
+
/**
|
|
176
|
+
* One handler per declared event — completeness and payload shapes
|
|
177
|
+
* are compile-checked against the protocol.
|
|
178
|
+
*/
|
|
179
|
+
handlers: { [K in keyof D["events"]]: ActorHandler<D, K>; };
|
|
180
|
+
store?: A2Store;
|
|
181
|
+
scheduler?: A2Scheduler;
|
|
182
|
+
telemetry?: A2Telemetry;
|
|
183
|
+
} & ([ActorPresenceOf<D>] extends [never] ? {
|
|
184
|
+
presence?: never;
|
|
185
|
+
} : {
|
|
186
|
+
/**
|
|
187
|
+
* The wire bit for the protocol's `presence` vocabulary — types
|
|
188
|
+
* erase, so the runtime needs one value to arm the presence
|
|
189
|
+
* lanes (frames on GET, envelopes on POST). Required exactly
|
|
190
|
+
* when the protocol declares `presence`; the vocabulary itself
|
|
191
|
+
* is typed there. Values are validated to the JSON floor and
|
|
192
|
+
* pass through `authorize` as `{ type: 'presence' }` operations;
|
|
193
|
+
* meaning stays with your renderers — treat values as user
|
|
194
|
+
* input.
|
|
195
|
+
*/
|
|
196
|
+
presence: true;
|
|
197
|
+
});
|
|
198
|
+
/**
|
|
199
|
+
* One HTTP operation `handle.fetch` is about to serve — the actor
|
|
200
|
+
* mirror of core's `A2Operation`. `authorize` sees it before any
|
|
201
|
+
* read, write, or subscription; returning `false` answers 403.
|
|
202
|
+
*/
|
|
203
|
+
type ActorOperation = {
|
|
204
|
+
readonly type: "state";
|
|
205
|
+
readonly id: string;
|
|
206
|
+
} | {
|
|
207
|
+
readonly type: "stream";
|
|
208
|
+
readonly id: string;
|
|
209
|
+
readonly startAfter: number;
|
|
210
|
+
} | {
|
|
211
|
+
readonly type: "call";
|
|
212
|
+
readonly id: string;
|
|
213
|
+
readonly event: string;
|
|
214
|
+
readonly input: unknown;
|
|
215
|
+
readonly messageId?: string;
|
|
216
|
+
} | {
|
|
217
|
+
readonly type: "presence";
|
|
218
|
+
readonly id: string;
|
|
219
|
+
readonly participant: string;
|
|
220
|
+
readonly values: Readonly<Record<string, unknown>>;
|
|
221
|
+
};
|
|
222
|
+
type ActorFetchOptions<D extends ActorProtocol> = {
|
|
223
|
+
/** Per-operation policy — authentication happened in your route. */
|
|
224
|
+
authorize?: (operation: ActorOperation) => boolean | Promise<boolean>;
|
|
225
|
+
/**
|
|
226
|
+
* Request-time projection: what this mount's audience sees. Applied
|
|
227
|
+
* to every state frame on the stream AND to call answers (the two
|
|
228
|
+
* untrusted lanes must agree, or the call lane leaks what the
|
|
229
|
+
* stream hides). The definition stays audience-agnostic — different
|
|
230
|
+
* routes project the same actor differently, and per-viewer views
|
|
231
|
+
* are just closures over the route's auth. Output is held to the
|
|
232
|
+
* JSON-tree floor like everything else. Transport-agnostic: applied
|
|
233
|
+
* at frame emission, so any wire the door speaks emits projected
|
|
234
|
+
* frames. Absent, the full state ships (the trusted default).
|
|
235
|
+
*/
|
|
236
|
+
view?: (state: D["state"]) => unknown;
|
|
237
|
+
};
|
|
238
|
+
type ActorSendOptions = {
|
|
239
|
+
/** Explicit message id — makes retries of this send idempotent. */
|
|
240
|
+
id?: string;
|
|
241
|
+
};
|
|
242
|
+
type ActorCallOptions = ActorSendOptions & {
|
|
243
|
+
/** How long to await the answer before rejecting the wait (default 30s). */
|
|
244
|
+
timeoutMs?: number;
|
|
245
|
+
};
|
|
246
|
+
type ActorCallResult<S> = {
|
|
247
|
+
/** The actor's state after this message was processed. */
|
|
248
|
+
state: S;
|
|
249
|
+
/** Log index of the state commit that answered this message. */
|
|
250
|
+
index: number;
|
|
251
|
+
};
|
|
252
|
+
type CallMethod<D extends ActorProtocol, K extends keyof D["events"]> = {} extends D["events"][K] ? (input?: D["events"][K], options?: ActorCallOptions) => Promise<ActorCallResult<D["state"]>> : (input: D["events"][K], options?: ActorCallOptions) => Promise<ActorCallResult<D["state"]>>;
|
|
253
|
+
type SendMethod<D extends ActorProtocol, K extends keyof D["events"]> = {} extends D["events"][K] ? (input?: D["events"][K], options?: ActorSendOptions) => Promise<{
|
|
254
|
+
id: string;
|
|
255
|
+
index: number;
|
|
256
|
+
}> : (input: D["events"][K], options?: ActorSendOptions) => Promise<{
|
|
257
|
+
id: string;
|
|
258
|
+
index: number;
|
|
259
|
+
}>;
|
|
260
|
+
/** One instance: typed calls and sends, reads, the managed door, the escape hatch. */
|
|
261
|
+
type ActorHandle<D extends ActorProtocol> = {
|
|
262
|
+
readonly id: string;
|
|
263
|
+
/**
|
|
264
|
+
* Send the event and await its answer: the state after the handler
|
|
265
|
+
* ran, or a rejection with `ActorRefusedError`. Serial events only —
|
|
266
|
+
* concurrent handlers produce no answer to await; `send` them.
|
|
267
|
+
*/
|
|
268
|
+
readonly call: { readonly [K in keyof D["events"]]: CallMethod<D, K>; };
|
|
269
|
+
/** Cast: validate, append the event durably, return without waiting. */
|
|
270
|
+
readonly send: { readonly [K in keyof D["events"]]: SendMethod<D, K>; };
|
|
271
|
+
/** Snapshot read — never queues behind pending messages. */
|
|
272
|
+
readonly state: () => Promise<ActorCallResult<D["state"]>>;
|
|
273
|
+
/**
|
|
274
|
+
* The managed door — the actor mirror of `server.fetch`, bound to
|
|
275
|
+
* this instance. GET streams the state plane (`a2.actor.state`
|
|
276
|
+
* commits only, `?index` resume, heartbeat, deadline rotation);
|
|
277
|
+
* POST is the call lane `createActorClient` speaks (`{ event,
|
|
278
|
+
* input, messageId? }` → the answer, 409 for a refusal). Your route
|
|
279
|
+
* authenticates and picks the instance; `authorize` sees every
|
|
280
|
+
* operation. Custom endpoints are route code around this call.
|
|
281
|
+
*/
|
|
282
|
+
readonly fetch: (request: Request, options?: ActorFetchOptions<D>) => Promise<Response>;
|
|
283
|
+
/** Escape hatch: the raw A2 session (history, stream, schedule). */
|
|
284
|
+
readonly session: Session<EventDefs>;
|
|
285
|
+
};
|
|
286
|
+
type ActorDefinition<D extends ActorProtocol> = {
|
|
287
|
+
readonly name: string;
|
|
288
|
+
/**
|
|
289
|
+
* Type-only carrier of the protocol, so clients deriving from
|
|
290
|
+
* `typeof def` can see the vocabularies (`experimental-a2/actor/react`
|
|
291
|
+
* types `presence` from it). Never a runtime value.
|
|
292
|
+
*/
|
|
293
|
+
readonly protocol?: D;
|
|
294
|
+
/** The assembled contract — declared events plus the reserved a2.actor.* pair. */
|
|
295
|
+
readonly contract: Contract<EventDefs>;
|
|
296
|
+
/** The underlying A2 server — `server.fetch` mounts the full session wire. */
|
|
297
|
+
readonly server: A2Server<EventDefs>;
|
|
298
|
+
/** The state fold — same identity as the client's follower fold. */
|
|
299
|
+
readonly reducer: Reducer<ActorClientEventDefs<D["state"]>, D["state"]>;
|
|
300
|
+
actor(id: string): ActorHandle<D>;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Define and serve an actor over its protocol: a named, durable
|
|
304
|
+
* instance-per-id with serialized state handlers, concurrent I/O
|
|
305
|
+
* handlers, and typed self-sends.
|
|
306
|
+
*/
|
|
307
|
+
declare function actor<D extends ActorProtocol>(options: ActorOptions<D>): ActorDefinition<D>;
|
|
308
|
+
//#endregion
|
|
309
|
+
export { ActorProtocol as _, ActorHandle as a, ActorScheduleSend as b, ActorOptions as c, concurrent as d, ActorConcurrentContext as f, ActorPresenceValues as g, ActorPresenceOf as h, ActorFetchOptions as i, ActorSendOptions as l, ActorPresenceMap as m, ActorCallResult as n, ActorHandler as o, ActorContext as p, ActorDefinition as r, ActorOperation as s, ActorCallOptions as t, actor as u, ActorRefusedError as v, ActorSend as x, ActorScheduleOptions as y };
|
|
310
|
+
//# sourceMappingURL=actor-D_54lz_1.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor-D_54lz_1.d.ts","names":[],"sources":["../src/actor-shared.ts","../src/actor.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;;;;;;;;;;;;;;KCzE3C,aAAa,UAAU,eAAe,gBAAgB,iBAC5D,KAAK,aAAa,IAAI,OAAO,YAAY,cAAc;EAEvD;EACA,SACE,KAAK,uBAAuB,IAC5B,OAAO,YAAY,cACT;;;;;;;;;;;;;;;;iBAiBF,WAAW,KAAK,GAC9B,SAAS,KAAK,KAAK,OAAO,aAAa;EACpC;EAAkB,SAAS,KAAK,KAAK,OAAO,aAAa;;KAIlD,aAAa,UAAU;;EAEjC;;EAEA,OAAO;;;;;EAKP,aAAa,WAAW,cAAc,aAAa,GAAG;EACtD,QAAQ;EACR,YAAY;EACZ,YAAY;MACR,gBAAgB;EAChB;;;;;;;;;;;;EAYA;;;;;;;KAQM;WACG;WAAwB;;WAExB;WACA;WACA;;WAGA;WACA;WACA;WACA;WACA;;WAGA;WACA;WACA;WACA,QAAQ,SAAS;;KAGpB,kBAAkB,UAAU;;EAEtC,aAAa,WAAW,6BAA6B;;;;;;;;;;;;EAYrD,QAAQ,OAAO;;KAGL;;EAEV;;KAGU,mBAAmB;;EAE7B;;KAGU,gBAAgB;;EAE1B,OAAO;;EAEP;;KAGG,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP,QAAQ,gBAAgB,gBAE3B,OAAO,YAAY,IACnB,UAAU,qBACP,QAAQ,gBAAgB;KAE5B,WACH,UAAU,eACV,gBAAgB,0BACH,YAAY,MAErB,QAAQ,YAAY,IACpB,UAAU,qBACP;EAAU;EAAY;MAEzB,OAAO,YAAY,IACnB,UAAU,qBACP;EAAU;EAAY;;;KAGnB,YAAY,UAAU;WACvB;;;;;;WAMA,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,kBAAkB,WAAW,cAAc,WAAW,GAAG;;WAEzD,aAAa,QAAQ,gBAAgB;;;;;;;;;;WAUrC,QACP,SAAS,SACT,UAAU,kBAAkB,OACzB,QAAQ;;WAEJ,SAAS,QAAQ;;KAGhB,gBAAgB,UAAU;WAC3B;;;;;;WAMA,WAAW;;WAEX,UAAU,SAAS;;WAEnB,QAAQ,SAAS;;WAEjB,SAAS,QAAQ,qBAAqB,aAAa;EAC5D,MAAM,aAAa,YAAY;;;;;;;iBAsDjB,MAAM,UAAU,eAC9B,SAAS,aAAa,KACrB,gBAAgB"}
|
package/dist/actor-client.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as ActorCallResult, v as ActorRefusedError } from "./actor-D_54lz_1.js";
|
|
2
2
|
//#region src/actor-client.d.ts
|
|
3
|
+
declare class ActorRequestError extends Error {
|
|
4
|
+
readonly status: number | undefined;
|
|
5
|
+
constructor(message: string, options?: {
|
|
6
|
+
status?: number;
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
3
10
|
/** The loose shape of an `actor()` definition — carried type-only. */
|
|
4
11
|
type AnyActorDefinition = {
|
|
5
12
|
actor(id: string): {
|
|
@@ -27,12 +34,14 @@ type ActorClientCall<D extends AnyActorDefinition, View = ActorStateOf<D>> = { r
|
|
|
27
34
|
type ActorClient<D extends AnyActorDefinition, View = ActorStateOf<D>> = {
|
|
28
35
|
/** Typed calls over POST — the response is the answer. */
|
|
29
36
|
readonly call: ActorClientCall<D, View>;
|
|
37
|
+
readonly state: () => Promise<ActorCallResult<View>>;
|
|
30
38
|
};
|
|
31
|
-
type CreateActorClientOptions = {
|
|
39
|
+
type CreateActorClientOptions<View = unknown> = {
|
|
32
40
|
/** The route whose handler delegates to `handle.fetch` for this instance. */
|
|
33
41
|
api: string;
|
|
34
42
|
/** Injectable transport (tests, custom auth headers). */
|
|
35
43
|
fetch?: typeof globalThis.fetch;
|
|
44
|
+
onSnapshot?: (snapshot: ActorCallResult<View>) => void;
|
|
36
45
|
};
|
|
37
46
|
/**
|
|
38
47
|
* A typed call surface for one actor instance's route. When the route
|
|
@@ -40,7 +49,7 @@ type CreateActorClientOptions = {
|
|
|
40
49
|
* answers carry the projected state:
|
|
41
50
|
* `createActorClient<typeof vault, PublicVault>({ api })`.
|
|
42
51
|
*/
|
|
43
|
-
declare function createActorClient<D extends AnyActorDefinition, View = ActorStateOf<D>>(options: CreateActorClientOptions): ActorClient<D, View>;
|
|
52
|
+
declare function createActorClient<D extends AnyActorDefinition, View = ActorStateOf<D>>(options: CreateActorClientOptions<View>): ActorClient<D, View>;
|
|
44
53
|
//#endregion
|
|
45
|
-
export { ActorClient, ActorClientCall, ActorRefusedError, ActorStateOf, AnyActorDefinition, CreateActorClientOptions, createActorClient };
|
|
54
|
+
export { ActorClient, ActorClientCall, ActorRefusedError, ActorRequestError, ActorStateOf, AnyActorDefinition, CreateActorClientOptions, createActorClient };
|
|
46
55
|
//# sourceMappingURL=actor-client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor-client.d.ts","names":[],"sources":["../src/actor-client.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"actor-client.d.ts","names":[],"sources":["../src/actor-client.ts"],"mappings":";;cAwBa,0BAA0B;WAC5B;EAET,YAAY,iBAAiB;IAAY;IAAiB;;;;KAQhD;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;WACzB,aAAa,QAAQ,gBAAgB;;KAGpC,yBAAyB;;EAEnC;;EAEA,eAAe,WAAW;EAC1B,cAAc,UAAU,gBAAgB;;;;;;;;iBA4F1B,kBACd,UAAU,oBACV,OAAO,aAAa,IACpB,SAAS,yBAAyB,QAAQ,YAAY,GAAG"}
|
package/dist/actor-client.js
CHANGED
|
@@ -18,9 +18,17 @@ import { i as ActorRefusedError } from "./actor-shared-DI7J5upy.js";
|
|
|
18
18
|
* await client.call.withdraw({ amount: 60 }) // ActorRefusedError on refusal
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
+
var ActorRequestError = class extends Error {
|
|
22
|
+
status;
|
|
23
|
+
constructor(message, options) {
|
|
24
|
+
super(message, { cause: options?.cause });
|
|
25
|
+
this.name = "ActorRequestError";
|
|
26
|
+
this.status = options?.status;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
21
29
|
const CALL_TIMEOUT_MS = 3e4;
|
|
22
30
|
const callOverWire = async (fetchImpl, api, event, input, options) => {
|
|
23
|
-
const response = await fetchImpl
|
|
31
|
+
const response = await requestOverWire(fetchImpl, api, {
|
|
24
32
|
method: "POST",
|
|
25
33
|
headers: { "content-type": "application/json" },
|
|
26
34
|
body: JSON.stringify({
|
|
@@ -30,12 +38,32 @@ const callOverWire = async (fetchImpl, api, event, input, options) => {
|
|
|
30
38
|
}),
|
|
31
39
|
signal: AbortSignal.timeout(options?.timeoutMs ?? CALL_TIMEOUT_MS)
|
|
32
40
|
});
|
|
33
|
-
const body = await response
|
|
41
|
+
const body = await responseBody(response);
|
|
34
42
|
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
|
-
|
|
37
|
-
|
|
43
|
+
if (!response.ok) throw new ActorRequestError(typeof body["error"] === "string" ? body["error"] : `actor call failed (${response.status})`, { status: response.status });
|
|
44
|
+
return snapshotBody(body, response.status);
|
|
45
|
+
};
|
|
46
|
+
const requestOverWire = async (fetchImpl, api, init) => {
|
|
47
|
+
try {
|
|
48
|
+
return await fetchImpl(api, init);
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
throw new ActorRequestError("actor request did not receive a response", { cause });
|
|
38
51
|
}
|
|
52
|
+
};
|
|
53
|
+
const responseBody = async (response) => {
|
|
54
|
+
try {
|
|
55
|
+
const body = await response.json();
|
|
56
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) throw new Error("invalid response");
|
|
57
|
+
return body;
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw new ActorRequestError("actor request returned an invalid response", {
|
|
60
|
+
status: response.status,
|
|
61
|
+
cause
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const snapshotBody = (body, status) => {
|
|
66
|
+
if (!("state" in body) || !Number.isSafeInteger(body["index"]) || body["index"] < 0) throw new ActorRequestError("actor request returned an invalid snapshot", { status });
|
|
39
67
|
return body;
|
|
40
68
|
};
|
|
41
69
|
/**
|
|
@@ -46,9 +74,37 @@ const callOverWire = async (fetchImpl, api, event, input, options) => {
|
|
|
46
74
|
*/
|
|
47
75
|
function createActorClient(options) {
|
|
48
76
|
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
49
|
-
|
|
77
|
+
const adopt = (snapshot) => {
|
|
78
|
+
const result = snapshot;
|
|
79
|
+
options.onSnapshot?.(result);
|
|
80
|
+
return result;
|
|
81
|
+
};
|
|
82
|
+
const state = async () => {
|
|
83
|
+
const api = options.api.split("#")[0];
|
|
84
|
+
const separator = api.includes("?") ? "&" : "?";
|
|
85
|
+
const response = await requestOverWire(fetchImpl, `${api}${separator}snapshot=1`, {
|
|
86
|
+
cache: "no-store",
|
|
87
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS)
|
|
88
|
+
});
|
|
89
|
+
const body = await responseBody(response);
|
|
90
|
+
if (!response.ok) throw new ActorRequestError(typeof body["error"] === "string" ? body["error"] : `actor state read failed (${response.status})`, { status: response.status });
|
|
91
|
+
return adopt(snapshotBody(body, response.status));
|
|
92
|
+
};
|
|
93
|
+
const methods = /* @__PURE__ */ new Map();
|
|
94
|
+
return {
|
|
95
|
+
call: new Proxy({}, { get: (_target, event) => {
|
|
96
|
+
if (typeof event !== "string") return void 0;
|
|
97
|
+
let method = methods.get(event);
|
|
98
|
+
if (!method) {
|
|
99
|
+
method = (input, callOptions) => callOverWire(fetchImpl, options.api, event, input, callOptions).then(adopt);
|
|
100
|
+
methods.set(event, method);
|
|
101
|
+
}
|
|
102
|
+
return method;
|
|
103
|
+
} }),
|
|
104
|
+
state
|
|
105
|
+
};
|
|
50
106
|
}
|
|
51
107
|
//#endregion
|
|
52
|
-
export { ActorRefusedError, createActorClient };
|
|
108
|
+
export { ActorRefusedError, ActorRequestError, createActorClient };
|
|
53
109
|
|
|
54
110
|
//# sourceMappingURL=actor-client.js.map
|
package/dist/actor-client.js.map
CHANGED
|
@@ -1 +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
|
|
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\nexport class ActorRequestError extends Error {\n readonly status: number | undefined\n\n constructor(message: string, options?: { status?: number; cause?: unknown }) {\n super(message, { cause: options?.cause })\n this.name = 'ActorRequestError'\n this.status = options?.status\n }\n}\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 readonly state: () => Promise<ActorCallResult<View>>\n}\n\nexport type CreateActorClientOptions<View = unknown> = {\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 onSnapshot?: (snapshot: ActorCallResult<View>) => void\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 requestOverWire(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 responseBody(response)\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 ActorRequestError(message, { status: response.status })\n }\n return snapshotBody(body, response.status)\n}\n\nconst requestOverWire = async (\n fetchImpl: typeof globalThis.fetch,\n api: string,\n init: RequestInit,\n): Promise<Response> => {\n try {\n return await fetchImpl(api, init)\n } catch (cause) {\n throw new ActorRequestError('actor request did not receive a response', {\n cause,\n })\n }\n}\n\nconst responseBody = async (\n response: Response,\n): Promise<Record<string, unknown>> => {\n try {\n const body: unknown = await response.json()\n if (!body || typeof body !== 'object' || Array.isArray(body))\n throw new Error('invalid response')\n return body as Record<string, unknown>\n } catch (cause) {\n throw new ActorRequestError('actor request returned an invalid response', {\n status: response.status,\n cause,\n })\n }\n}\n\nconst snapshotBody = (\n body: Record<string, unknown>,\n status: number,\n): ActorCallResult<unknown> => {\n if (\n !('state' in body) ||\n !Number.isSafeInteger(body['index']) ||\n (body['index'] as number) < 0\n ) {\n throw new ActorRequestError('actor request returned an invalid snapshot', {\n status,\n })\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<View>): ActorClient<D, View> {\n const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis)\n const adopt = (snapshot: ActorCallResult<unknown>): ActorCallResult<View> => {\n const result = snapshot as ActorCallResult<View>\n options.onSnapshot?.(result)\n return result\n }\n const state = async (): Promise<ActorCallResult<View>> => {\n const api = options.api.split('#')[0]!\n const separator = api.includes('?') ? '&' : '?'\n const response = await requestOverWire(\n fetchImpl,\n `${api}${separator}snapshot=1`,\n {\n cache: 'no-store',\n signal: AbortSignal.timeout(CALL_TIMEOUT_MS),\n },\n )\n const body = await responseBody(response)\n if (!response.ok) {\n throw new ActorRequestError(\n typeof body['error'] === 'string'\n ? body['error']\n : `actor state read failed (${response.status})`,\n { status: response.status },\n )\n }\n return adopt(snapshotBody(body, response.status))\n }\n const methods = new Map<\n string,\n (\n input?: unknown,\n callOptions?: ActorCallOptions,\n ) => Promise<ActorCallResult<View>>\n >()\n const call = new Proxy(\n {},\n {\n get: (_target, event) => {\n if (typeof event !== 'string') return undefined\n let method = methods.get(event)\n if (!method) {\n method = (input, callOptions) =>\n callOverWire(\n fetchImpl,\n options.api,\n event,\n input,\n callOptions,\n ).then(adopt)\n methods.set(event, method)\n }\n return method\n },\n },\n ) as ActorClientCall<D, View>\n return { call, state }\n}\n"],"mappings":"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"}
|
package/dist/actor-react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as ContractEvent, s as EventDefs } from "./reducer-DJKWm3cp.js";
|
|
2
|
-
import {
|
|
2
|
+
import { g as ActorPresenceValues, m as ActorPresenceMap } from "./actor-D_54lz_1.js";
|
|
3
3
|
import { ActorClientCall, ActorStateOf, AnyActorDefinition } from "./actor-client.js";
|
|
4
|
-
import { Connection } from "./client
|
|
4
|
+
import { a as Connection } from "./client-P_NNNRM-.js";
|
|
5
5
|
//#region src/actor-react.d.ts
|
|
6
6
|
/** The server-to-client handoff: `await def.actor(id).state()`. */
|
|
7
7
|
type ActorSnapshot<S> = {
|
|
@@ -25,13 +25,14 @@ type PresenceOf<D extends AnyActorDefinition> = D extends {
|
|
|
25
25
|
presence: infer Values extends object;
|
|
26
26
|
} ? Values : never : never;
|
|
27
27
|
type UseActorResult<D extends AnyActorDefinition, View> = {
|
|
28
|
-
/** The
|
|
28
|
+
/** The live view, advanced by call answers, refreshes, and stream commits. */
|
|
29
29
|
state: View;
|
|
30
|
-
/** The
|
|
30
|
+
/** The latest adopted state index. */
|
|
31
31
|
index: number;
|
|
32
32
|
connection: Connection;
|
|
33
33
|
/** Typed calls over POST — the response is the answer. */
|
|
34
34
|
call: ActorClientCall<D, View>;
|
|
35
|
+
refresh: () => Promise<ActorSnapshot<View>>;
|
|
35
36
|
/** The state-commit feed this browser has observed. */
|
|
36
37
|
events: ContractEvent<EventDefs>[];
|
|
37
38
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actor-react.d.ts","names":[],"sources":["../src/actor-react.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"actor-react.d.ts","names":[],"sources":["../src/actor-react.ts"],"mappings":";;;;;;KA8CY,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;EACzB,eAAe,QAAQ,cAAc;;EAErC,QAAQ,cAAc;;;;;;;EAOtB,UAAU,iBAAiB,WAAW;;;;;EAKtC,cAAc,QAAQ,oBAAoB,WAAW;;;iBA2BvC,SAAS,UAAU,oBAAoB,OAAO,aAAa,IACzE,SAAS,gBAAgB,QACxB,eAAe,GAAG"}
|
package/dist/actor-react.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createClient } from "./client.js";
|
|
2
|
+
import { t as createClient } from "./client-Bf6uSEAk.js";
|
|
3
3
|
import { useSession } from "./react.js";
|
|
4
4
|
import { l as openPresenceDefs } from "./actor-shared-DI7J5upy.js";
|
|
5
5
|
import { createActorClient } from "./actor-client.js";
|
|
6
|
+
import { useMemo } from "react";
|
|
6
7
|
//#region src/actor-react.ts
|
|
7
8
|
/**
|
|
8
9
|
* experimental-a2/actor/react — use an actor from React: live state
|
|
@@ -62,12 +63,25 @@ function useActor(options) {
|
|
|
62
63
|
hydrate: options.initial,
|
|
63
64
|
...options.participant === void 0 ? {} : { participant: options.participant }
|
|
64
65
|
});
|
|
65
|
-
const { call } = createActorClient({
|
|
66
|
+
const { call, state: refresh } = useMemo(() => createActorClient({
|
|
67
|
+
api,
|
|
68
|
+
onSnapshot: ({ state, index }) => {
|
|
69
|
+
client.session(options.id, {
|
|
70
|
+
initialState: state,
|
|
71
|
+
initialIndex: index
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}), [
|
|
75
|
+
api,
|
|
76
|
+
client,
|
|
77
|
+
options.id
|
|
78
|
+
]);
|
|
66
79
|
return {
|
|
67
80
|
state: session.state,
|
|
68
81
|
index: session.index,
|
|
69
82
|
connection: session.connection,
|
|
70
83
|
call,
|
|
84
|
+
refresh,
|
|
71
85
|
events: session.events,
|
|
72
86
|
presence: session.presence,
|
|
73
87
|
setPresence: session.setPresence
|