experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
package/src/react.ts ADDED
@@ -0,0 +1,284 @@
1
+ 'use client'
2
+ /**
3
+ * experimental-a2/react — React bindings over experimental-a2/client.
4
+ *
5
+ * `createReact({ client })` is a factory (like createContext): call it
6
+ * once in a `'use client'` module with a shared A2 client and export the
7
+ * bound pair. Server components import `SessionProvider` from that module
8
+ * as a client reference; client components import `useSession` from the
9
+ * same file. The shared client also makes imperative pushes and provider
10
+ * renders resolve the same live session. Everything is typed by the
11
+ * reducer value — no type arguments, and nothing here ever touches the
12
+ * machine module.
13
+ */
14
+
15
+ import {
16
+ createContext,
17
+ createElement,
18
+ useContext,
19
+ useEffect,
20
+ useMemo,
21
+ useSyncExternalStore,
22
+ type ReactElement,
23
+ type ReactNode,
24
+ } from 'react'
25
+ import {
26
+ createClient,
27
+ type A2Client,
28
+ type Connection,
29
+ type HistoryState,
30
+ type LoadHistoryOptions,
31
+ type PushResult,
32
+ type SessionClient,
33
+ type SessionClientPresence,
34
+ type SessionSnapshot,
35
+ } from './client.ts'
36
+ import type {
37
+ AppendInput,
38
+ ContractEvent,
39
+ EventDefs,
40
+ PresenceDefs,
41
+ PresenceMap,
42
+ PresencePatch,
43
+ WithPresence,
44
+ } from './contract.ts'
45
+ import type { Reducer } from './reducer.ts'
46
+
47
+ export type SessionProviderProps<D extends EventDefs, S> = {
48
+ /** Which session to subscribe to. */
49
+ sessionId: string
50
+ /** Base path of the route exposing GET (stream) and POST (push). */
51
+ api: string
52
+ /** The server-rendered fold — the first paint, no client JS needed. */
53
+ initialState: S
54
+ /** The fold's frontier — the stream resumes exactly there. */
55
+ initialIndex: number
56
+ /** Server-rendered event history through `initialIndex`. */
57
+ initialEvents?: ContractEvent<D>[]
58
+ /**
59
+ * This client's presence identity — required to call `setPresence`.
60
+ * Caller-supplied (a user id, a tab nonce, a guest name): A2 does
61
+ * not invent an identity story.
62
+ */
63
+ participant?: string
64
+ children?: ReactNode
65
+ }
66
+
67
+ export type BoundSessionProviderProps<D extends EventDefs, S> = Omit<
68
+ SessionProviderProps<D, S>,
69
+ 'api'
70
+ >
71
+
72
+ /**
73
+ * The presence members of the hook result — intersected in via
74
+ * `WithPresence`, so they exist exactly when the contract declares
75
+ * presence fields (the reducer is the client's typed handle on it).
76
+ */
77
+ export type UseSessionPresence<P extends PresenceDefs> = {
78
+ /** The replicated presence map, this client included (local echo). */
79
+ presence: PresenceMap<P>
80
+ /**
81
+ * Fire-and-forget: validated locally, echoed into the map
82
+ * immediately, coalesced on the wire to a fixed cadence (a leading
83
+ * send immediately, then at most one merged send per interval;
84
+ * later values win per field), stamped with the frontier at send
85
+ * time as `seen`. Requires the provider's `participant`.
86
+ */
87
+ setPresence: (values: PresencePatch<P>['values']) => void
88
+ }
89
+
90
+ export type UseSessionResult<
91
+ D extends EventDefs,
92
+ S,
93
+ P extends PresenceDefs = Record<never, never>,
94
+ > = WithPresence<P, UseSessionPresence<P>> & {
95
+ /** The live view, folded through the shared reducer. */
96
+ state: S
97
+ /** The raw event feed — observed, seeded, or backscrolled. */
98
+ events: ContractEvent<D>[]
99
+ /** The stream frontier — `lastSeenIndex` for cancellation. */
100
+ index: number
101
+ /**
102
+ * The connection, as a discriminated union — `error` exists only
103
+ * while disconnected; "reconnecting…" is
104
+ * `status === 'connecting' && reconnects > 0`.
105
+ */
106
+ connection: Connection
107
+ /**
108
+ * Typed optimistic append. Awaiting it gives the server ack;
109
+ * `.confirmed` resolves when the live stream has delivered the batch
110
+ * back. Rejects with the server's A2Error codes.
111
+ */
112
+ push: (...events: AppendInput<D>[]) => PushResult<D>
113
+ /**
114
+ * Backscroll: fetch a bounded slice of older events into `events`.
115
+ * Defaults page backward 50 at a time from the oldest loaded event.
116
+ * Display data only — `state` and the optimistic overlay never move.
117
+ */
118
+ loadHistory: (options?: LoadHistoryOptions) => Promise<ContractEvent<D>[]>
119
+ /** Backscroll progress: `complete` means the feed reaches index 1. */
120
+ history: HistoryState
121
+ }
122
+
123
+ export type A2React<
124
+ D extends EventDefs,
125
+ S,
126
+ P extends PresenceDefs = Record<never, never>,
127
+ > = {
128
+ SessionProvider: (props: SessionProviderProps<D, S>) => ReactElement
129
+ useSession: () => UseSessionResult<D, S, P>
130
+ }
131
+
132
+ export type BoundA2React<
133
+ D extends EventDefs,
134
+ S,
135
+ P extends PresenceDefs = Record<never, never>,
136
+ > = {
137
+ SessionProvider: (props: BoundSessionProviderProps<D, S>) => ReactElement
138
+ useSession: () => UseSessionResult<D, S, P>
139
+ }
140
+
141
+ export function createReact<
142
+ D extends EventDefs,
143
+ S,
144
+ P extends PresenceDefs = Record<never, never>,
145
+ >(options: { client: A2Client<D, S, P> }): BoundA2React<D, S, P>
146
+ export function createReact<
147
+ D extends EventDefs,
148
+ S,
149
+ P extends PresenceDefs = Record<never, never>,
150
+ >(options: { reducer: Reducer<D, S, P> }): A2React<D, S, P>
151
+ export function createReact<
152
+ D extends EventDefs,
153
+ S,
154
+ P extends PresenceDefs = Record<never, never>,
155
+ >(
156
+ options: { client: A2Client<D, S, P> } | { reducer: Reducer<D, S, P> },
157
+ ): BoundA2React<D, S, P> | A2React<D, S, P> {
158
+ const Context = createContext<SessionClient<D, S, P> | null>(null)
159
+ const providerMounts = new WeakMap<SessionClient<D, S, P> & object, number>()
160
+ const resolveSession: (
161
+ sessionId: string,
162
+ api: string | undefined,
163
+ initialState: S,
164
+ initialIndex: number,
165
+ initialEvents: ContractEvent<D>[] | undefined,
166
+ participant: string | undefined,
167
+ ) => SessionClient<D, S, P> =
168
+ 'client' in options
169
+ ? (
170
+ sessionId,
171
+ _api,
172
+ initialState,
173
+ initialIndex,
174
+ initialEvents,
175
+ participant,
176
+ ) =>
177
+ options.client.session(sessionId, {
178
+ initialState,
179
+ initialIndex,
180
+ ...(initialEvents === undefined ? {} : { initialEvents }),
181
+ ...(participant === undefined ? {} : { participant }),
182
+ })
183
+ : (() => {
184
+ const clients = new Map<string, A2Client<D, S, P>>()
185
+ return (
186
+ sessionId: string,
187
+ api: string | undefined,
188
+ initialState: S,
189
+ initialIndex: number,
190
+ initialEvents: ContractEvent<D>[] | undefined,
191
+ participant: string | undefined,
192
+ ) => {
193
+ if (api === undefined) {
194
+ throw new TypeError(
195
+ 'SessionProvider requires api when createReact binds a reducer',
196
+ )
197
+ }
198
+ let client = clients.get(api)
199
+ if (!client) {
200
+ client = createClient({ reducer: options.reducer, api })
201
+ clients.set(api, client)
202
+ }
203
+ return client.session(sessionId, {
204
+ initialState,
205
+ initialIndex,
206
+ ...(initialEvents === undefined ? {} : { initialEvents }),
207
+ ...(participant === undefined ? {} : { participant }),
208
+ })
209
+ }
210
+ })()
211
+
212
+ function SessionProvider(
213
+ props: SessionProviderProps<D, S> | BoundSessionProviderProps<D, S>,
214
+ ): ReactElement {
215
+ const {
216
+ sessionId,
217
+ initialState,
218
+ initialIndex,
219
+ initialEvents,
220
+ participant,
221
+ } = props
222
+ const api = 'api' in props ? props.api : undefined
223
+ const client = useMemo(
224
+ () =>
225
+ resolveSession(
226
+ sessionId,
227
+ api,
228
+ initialState,
229
+ initialIndex,
230
+ initialEvents,
231
+ participant,
232
+ ),
233
+ [sessionId, api, initialState, initialIndex, initialEvents, participant],
234
+ )
235
+ useEffect(() => {
236
+ const mounts = providerMounts.get(client) ?? 0
237
+ providerMounts.set(client, mounts + 1)
238
+ if (mounts === 0) client.connect()
239
+ return () => {
240
+ const remaining = (providerMounts.get(client) ?? 1) - 1
241
+ if (remaining === 0) {
242
+ providerMounts.delete(client)
243
+ client.close()
244
+ } else {
245
+ providerMounts.set(client, remaining)
246
+ }
247
+ }
248
+ }, [client])
249
+ return createElement(Context.Provider, { value: client }, props.children)
250
+ }
251
+
252
+ function useSession(): UseSessionResult<D, S, P> {
253
+ const client = useContext(Context)
254
+ if (!client) {
255
+ throw new Error(
256
+ 'useSession must be rendered inside its matching SessionProvider',
257
+ )
258
+ }
259
+ const snapshot: SessionSnapshot<D, S, P> = useSyncExternalStore(
260
+ client.subscribe,
261
+ client.getSnapshot,
262
+ client.getSnapshot,
263
+ )
264
+ return useMemo(() => {
265
+ // Runtime mirror of the WithPresence surface: the client attaches
266
+ // setPresence (and the snapshot carries presence) exactly when the
267
+ // reducer declares presence fields — no runtime-inert members.
268
+ const { setPresence } = client as Partial<SessionClientPresence<P>>
269
+ const { presence } = snapshot as Partial<{ presence: PresenceMap<P> }>
270
+ return {
271
+ state: snapshot.state,
272
+ events: snapshot.events,
273
+ index: snapshot.index,
274
+ connection: snapshot.connection,
275
+ push: client.push,
276
+ loadHistory: client.loadHistory,
277
+ history: snapshot.history,
278
+ ...(setPresence && presence ? { presence, setPresence } : {}),
279
+ } as UseSessionResult<D, S, P>
280
+ }, [snapshot, client])
281
+ }
282
+
283
+ return { SessionProvider, useSession }
284
+ }
package/src/reducer.ts ADDED
@@ -0,0 +1,108 @@
1
+ import type { ContractEvent, EventDefs, PresenceDefs } from './contract.ts'
2
+ import type { StandardSchemaV1 } from './standard-schema.ts'
3
+ import { validateSync } from './validate.ts'
4
+
5
+ /**
6
+ * A pure fold over a session's events — `(state, event) => state` —
7
+ * derived from a contract via `contract.reducer(options).fold(fn)`.
8
+ *
9
+ * The name is the reducer's identity. Cached folds (server snapshots,
10
+ * the browser cache) are keyed by it, which makes it the invalidation
11
+ * knob too: changed the fold's logic? Change the name. Snapshots under
12
+ * the old name are ignored and the next read refolds from raw events.
13
+ * See docs/concepts/04-state.mdx.
14
+ */
15
+ export type Reducer<
16
+ D extends EventDefs = EventDefs,
17
+ S = unknown,
18
+ P extends PresenceDefs = Record<never, never>,
19
+ > = {
20
+ /** Identity and cache key. Rename when the fold's logic changes. */
21
+ readonly name: string
22
+ /** The contract's vocabulary — types every event in the fold. */
23
+ readonly events: Readonly<D>
24
+ /**
25
+ * The contract's presence vocabulary — carried because the reducer
26
+ * is the client's typed handle on the contract. The fold never sees
27
+ * presence.
28
+ */
29
+ readonly presence: Readonly<P>
30
+ readonly fold: (state: S, event: ContractEvent<D>) => S
31
+ readonly initialState: S
32
+ /**
33
+ * When present: `initialState` was validated against it at
34
+ * definition, and any cached fold a2 rehydrates (server snapshots,
35
+ * the browser cache) is checked against it — failing snapshots are
36
+ * discarded and refolded, catching shape drift a stale name can't.
37
+ */
38
+ readonly stateSchema: StandardSchemaV1<unknown, S> | undefined
39
+ }
40
+
41
+ /** The second step of `contract.reducer` — see the method's doc. */
42
+ export type ReducerBuilder<
43
+ D extends EventDefs,
44
+ S,
45
+ P extends PresenceDefs = Record<never, never>,
46
+ > = {
47
+ fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S, P>
48
+ }
49
+
50
+ /** Internal — reducers are created through `contract.reducer(...).fold(...)`. */
51
+ export function makeReducerBuilder<
52
+ D extends EventDefs,
53
+ P extends PresenceDefs,
54
+ Seed,
55
+ S,
56
+ >(
57
+ events: Readonly<D>,
58
+ presence: Readonly<P>,
59
+ options: {
60
+ name: string
61
+ initialState: Seed
62
+ stateSchema?: StandardSchemaV1<Seed, S>
63
+ },
64
+ ): ReducerBuilder<D, S, P> {
65
+ const { name } = options
66
+ if (typeof name !== 'string' || name.length === 0) {
67
+ throw new TypeError(
68
+ 'reducer name must be a non-empty string — it identifies the reducer and keys cached snapshots',
69
+ )
70
+ }
71
+
72
+ // Without a schema, S = Seed (the type parameter default), so the
73
+ // seed is the state. With one, the seed is the schema's input and the
74
+ // validated output (defaults applied) becomes the fold's actual seed.
75
+ let initialState = options.initialState as unknown as S
76
+ if (options.stateSchema) {
77
+ const result = validateSync(
78
+ options.stateSchema,
79
+ options.initialState,
80
+ 'the stateSchema',
81
+ )
82
+ if (result.issues) {
83
+ throw new TypeError(
84
+ `reducer initialState does not match its stateSchema: ${result.issues
85
+ .map((issue) => issue.message)
86
+ .join('; ')}`,
87
+ )
88
+ }
89
+ initialState = result.value as S
90
+ }
91
+
92
+ return {
93
+ fold(fold) {
94
+ if (typeof fold !== 'function') {
95
+ throw new TypeError('reducer fold must be a function')
96
+ }
97
+ return {
98
+ name,
99
+ events,
100
+ presence,
101
+ fold,
102
+ initialState,
103
+ stateSchema: options.stateSchema as
104
+ StandardSchemaV1<unknown, S> | undefined,
105
+ }
106
+ },
107
+ }
108
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * One lazy in-flight attempt: concurrent callers share it, fulfilled values
3
+ * stay cached, and rejected attempts are evicted so a later call can retry.
4
+ */
5
+ export type RetryableLazy<T> = {
6
+ get(): Promise<T>
7
+ peek(): Promise<T> | null
8
+ }
9
+
10
+ export const retryableLazy = <T>(
11
+ initialize: () => Promise<T>,
12
+ ): RetryableLazy<T> => {
13
+ let current: Promise<T> | null = null
14
+
15
+ const get = (): Promise<T> => {
16
+ if (current) return current
17
+
18
+ const attempt = Promise.resolve().then(initialize)
19
+ current = attempt
20
+ void attempt.catch(() => {
21
+ if (current === attempt) current = null
22
+ })
23
+ return attempt
24
+ }
25
+
26
+ return { get, peek: () => current }
27
+ }