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
|
@@ -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 {}
|