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.
Files changed (182) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +36 -0
  3. package/README.md +29 -0
  4. package/dist/actor-client.d.ts +46 -0
  5. package/dist/actor-client.d.ts.map +1 -0
  6. package/dist/actor-client.js +54 -0
  7. package/dist/actor-client.js.map +1 -0
  8. package/dist/actor-react.d.ts +54 -0
  9. package/dist/actor-react.d.ts.map +1 -0
  10. package/dist/actor-react.js +79 -0
  11. package/dist/actor-react.js.map +1 -0
  12. package/dist/actor-shared-BACubf4x.d.ts +136 -0
  13. package/dist/actor-shared-BACubf4x.d.ts.map +1 -0
  14. package/dist/actor-shared-DI7J5upy.js +127 -0
  15. package/dist/actor-shared-DI7J5upy.js.map +1 -0
  16. package/dist/actor.browser.d.ts +1 -0
  17. package/dist/actor.browser.js +13 -0
  18. package/dist/actor.browser.js.map +1 -0
  19. package/dist/actor.d.ts +176 -0
  20. package/dist/actor.d.ts.map +1 -0
  21. package/dist/actor.js +437 -0
  22. package/dist/actor.js.map +1 -0
  23. package/dist/ai-server.d.ts +2 -2
  24. package/dist/ai-server.js +2 -2
  25. package/dist/ai.d.ts +2 -2
  26. package/dist/client.d.ts +1 -1
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +4 -4
  29. package/dist/client.js.map +1 -1
  30. package/dist/{errors-BQuJpe82.js → errors-DCk6ch5n.js} +16 -2
  31. package/dist/{errors-BQuJpe82.js.map → errors-DCk6ch5n.js.map} +1 -1
  32. package/dist/{idempotent-replay-DuqEkYA7.js → idempotent-replay-DVOlyYbx.js} +2 -2
  33. package/dist/{idempotent-replay-DuqEkYA7.js.map → idempotent-replay-DVOlyYbx.js.map} +1 -1
  34. package/dist/index.d.ts +16 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -2
  37. package/dist/react.d.ts +1 -1
  38. package/dist/{contract-jIfaR085.d.ts → reducer-DJKWm3cp.d.ts} +39 -39
  39. package/dist/reducer-DJKWm3cp.d.ts.map +1 -0
  40. package/dist/scheduler-qstash.d.ts +2 -2
  41. package/dist/scheduler-qstash.js +2 -2
  42. package/dist/scheduler-vercel.d.ts +2 -2
  43. package/dist/scheduler-vercel.js +1 -1
  44. package/dist/{server-B2XNevQA.js → server-CBET-jSz.js} +6 -6
  45. package/dist/server-CBET-jSz.js.map +1 -0
  46. package/dist/{server-DjPhHnbI.d.ts → server-CKY3_lbw.d.ts} +3 -3
  47. package/dist/{server-DjPhHnbI.d.ts.map → server-CKY3_lbw.d.ts.map} +1 -1
  48. package/dist/server.d.ts +3 -3
  49. package/dist/server.js +1 -1
  50. package/dist/{store-RJO35BMj.d.ts → store-DGHeBtIQ.d.ts} +2 -2
  51. package/dist/{store-RJO35BMj.d.ts.map → store-DGHeBtIQ.d.ts.map} +1 -1
  52. package/dist/store-memory.d.ts +1 -1
  53. package/dist/store-memory.js +2 -2
  54. package/dist/store-postgres.d.ts +1 -1
  55. package/dist/store-postgres.js +2 -2
  56. package/dist/{store-redis-core-DT01r4GZ.js → store-redis-core-z-ykbyMg.js} +3 -3
  57. package/dist/{store-redis-core-DT01r4GZ.js.map → store-redis-core-z-ykbyMg.js.map} +1 -1
  58. package/dist/store-redis-http.d.ts +1 -1
  59. package/dist/store-redis-http.js +2 -2
  60. package/dist/store-redis.d.ts +1 -1
  61. package/dist/store-redis.js +2 -2
  62. package/dist/store-sqlite.d.ts +1 -1
  63. package/dist/store-sqlite.js +2 -2
  64. package/dist/{wire-B6te_wns.js → wire--yji6mO3.js} +2 -2
  65. package/dist/{wire-B6te_wns.js.map → wire--yji6mO3.js.map} +1 -1
  66. package/docs/actors/01-introduction.mdx +189 -0
  67. package/docs/actors/02-concurrency.mdx +154 -0
  68. package/docs/actors/03-timers.mdx +120 -0
  69. package/docs/actors/04-routes.mdx +352 -0
  70. package/docs/actors/meta.ts +1 -0
  71. package/docs/concepts/meta.ts +1 -0
  72. package/docs/guides/07-examples.mdx +56 -0
  73. package/docs/guides/meta.ts +1 -0
  74. package/docs/index.mdx +16 -0
  75. package/docs/reference/02-errors.mdx +33 -0
  76. package/docs/reference/meta.ts +1 -0
  77. package/examples/README.md +15 -0
  78. package/examples/playground/AGENTS.md +11 -0
  79. package/examples/playground/DEPLOY.md +106 -0
  80. package/examples/playground/README.md +19 -0
  81. package/examples/playground/activity-feed.test.ts +10 -0
  82. package/examples/playground/app/agent/[agentId]/agent-client.tsx +376 -0
  83. package/examples/playground/app/agent/[agentId]/page.tsx +29 -0
  84. package/examples/playground/app/agent/events/route.ts +4 -0
  85. package/examples/playground/app/agent/model.ts +3 -0
  86. package/examples/playground/app/agent/new-agent-session.tsx +98 -0
  87. package/examples/playground/app/agent/page.tsx +25 -0
  88. package/examples/playground/app/agent/scheduler/route.ts +5 -0
  89. package/examples/playground/app/agent/server.ts +153 -0
  90. package/examples/playground/app/agent/session.ts +13 -0
  91. package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +682 -0
  92. package/examples/playground/app/canvas/[canvasId]/canvas-replay.test.ts +68 -0
  93. package/examples/playground/app/canvas/[canvasId]/canvas-replay.ts +19 -0
  94. package/examples/playground/app/canvas/[canvasId]/page.tsx +22 -0
  95. package/examples/playground/app/canvas/[canvasId]/session.ts +19 -0
  96. package/examples/playground/app/canvas/events/route.ts +13 -0
  97. package/examples/playground/app/canvas/model.ts +94 -0
  98. package/examples/playground/app/canvas/open-canvas.tsx +40 -0
  99. package/examples/playground/app/canvas/page.tsx +20 -0
  100. package/examples/playground/app/canvas/server.ts +9 -0
  101. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +118 -0
  102. package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +316 -0
  103. package/examples/playground/app/chat/[chatId]/chat-client.tsx +922 -0
  104. package/examples/playground/app/chat/[chatId]/chat-view.test.ts +152 -0
  105. package/examples/playground/app/chat/[chatId]/chat-view.ts +101 -0
  106. package/examples/playground/app/chat/[chatId]/composer.test.ts +44 -0
  107. package/examples/playground/app/chat/[chatId]/composer.ts +30 -0
  108. package/examples/playground/app/chat/[chatId]/page.tsx +30 -0
  109. package/examples/playground/app/chat/[chatId]/session.ts +7 -0
  110. package/examples/playground/app/chat/events/route.ts +7 -0
  111. package/examples/playground/app/chat/model.test.ts +155 -0
  112. package/examples/playground/app/chat/model.ts +310 -0
  113. package/examples/playground/app/chat/new-conversation.tsx +16 -0
  114. package/examples/playground/app/chat/page.tsx +25 -0
  115. package/examples/playground/app/chat/scheduler/route.ts +5 -0
  116. package/examples/playground/app/chat/server.ts +184 -0
  117. package/examples/playground/app/components/activity-feed.tsx +54 -0
  118. package/examples/playground/app/components/connection-pill.tsx +29 -0
  119. package/examples/playground/app/counter/counter-client.tsx +72 -0
  120. package/examples/playground/app/counter/events/route.ts +4 -0
  121. package/examples/playground/app/counter/model.test.ts +36 -0
  122. package/examples/playground/app/counter/model.ts +31 -0
  123. package/examples/playground/app/counter/page.tsx +24 -0
  124. package/examples/playground/app/counter/server.ts +9 -0
  125. package/examples/playground/app/counter/session.ts +13 -0
  126. package/examples/playground/app/documents/[documentId]/code-editor.tsx +80 -0
  127. package/examples/playground/app/documents/[documentId]/document-client.tsx +525 -0
  128. package/examples/playground/app/documents/[documentId]/page.tsx +23 -0
  129. package/examples/playground/app/documents/[documentId]/session.ts +7 -0
  130. package/examples/playground/app/documents/events/route.ts +4 -0
  131. package/examples/playground/app/documents/model.ts +55 -0
  132. package/examples/playground/app/documents/open-document.tsx +40 -0
  133. package/examples/playground/app/documents/page.tsx +22 -0
  134. package/examples/playground/app/documents/server.ts +9 -0
  135. package/examples/playground/app/globals.css +2078 -0
  136. package/examples/playground/app/layout.tsx +44 -0
  137. package/examples/playground/app/orders/[orderId]/order-client.tsx +140 -0
  138. package/examples/playground/app/orders/[orderId]/page.tsx +29 -0
  139. package/examples/playground/app/orders/[orderId]/session.ts +11 -0
  140. package/examples/playground/app/orders/create/route.ts +30 -0
  141. package/examples/playground/app/orders/events/route.ts +4 -0
  142. package/examples/playground/app/orders/model.ts +79 -0
  143. package/examples/playground/app/orders/new-order-form.tsx +98 -0
  144. package/examples/playground/app/orders/page.tsx +22 -0
  145. package/examples/playground/app/orders/scheduler/route.ts +5 -0
  146. package/examples/playground/app/orders/server.ts +50 -0
  147. package/examples/playground/app/page.tsx +111 -0
  148. package/examples/playground/app/recovery/[recoveryId]/page.tsx +31 -0
  149. package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +144 -0
  150. package/examples/playground/app/recovery/[recoveryId]/session.ts +7 -0
  151. package/examples/playground/app/recovery/events/route.ts +3 -0
  152. package/examples/playground/app/recovery/model.ts +55 -0
  153. package/examples/playground/app/recovery/new-recovery-session.tsx +20 -0
  154. package/examples/playground/app/recovery/page.tsx +22 -0
  155. package/examples/playground/app/recovery/scheduler/route.ts +7 -0
  156. package/examples/playground/app/recovery/server.ts +53 -0
  157. package/examples/playground/app/recovery/start/route.ts +41 -0
  158. package/examples/playground/app/vault/[vaultId]/route.ts +19 -0
  159. package/examples/playground/app/vault/page.tsx +12 -0
  160. package/examples/playground/app/vault/server.ts +9 -0
  161. package/examples/playground/app/vault/vault-client.tsx +124 -0
  162. package/examples/playground/app/vault/vault.test.ts +147 -0
  163. package/examples/playground/app/vault/vault.ts +119 -0
  164. package/examples/playground/css.d.ts +4 -0
  165. package/examples/playground/lib/store.ts +15 -0
  166. package/examples/playground/next-env.d.ts +5 -0
  167. package/examples/playground/next.config.ts +10 -0
  168. package/examples/playground/package.json +46 -0
  169. package/examples/playground/tsconfig.json +37 -0
  170. package/examples/playground/vercel.json +40 -0
  171. package/package.json +11 -2
  172. package/src/actor-client.ts +132 -0
  173. package/src/actor-react.ts +143 -0
  174. package/src/actor-shared.ts +356 -0
  175. package/src/actor.browser.ts +12 -0
  176. package/src/actor.ts +914 -0
  177. package/src/client.ts +9 -1
  178. package/src/errors.ts +15 -0
  179. package/src/index.ts +1 -1
  180. package/src/server.ts +13 -3
  181. package/dist/contract-jIfaR085.d.ts.map +0 -1
  182. package/dist/server-B2XNevQA.js.map +0 -1
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Internal shared vocabulary of the actor module — the reserved event
3
+ * envelopes, the protocol and context types, the state fold. Not an
4
+ * entry point: the public surfaces are experimental-a2/actor (server),
5
+ * /actor/client, and /actor/react.
6
+ *
7
+ * An actor is a durable mailbox with memory: one session per instance,
8
+ * events processed one at a time (a lane) unless a handler opts out
9
+ * with `concurrent: true`, state materialized as `a2.actor.state`
10
+ * events committed atomically with each handler's completion.
11
+ */
12
+
13
+ import { contract as createContract } from './contract.ts'
14
+ import type { Reducer } from './reducer.ts'
15
+ import type { ScheduleTiming } from './server.ts'
16
+ import type { StandardSchemaV1 } from './standard-schema.ts'
17
+
18
+ /** Event type of a committed state change — one per completed serial handler. */
19
+ export const ACTOR_STATE_EVENT = 'a2.actor.state'
20
+ /** Event type of a refusal — a handler that answered by throwing NonRetriableError. */
21
+ export const ACTOR_FAILED_EVENT = 'a2.actor.failed'
22
+ /** The per-instance lane every serial handler shares — the state's write lock. */
23
+ export const ACTOR_LANE = 'a2.actor'
24
+ /** Reducer identity — bump when the fold's meaning changes. */
25
+ export const ACTOR_REDUCER_NAME = 'a2.actor.v1'
26
+
27
+ /**
28
+ * A handler answered by throwing `NonRetriableError`: the event
29
+ * settled, the state did not change. Isomorphic — the server raises it
30
+ * from calls and `fetch()` serializes it as a 409; the client's `call`
31
+ * proxy revives it from that response.
32
+ */
33
+ export class ActorRefusedError extends Error {
34
+ /** The refused event's name. */
35
+ readonly event: string
36
+ /** The refused event's message id. */
37
+ readonly messageId: string
38
+ constructor(event: string, messageId: string, message: string) {
39
+ super(message)
40
+ this.name = 'ActorRefusedError'
41
+ this.event = event
42
+ this.messageId = messageId
43
+ }
44
+ }
45
+
46
+ /**
47
+ * The declaration an actor is defined over: its state shape and its
48
+ * event vocabulary, as types. Core A2 contracts are schemas because
49
+ * the wire is untrusted; actor protocols are types because your
50
+ * server is trusted — same contract-first design language, dialed to
51
+ * the trust level.
52
+ */
53
+ export type ActorProtocol = {
54
+ state: object
55
+ events: object
56
+ /**
57
+ * Optional third vocabulary: presence field → value type. Ephemeral
58
+ * audience state (who is here, cursors), replicated to subscribers
59
+ * and never stored in the log. Declaring it requires `presence:
60
+ * true` in the actor options — the wire bit types cannot carry.
61
+ */
62
+ presence?: object
63
+ }
64
+
65
+ /** The protocol's presence vocabulary, `never` when undeclared. */
66
+ export type ActorPresenceOf<D extends ActorProtocol> = D extends {
67
+ presence: infer P extends object
68
+ }
69
+ ? P
70
+ : never
71
+
72
+ /** A `setPresence` patch: changed fields, `null` clears one. */
73
+ export type ActorPresenceValues<P> = {
74
+ [F in keyof P & string]?: P[F] | null
75
+ }
76
+
77
+ /**
78
+ * The replicated presence map: participant → field → latest value,
79
+ * last write wins per field. Values are peer-authored — render them
80
+ * like user input.
81
+ */
82
+ export type ActorPresenceMap<P> = {
83
+ [participant: string]: {
84
+ [F in keyof P & string]?: { value: P[F]; seen: number; at: Date }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * The typed self-send surface: one method per declared event —
90
+ * `ctx.send.transfer({ ref, amount })`. Buffered, not immediate: every
91
+ * send requested during a handler commits atomically with that
92
+ * handler's completion (a serial handler's state commit, a concurrent
93
+ * handler's settlement), riding core's returned-events semantics — so
94
+ * a reserve-and-trigger can never half-happen, and re-runs converge on
95
+ * the same deterministic message ids.
96
+ */
97
+ export type ActorSend<E> = {
98
+ readonly [K in keyof E]: {} extends E[K]
99
+ ? (input?: E[K]) => void
100
+ : (input: E[K]) => void
101
+ }
102
+
103
+ /**
104
+ * Timing for a scheduled event: exactly `{ delay: '5d' }` or
105
+ * `{ at: Date }` (core §6 semantics — relative delays anchor to the
106
+ * triggering message's durable `createdAt`, so re-runs resolve the
107
+ * same due time). `name` overrides the timer's identity — it defaults
108
+ * to the target event name, scoped to the triggering message, so one
109
+ * handler run gets one timer per target event unless named apart.
110
+ */
111
+ export type ActorScheduleOptions = ScheduleTiming & { name?: string }
112
+
113
+ /**
114
+ * The typed durable-timer surface: one method per declared event —
115
+ * `ctx.schedule.refund({ ref: ctx.id }, { delay: '1h' })`. Unlike
116
+ * `send`, scheduling is immediate, not buffered: it awaits provider
117
+ * acceptance at call time (a timer is provider-side, not a log row),
118
+ * so a handler that schedules and then refuses has still armed the
119
+ * timer. That is safe by the guarded-delivery idiom — the stale timer
120
+ * fires into an idempotent no-op — but it is the one exception to
121
+ * "refusals are total". Requires a configured `scheduler`.
122
+ */
123
+ export type ActorScheduleSend<E> = {
124
+ readonly [K in keyof E]: {} extends E[K]
125
+ ? (input: E[K] | undefined, options: ActorScheduleOptions) => Promise<void>
126
+ : (input: E[K], options: ActorScheduleOptions) => Promise<void>
127
+ }
128
+
129
+ /** What a serial handler receives alongside its typed input. */
130
+ export type ActorContext<D extends ActorProtocol> = {
131
+ /** Mutable draft, committed atomically with the handler's completion. */
132
+ state: D['state']
133
+ /** The message id — stable across re-runs; the idempotency key for external I/O. */
134
+ id: string
135
+ /** Durable 1-based dispatch ordinal of this message. */
136
+ attempt: number
137
+ /** The ordinary A2 handler signal — fires on claim expiry or supersession. */
138
+ signal: AbortSignal
139
+ /** Typed buffered self-send — committed atomically with the state commit. */
140
+ send: ActorSend<D['events']>
141
+ /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
142
+ schedule: ActorScheduleSend<D['events']>
143
+ }
144
+
145
+ /**
146
+ * What a `concurrent: true` handler receives. No draft — concurrent
147
+ * handlers run off the lane, in parallel, so state is a snapshot read
148
+ * (honest about staleness) and mutations happen by sending events
149
+ * whose serial handlers decide against fresh state.
150
+ */
151
+ export type ActorConcurrentContext<D extends ActorProtocol> = {
152
+ /** The message id — stable across re-runs; the idempotency key for external I/O. */
153
+ id: string
154
+ /** Durable 1-based dispatch ordinal of this message. */
155
+ attempt: number
156
+ /** The ordinary A2 handler signal — fires on claim expiry or supersession. */
157
+ signal: AbortSignal
158
+ /** Snapshot read — observational; the world moves while this runs. */
159
+ state(): Promise<{ state: D['state']; index: number }>
160
+ /** Typed buffered self-send — committed atomically with settlement. */
161
+ send: ActorSend<D['events']>
162
+ /** Typed durable timers — immediate provider handoff, see ActorScheduleSend. */
163
+ schedule: ActorScheduleSend<D['events']>
164
+ }
165
+
166
+ export type ActorStatePayload<S> = {
167
+ state: S
168
+ /** The event whose handler committed this state. */
169
+ event: string
170
+ /** The message (invocation event) id this state answers. */
171
+ message: string
172
+ }
173
+
174
+ export type ActorFailedPayload = {
175
+ /** The message (invocation event) id this refusal answers. */
176
+ message: string
177
+ event: string
178
+ error: string
179
+ }
180
+
181
+ /** The event vocabulary a client needs to follow an actor's state. */
182
+ export type ActorClientEventDefs<S> = {
183
+ 'a2.actor.state': StandardSchemaV1<ActorStatePayload<S>>
184
+ }
185
+
186
+ type SchemaResult<T> = StandardSchemaV1.Result<T>
187
+
188
+ const issue = <T>(message: string): SchemaResult<T> => ({
189
+ issues: [{ message }],
190
+ })
191
+
192
+ const schema = <T>(
193
+ label: string,
194
+ parse: (value: unknown) => SchemaResult<T>,
195
+ ): StandardSchemaV1<T> => ({
196
+ '~standard': {
197
+ version: 1,
198
+ vendor: 'a2',
199
+ validate(value) {
200
+ try {
201
+ return parse(value)
202
+ } catch (error) {
203
+ return issue(
204
+ `${label}: ${error instanceof Error ? error.message : String(error)}`,
205
+ )
206
+ }
207
+ },
208
+ },
209
+ })
210
+
211
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
212
+ typeof value === 'object' && value !== null && !Array.isArray(value)
213
+
214
+ /** True for plain JSON trees — what the log can store without loss. */
215
+ export function isJsonTree(
216
+ value: unknown,
217
+ seen: Set<object> = new Set(),
218
+ ): boolean {
219
+ if (
220
+ value === null ||
221
+ typeof value === 'string' ||
222
+ typeof value === 'boolean'
223
+ ) {
224
+ return true
225
+ }
226
+ if (typeof value === 'number') return Number.isFinite(value)
227
+ if (typeof value !== 'object') return false
228
+ if (seen.has(value)) return false
229
+ seen.add(value)
230
+ const valid = Array.isArray(value)
231
+ ? value.every((item) => isJsonTree(item, seen))
232
+ : Object.getPrototypeOf(value) === Object.prototype &&
233
+ Object.values(value).every(
234
+ (item) => item === undefined || isJsonTree(item, seen),
235
+ )
236
+ seen.delete(value)
237
+ return valid
238
+ }
239
+
240
+ /** Per-field ceiling for presence values — presence is a cursor, not a document. */
241
+ export const PRESENCE_VALUE_MAX_BYTES = 8_192
242
+
243
+ /**
244
+ * The open presence vocabulary: the '*' catch-all validates any field
245
+ * name against the JSON floor. The protocol types the vocabulary at
246
+ * compile time; the route's `authorize` sees every set before
247
+ * acceptance; renderers treat values as user input. Meaning has three
248
+ * guards — this schema only owns the bytes.
249
+ */
250
+ export const openPresenceDefs: Readonly<
251
+ Record<string, StandardSchemaV1<unknown>>
252
+ > = Object.freeze({
253
+ '*': {
254
+ '~standard': {
255
+ version: 1,
256
+ vendor: 'a2',
257
+ validate: (value: unknown) => {
258
+ if (!isJsonTree(value)) {
259
+ return {
260
+ issues: [{ message: 'presence values must be plain JSON trees' }],
261
+ }
262
+ }
263
+ if (JSON.stringify(value).length > PRESENCE_VALUE_MAX_BYTES) {
264
+ return {
265
+ issues: [
266
+ {
267
+ message: `presence values are capped at ${PRESENCE_VALUE_MAX_BYTES} bytes`,
268
+ },
269
+ ],
270
+ }
271
+ }
272
+ return { value }
273
+ },
274
+ },
275
+ } satisfies StandardSchemaV1<unknown>,
276
+ })
277
+
278
+ /**
279
+ * The `a2.actor.state` envelope: provenance plus the state, held to a
280
+ * plain JSON tree (a `Date` or `Map` in state fails loudly at commit,
281
+ * never silently coerces in the log).
282
+ */
283
+ export function actorStateSchema<S>(): StandardSchemaV1<ActorStatePayload<S>> {
284
+ return schema(ACTOR_STATE_EVENT, (value) => {
285
+ if (
286
+ !isRecord(value) ||
287
+ typeof value['event'] !== 'string' ||
288
+ typeof value['message'] !== 'string'
289
+ ) {
290
+ return issue(`invalid ${ACTOR_STATE_EVENT} payload`)
291
+ }
292
+ const state = value['state']
293
+ if (!isJsonTree(state)) {
294
+ return issue('the actor state must be a plain JSON tree')
295
+ }
296
+ return {
297
+ value: {
298
+ state: state as S,
299
+ event: value['event'],
300
+ message: value['message'],
301
+ },
302
+ }
303
+ })
304
+ }
305
+
306
+ export const actorFailedSchema: StandardSchemaV1<ActorFailedPayload> = schema(
307
+ ACTOR_FAILED_EVENT,
308
+ (value) => {
309
+ if (
310
+ !isRecord(value) ||
311
+ typeof value['message'] !== 'string' ||
312
+ typeof value['event'] !== 'string' ||
313
+ typeof value['error'] !== 'string'
314
+ ) {
315
+ return issue(`invalid ${ACTOR_FAILED_EVENT} payload`)
316
+ }
317
+ return {
318
+ value: {
319
+ message: value['message'],
320
+ event: value['event'],
321
+ error: value['error'],
322
+ },
323
+ }
324
+ },
325
+ )
326
+
327
+ export type ActorReducerOptions<S> = {
328
+ /** The actor's name — must match the server-side definition. */
329
+ name: string
330
+ /** The initial state — a plain JSON tree, the fold's seed. */
331
+ state: S
332
+ }
333
+
334
+ /**
335
+ * The library fold over an actor's state commits — last write wins.
336
+ * Pure library code: a browser bundle folds an actor's live state with
337
+ * no user code, so the actor definition itself stays server-only
338
+ * (import the server module's type for typing, this reducer for data).
339
+ */
340
+ export function actorReducer<S>(
341
+ options: ActorReducerOptions<S>,
342
+ ): Reducer<ActorClientEventDefs<S>, S> {
343
+ if (!isJsonTree(options.state)) {
344
+ throw new TypeError(
345
+ `actor '${options.name}': the initial state must be a plain JSON tree`,
346
+ )
347
+ }
348
+ const events: ActorClientEventDefs<S> = {
349
+ [ACTOR_STATE_EVENT]: actorStateSchema(),
350
+ }
351
+ return createContract({ name: options.name, events })
352
+ .reducer({ name: ACTOR_REDUCER_NAME, initialState: options.state })
353
+ .fold((state, event) =>
354
+ event.type === ACTOR_STATE_EVENT ? event.payload.state : state,
355
+ )
356
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The browser build of experimental-a2/actor. There isn't one — on
3
+ * purpose. Actions are server code; the browser follows an actor's
4
+ * state with `actorReducer()` from `experimental-a2/actor` (isomorphic)
5
+ * over the ordinary session client. This module existing in a client
6
+ * bundle means a `'use client'` file (or something it imports)
7
+ * value-imported your actor module.
8
+ */
9
+ throw new Error(
10
+ 'experimental-a2/actor is server-only — a client bundle imported it. Import actorReducer from experimental-a2/actor and the session client (experimental-a2/client, experimental-a2/react) in browser code instead.',
11
+ )
12
+ export {}