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/wire.ts ADDED
@@ -0,0 +1,454 @@
1
+ /**
2
+ * The wire format shared by experimental-a2/http (server side) and experimental-a2/client
3
+ * (browser side): events as JSON with ISO timestamps, and the A2Error
4
+ * envelope. Internal module — both entry points re-export what's
5
+ * public.
6
+ */
7
+
8
+ import { A2Error, type A2ErrorCode } from './errors.ts'
9
+ import { nullProtoRecord } from './internal.ts'
10
+ import type { Event } from './store.ts'
11
+ import type {
12
+ PresenceMap,
13
+ PresencePatch,
14
+ PresenceSnapshot,
15
+ } from './contract.ts'
16
+
17
+ export type WireEvent = {
18
+ id: string
19
+ type: string
20
+ payload: unknown
21
+ index: number
22
+ sessionId: string
23
+ /** ISO 8601 — revived to a Date on the client so reducers see the
24
+ * same shape on both sides of the wire. */
25
+ createdAt: string
26
+ }
27
+
28
+ export function eventToWire(event: Event): WireEvent {
29
+ return {
30
+ id: event.id,
31
+ type: event.type,
32
+ payload: event.payload,
33
+ index: event.index,
34
+ sessionId: event.sessionId,
35
+ createdAt: event.createdAt.toISOString(),
36
+ }
37
+ }
38
+
39
+ export function eventFromWire(wire: WireEvent): Event {
40
+ return {
41
+ id: wire.id,
42
+ type: wire.type,
43
+ payload: wire.payload,
44
+ index: wire.index,
45
+ sessionId: wire.sessionId,
46
+ createdAt: new Date(wire.createdAt),
47
+ }
48
+ }
49
+
50
+ export function isWireEvent(value: unknown): value is WireEvent {
51
+ if (value === null || typeof value !== 'object') return false
52
+ const v = value as Record<string, unknown>
53
+ return (
54
+ typeof v['id'] === 'string' &&
55
+ typeof v['type'] === 'string' &&
56
+ typeof v['index'] === 'number' &&
57
+ typeof v['sessionId'] === 'string' &&
58
+ typeof v['createdAt'] === 'string'
59
+ )
60
+ }
61
+
62
+ // ── the presence wire pair ───────────────────────────────────────────
63
+
64
+ /** The `event: presence` SSE frame body: one patch as JSON. */
65
+ export type WirePresencePatch = {
66
+ participant: string
67
+ values: Record<string, unknown>
68
+ seen: number
69
+ /** ISO 8601 — revived to a Date on the client. */
70
+ at: string
71
+ }
72
+
73
+ /**
74
+ * The `event: presence-snapshot` SSE frame body: the in-memory
75
+ * `PresenceSnapshot` shape verbatim, with each per-field `at`
76
+ * serialized to ISO 8601.
77
+ */
78
+ export type WirePresenceSnapshot = {
79
+ snapshot: {
80
+ [participant: string]: {
81
+ [field: string]: { value: unknown; seen: number; at: string }
82
+ }
83
+ }
84
+ }
85
+
86
+ export function presencePatchToWire(patch: PresencePatch): WirePresencePatch {
87
+ return {
88
+ participant: patch.participant,
89
+ values: patch.values,
90
+ seen: patch.seen,
91
+ at: patch.at.toISOString(),
92
+ }
93
+ }
94
+
95
+ export function presencePatchFromWire(wire: WirePresencePatch): PresencePatch {
96
+ return {
97
+ participant: wire.participant,
98
+ values: wire.values,
99
+ seen: wire.seen,
100
+ at: new Date(wire.at),
101
+ }
102
+ }
103
+
104
+ // Participant and field keys come off the wire, so every object keyed
105
+ // by them is built null-prototype — see `nullProtoRecord`.
106
+ export function presenceSnapshotToWire(
107
+ snapshot: PresenceSnapshot,
108
+ ): WirePresenceSnapshot {
109
+ const wire: WirePresenceSnapshot['snapshot'] = nullProtoRecord()
110
+ for (const [participant, fields] of Object.entries(snapshot.snapshot)) {
111
+ const wireFields: WirePresenceSnapshot['snapshot'][string] =
112
+ nullProtoRecord()
113
+ for (const [field, entry] of Object.entries(fields)) {
114
+ if (entry === undefined) continue
115
+ wireFields[field] = {
116
+ value: entry.value,
117
+ seen: entry.seen,
118
+ at: entry.at.toISOString(),
119
+ }
120
+ }
121
+ wire[participant] = wireFields
122
+ }
123
+ return { snapshot: wire }
124
+ }
125
+
126
+ export function presenceSnapshotFromWire(
127
+ wire: WirePresenceSnapshot,
128
+ ): PresenceSnapshot {
129
+ const map: PresenceMap = nullProtoRecord()
130
+ for (const [participant, fields] of Object.entries(wire.snapshot)) {
131
+ const revived: PresenceMap[string] = nullProtoRecord()
132
+ for (const [field, entry] of Object.entries(fields)) {
133
+ revived[field] = {
134
+ value: entry.value,
135
+ seen: entry.seen,
136
+ at: new Date(entry.at),
137
+ }
138
+ }
139
+ map[participant] = revived
140
+ }
141
+ return { snapshot: map }
142
+ }
143
+
144
+ export function isWirePresencePatch(
145
+ value: unknown,
146
+ ): value is WirePresencePatch {
147
+ if (value === null || typeof value !== 'object') return false
148
+ const v = value as Record<string, unknown>
149
+ return (
150
+ typeof v['participant'] === 'string' &&
151
+ v['values'] !== null &&
152
+ typeof v['values'] === 'object' &&
153
+ !Array.isArray(v['values']) &&
154
+ typeof v['seen'] === 'number' &&
155
+ typeof v['at'] === 'string'
156
+ )
157
+ }
158
+
159
+ export function isWirePresenceSnapshot(
160
+ value: unknown,
161
+ ): value is WirePresenceSnapshot {
162
+ if (value === null || typeof value !== 'object') return false
163
+ const v = value as Record<string, unknown>
164
+ return (
165
+ v['snapshot'] !== null &&
166
+ typeof v['snapshot'] === 'object' &&
167
+ !Array.isArray(v['snapshot'])
168
+ )
169
+ }
170
+
171
+ // ── the A2Error envelope ─────────────────────────────────────────────
172
+
173
+ export type WireError = {
174
+ error: { code: A2ErrorCode; message: string; details?: unknown }
175
+ }
176
+
177
+ const ERROR_STATUS: Record<A2ErrorCode, number> = {
178
+ INVALID_PAYLOAD: 400,
179
+ UNKNOWN_EVENT_TYPE: 400,
180
+ PARTIAL_DUPLICATE_BATCH: 400,
181
+ SUPERSEDED_ATTEMPT: 409,
182
+ CLAIM_EXPIRED: 409,
183
+ STORE_UNAVAILABLE: 503,
184
+ STORE_NOT_CONFIGURED: 500,
185
+ UNKNOWN_PRESENCE_FIELD: 400,
186
+ PRESENCE_NOT_SUPPORTED: 500,
187
+ }
188
+
189
+ export function errorStatus(code: A2ErrorCode): number {
190
+ return ERROR_STATUS[code]
191
+ }
192
+
193
+ export function errorToWire(error: A2Error): WireError {
194
+ const body: WireError = {
195
+ error: { code: error.code, message: error.message },
196
+ }
197
+ if (error.details !== undefined) body.error.details = error.details
198
+ return body
199
+ }
200
+
201
+ /** Wrap an arbitrary thrown value for the wire: A2Errors pass through,
202
+ * anything else becomes STORE_UNAVAILABLE — from the client's
203
+ * perspective an unknown server failure is retryable-once, not a
204
+ * protocol contract. */
205
+ export function asA2Error(error: unknown): A2Error {
206
+ return error instanceof A2Error
207
+ ? error
208
+ : new A2Error('STORE_UNAVAILABLE', 'internal error', { cause: error })
209
+ }
210
+
211
+ /** Rebuild an A2Error from a wire body; null if the body isn't one. */
212
+ export function errorFromWire(body: unknown): A2Error | null {
213
+ if (body === null || typeof body !== 'object') return null
214
+ const err = (body as { error?: unknown }).error
215
+ if (err === null || typeof err !== 'object') return null
216
+ const { code, message, details } = err as Record<string, unknown>
217
+ if (typeof code !== 'string' || !Object.hasOwn(ERROR_STATUS, code))
218
+ return null
219
+ return new A2Error(code as A2ErrorCode, String(message ?? code), {
220
+ details,
221
+ })
222
+ }
223
+
224
+ // ── the ws frame layer ───────────────────────────────────────────────
225
+ // The SSE lanes reframed for a socket (specs/a2-api.md §13): every
226
+ // message is one JSON text frame, and a frame is its wire payload plus
227
+ // a `kind` discriminant (plus `req` where a reply must correlate).
228
+ // Unknown kinds are skipped by both sides — the same
229
+ // forward-compatibility rule as named SSE frames.
230
+ //
231
+ // The multiplexed superset rides the same frames: `subscribe` /
232
+ // `unsubscribe` up-frames open and close per-session lanes on one
233
+ // socket, `sessionId` tags route everything else. Single-session
234
+ // frames (no tags) keep parsing unchanged — a route opts into
235
+ // multiplexing by choosing the multi-session server handler, never by
236
+ // breaking the old protocol.
237
+
238
+ /**
239
+ * Server → client, parsed: a live-stream item, a push ack, a
240
+ * subscription lifecycle notice, or the heartbeat. `parseSocketFrame`
241
+ * yields these; frames of unknown kind (or ones failing their shape
242
+ * guard) come back `null`. `sessionId` is present on frames from a
243
+ * multiplexed socket and absent on a single-session one.
244
+ */
245
+ export type SocketDownFrame =
246
+ | { kind: 'event'; event: WireEvent }
247
+ | { kind: 'presence'; patch: WirePresencePatch; sessionId?: string }
248
+ | {
249
+ kind: 'presence-snapshot'
250
+ snapshot: WirePresenceSnapshot
251
+ sessionId?: string
252
+ }
253
+ | { kind: 'ack'; req: number; events: WireEvent[]; sessionId?: string }
254
+ | { kind: 'ack'; req: number; error: A2Error; sessionId?: string }
255
+ | { kind: 'subscribed'; sessionId: string }
256
+ | { kind: 'unsubscribed'; sessionId: string; reason?: string }
257
+ | { kind: 'ping' }
258
+
259
+ /**
260
+ * Client → server: the push envelope's two planes. `sessionId` is
261
+ * implied by the socket on a single-session connection and required by
262
+ * the multiplexed handler. Plain JSON on the wire; the server parses
263
+ * and validates them through the same seams as `parsePushBody`.
264
+ */
265
+ export type SocketPushFrame = {
266
+ kind: 'push'
267
+ /** Client-local ack correlator — opaque to the server. */
268
+ req: number
269
+ events: Array<{ type: string; payload: unknown; id?: string }>
270
+ sessionId?: string
271
+ }
272
+
273
+ export type SocketPresenceFrame = {
274
+ kind: 'presence'
275
+ participant: string
276
+ values: Record<string, unknown>
277
+ seen?: number
278
+ at?: number
279
+ sessionId?: string
280
+ }
281
+
282
+ /** Open per-session lanes on a multiplexed socket. `index` is each
283
+ * session's exclusive resume frontier — `stream({ startAfter })`. */
284
+ export type SocketSubscribeFrame = {
285
+ kind: 'subscribe'
286
+ sessions: Array<{ id: string; index: number }>
287
+ }
288
+
289
+ export type SocketUnsubscribeFrame = {
290
+ kind: 'unsubscribe'
291
+ sessions: string[]
292
+ }
293
+
294
+ export type SocketUpFrame =
295
+ | SocketPushFrame
296
+ | SocketPresenceFrame
297
+ | SocketSubscribeFrame
298
+ | SocketUnsubscribeFrame
299
+
300
+ export const SOCKET_PING_FRAME: string = JSON.stringify({ kind: 'ping' })
301
+
302
+ /** One stream item as a socket frame — `sseResponse`'s framing over
303
+ * the same codecs, with `kind` instead of an SSE event name. On a
304
+ * multiplexed socket presence frames carry the `sessionId` tag; events
305
+ * already carry theirs in the wire event. */
306
+ export function socketFrameFor(
307
+ item: Event | PresencePatch | PresenceSnapshot,
308
+ sessionId?: string,
309
+ ): string {
310
+ const tag = sessionId === undefined ? {} : { sessionId }
311
+ if ('snapshot' in item) {
312
+ return JSON.stringify({
313
+ kind: 'presence-snapshot',
314
+ ...presenceSnapshotToWire(item),
315
+ ...tag,
316
+ })
317
+ }
318
+ if ('participant' in item) {
319
+ return JSON.stringify({
320
+ kind: 'presence',
321
+ ...presencePatchToWire(item),
322
+ ...tag,
323
+ })
324
+ }
325
+ return JSON.stringify({ kind: 'event', ...eventToWire(item) })
326
+ }
327
+
328
+ export function socketAckFor(
329
+ req: number,
330
+ events: Event[],
331
+ sessionId?: string,
332
+ ): string {
333
+ return JSON.stringify({
334
+ kind: 'ack',
335
+ req,
336
+ events: events.map(eventToWire),
337
+ ...(sessionId === undefined ? {} : { sessionId }),
338
+ })
339
+ }
340
+
341
+ export function socketErrorAckFor(
342
+ req: number,
343
+ error: A2Error,
344
+ sessionId?: string,
345
+ ): string {
346
+ return JSON.stringify({
347
+ kind: 'ack',
348
+ req,
349
+ ...errorToWire(error),
350
+ ...(sessionId === undefined ? {} : { sessionId }),
351
+ })
352
+ }
353
+
354
+ export function socketSubscribedFor(sessionId: string): string {
355
+ return JSON.stringify({ kind: 'subscribed', sessionId })
356
+ }
357
+
358
+ /** The subscription-over notice: with a `reason` the server rejected
359
+ * or lost the session's stream, without one it ended cleanly. */
360
+ export function socketUnsubscribedFor(
361
+ sessionId: string,
362
+ reason?: string,
363
+ ): string {
364
+ return JSON.stringify({
365
+ kind: 'unsubscribed',
366
+ sessionId,
367
+ ...(reason === undefined ? {} : { reason }),
368
+ })
369
+ }
370
+
371
+ export function parseSocketFrame(data: string): SocketDownFrame | null {
372
+ let parsed: unknown
373
+ try {
374
+ parsed = JSON.parse(data)
375
+ } catch {
376
+ return null
377
+ }
378
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
379
+ return null
380
+ }
381
+ const frame = parsed as Record<string, unknown>
382
+ switch (frame['kind']) {
383
+ case 'ping':
384
+ return { kind: 'ping' }
385
+ case 'event': {
386
+ if (!isWireEvent(parsed)) return null
387
+ const { id, type, payload, index, sessionId, createdAt } = parsed
388
+ return {
389
+ kind: 'event',
390
+ event: { id, type, payload, index, sessionId, createdAt },
391
+ }
392
+ }
393
+ case 'presence': {
394
+ if (!isWirePresencePatch(parsed)) return null
395
+ const { participant, values, seen, at } = parsed
396
+ return {
397
+ kind: 'presence',
398
+ patch: { participant, values, seen, at },
399
+ ...sessionTag(frame),
400
+ }
401
+ }
402
+ case 'presence-snapshot': {
403
+ if (!isWirePresenceSnapshot(parsed)) return null
404
+ return {
405
+ kind: 'presence-snapshot',
406
+ snapshot: { snapshot: parsed.snapshot },
407
+ ...sessionTag(frame),
408
+ }
409
+ }
410
+ case 'subscribed': {
411
+ const sessionId = frame['sessionId']
412
+ if (typeof sessionId !== 'string') return null
413
+ return { kind: 'subscribed', sessionId }
414
+ }
415
+ case 'unsubscribed': {
416
+ const sessionId = frame['sessionId']
417
+ if (typeof sessionId !== 'string') return null
418
+ const reason = frame['reason']
419
+ return typeof reason === 'string'
420
+ ? { kind: 'unsubscribed', sessionId, reason }
421
+ : { kind: 'unsubscribed', sessionId }
422
+ }
423
+ case 'ack': {
424
+ const req = frame['req']
425
+ if (typeof req !== 'number') return null
426
+ const tag = sessionTag(frame)
427
+ const events = frame['events']
428
+ if (Array.isArray(events) && events.every(isWireEvent)) {
429
+ return { kind: 'ack', req, events, ...tag }
430
+ }
431
+ const error = errorFromWire(parsed)
432
+ if (error) return { kind: 'ack', req, error, ...tag }
433
+ // A correlatable ack must never be dropped: its waiter would hang
434
+ // forever behind a healthy socket (pings keep the watchdog fed).
435
+ // An outcome this client cannot interpret — an error code from a
436
+ // newer server, an event shape that fails a guard — degrades to
437
+ // a lost ack: retryable, and the client-generated ids make the
438
+ // retry idempotent even if the append actually committed.
439
+ return {
440
+ kind: 'ack',
441
+ req,
442
+ error: new A2Error('STORE_UNAVAILABLE', 'unintelligible ack'),
443
+ ...tag,
444
+ }
445
+ }
446
+ default:
447
+ return null
448
+ }
449
+ }
450
+
451
+ const sessionTag = (frame: Record<string, unknown>): { sessionId?: string } => {
452
+ const sessionId = frame['sessionId']
453
+ return typeof sessionId === 'string' ? { sessionId } : {}
454
+ }
@@ -1,44 +0,0 @@
1
- //#region src/internal.ts
2
- const serverInternals = /* @__PURE__ */ new WeakMap();
3
- /**
4
- * Drain timing knobs. Mutable only as a white-box test seam. Claim
5
- * heartbeats run on real timers, so tests shrink these to keep suites
6
- * fast. Production always uses the defaults.
7
- */
8
- const DRAIN_TIMINGS = {
9
- claimTtlMs: 5e3,
10
- claimHeartbeatMs: 2e3,
11
- recoveryGraceMs: 1e3,
12
- recoveryArmTimeoutMs: 2e3
13
- };
14
- /**
15
- * Stream liveness knobs — the server-side SSE heartbeat and the
16
- * client-side stall watchdog. Paired by design: the client declares a
17
- * connection dead after roughly two missed heartbeats. Mutable only as
18
- * a white-box test seam; production always uses the defaults.
19
- */
20
- /**
21
- * Poll cadence for the poll-based log streams (sqlite, postgres) —
22
- * owned, not configurable: adaptive polling removed the operator's
23
- * reason to tune it (the floor keeps active streams smooth; the
24
- * ceiling bounds idle cost). Mutable only as a white-box test seam so
25
- * suites don't wait out real idle gaps.
26
- */
27
- const POLL_TIMINGS = {
28
- activeFloorMs: 25,
29
- idleCeilingMs: 250
30
- };
31
- /**
32
- * Safety-read cadence for the notify-driven redis live stream. A
33
- * parked feed re-reads the log at this interval even without a
34
- * pub/sub wake-up, so a lost notification (subscriber reconnect gap,
35
- * dropped PUBLISH) costs latency, never an event. Mutable only as a
36
- * white-box test seam.
37
- */
38
- const NOTIFY_TIMINGS = { safetyReadMs: 1e4 };
39
- const STREAM_TIMINGS = {
40
- sseHeartbeatMs: 15e3,
41
- stallTimeoutMs: 35e3
42
- };
43
- //#endregion
44
- export { serverInternals as a, STREAM_TIMINGS as i, NOTIFY_TIMINGS as n, POLL_TIMINGS as r, DRAIN_TIMINGS as t };
@@ -1,11 +0,0 @@
1
- import { c as IdSource, i as Clock, t as A2Log } from "./log-ldf5g8Cx.js";
2
- //#region src/log-memory.d.ts
3
- type MemoryLogOptions = {
4
- /** Injectable clock — every stored timestamp comes from here. */
5
- clock?: Clock;
6
- /** Injectable id source for generated event ids. */
7
- ids?: IdSource;
8
- };
9
- declare function memory(options?: MemoryLogOptions): A2Log;
10
- //#endregion
11
- export { MemoryLogOptions, memory };
@@ -1,119 +0,0 @@
1
- import { a as serverInternals, t as DRAIN_TIMINGS } from "./internal-gCd5qMry.js";
2
- import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
3
- import { n as SYSTEM_CLOCK } from "./log-yJbXUf72.js";
4
- //#region src/recovery-vercel.ts
5
- /**
6
- * experimental-a2/recovery-vercel — queue-backed recovery over Vercel Queues.
7
- *
8
- * `vercelQueues()` returns an `A2Recovery` (a2-implementation.md §9):
9
- *
10
- * - `arm` rounds the requested watchdog time up to a one-second slot.
11
- * One message per `(contract, session, dueAt)` coalesces appends,
12
- * claim renewals and racing callbacks. `DuplicateMessageError` is
13
- * success because that due-time slot is already durable.
14
- * - `handler(...servers)` is the delivery route: look up the server for
15
- * the contract named in the message and drain it. Settled drains ack;
16
- * contention arms a later watchdog before acking; handler failure keeps
17
- * the current message and redelivers with backoff.
18
- *
19
- * `@vercel/queue` is an optional peer dependency, imported lazily. The
20
- * `transport` option is the injection seam — tests run a real little
21
- * in-memory queue through the same interface.
22
- */
23
- /**
24
- * The "not settled yet" signal for a stalled handler — an expected state,
25
- * not a transport failure. The default transport classifies it in
26
- * `@vercel/queue`'s `retry` option and reschedules the delivery quietly
27
- * (`{ afterSeconds }` → visibility change → 200 response, no error log)
28
- * instead of letting it surface as a thrown 500. Busy deliveries use a
29
- * fresh watchdog instead. Custom transports should treat this class the
30
- * same way when their protocol has a polite reschedule.
31
- */
32
- var RecoveryUnsettled = class extends Error {};
33
- /** Vercel Queues rejects shorter visibility changes. */
34
- const MIN_VISIBILITY_SECONDS = 30;
35
- /** Redelivery backoff for retry states: base, 2×, 4×… bounded. */
36
- const unsettledBackoffSeconds = (delaySeconds, deliveryCount) => Math.min(Math.max(delaySeconds * 2 ** Math.max(deliveryCount - 1, 0), MIN_VISIBILITY_SECONDS), 60);
37
- const vercelQueueTransport = async (delaySeconds) => {
38
- const queue = await import("@vercel/queue").catch(() => {
39
- throw new Error("experimental-a2/recovery-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport");
40
- });
41
- return {
42
- send: (topic, message, options) => queue.send(topic, message, options),
43
- handleCallback: (handler) => queue.handleCallback(handler, {
44
- visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,
45
- retry: (error, metadata) => error instanceof RecoveryUnsettled ? { afterSeconds: unsettledBackoffSeconds(delaySeconds, metadata.deliveryCount) } : void 0
46
- }),
47
- isDuplicate: (error) => error instanceof queue.DuplicateMessageError
48
- };
49
- };
50
- function vercelQueues(options = {}) {
51
- const topic = options.topic ?? "a2";
52
- const delaySeconds = options.delaySeconds ?? 5;
53
- const clock = options.clock ?? SYSTEM_CLOCK;
54
- const transport = retryableLazy(() => options.transport ? Promise.resolve(options.transport) : vercelQueueTransport(delaySeconds)).get;
55
- const sendWatchdog = async (opts) => {
56
- const t = await transport();
57
- const nowMs = clock.now().getTime();
58
- const requestedDueAt = opts.dueAt ?? nowMs + delaySeconds * 1e3;
59
- const dueAt = Math.ceil(requestedDueAt / 1e3) * 1e3;
60
- const recoveryId = `${opts.contract}:${opts.sessionId}:watchdog:${dueAt}`;
61
- try {
62
- await t.send(topic, {
63
- contract: opts.contract,
64
- sessionId: opts.sessionId,
65
- dueAt
66
- }, {
67
- delaySeconds: Math.max(0, Math.ceil((dueAt - nowMs) / 1e3)),
68
- idempotencyKey: recoveryId
69
- });
70
- } catch (err) {
71
- if (t.isDuplicate(err)) return;
72
- throw err;
73
- }
74
- };
75
- const successorDueAt = (message) => {
76
- const minimumDueAt = clock.now().getTime() + DRAIN_TIMINGS.claimTtlMs + DRAIN_TIMINGS.recoveryGraceMs;
77
- if (message.dueAt === void 0) return minimumDueAt;
78
- const beats = Math.max(1, Math.ceil((minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs));
79
- return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs;
80
- };
81
- return {
82
- async arm({ contract, sessionId, dueAt }) {
83
- await sendWatchdog({
84
- contract,
85
- sessionId,
86
- ...dueAt !== void 0 ? { dueAt } : {}
87
- });
88
- },
89
- handler(...servers) {
90
- const byContract = /* @__PURE__ */ new Map();
91
- for (const server of servers) byContract.set(server.contract.name, server);
92
- return async (req) => {
93
- return (await transport()).handleCallback(async (message) => {
94
- const server = byContract.get(message.contract);
95
- if (!server) throw new Error(`a2 recovery: no server for contract '${message.contract}' — pass it to recovery.handler(...)`);
96
- const internals = serverInternals.get(server);
97
- const nextDueAt = successorDueAt(message);
98
- const result = internals ? await internals.recoveryDrain(message.sessionId, { recoveryDueAt: nextDueAt }) : {
99
- ...await server.drain(message.sessionId),
100
- outcome: "unknown"
101
- };
102
- if (result.settled) return;
103
- if (result.outcome === "busy") {
104
- if (result.recoveryArm) await result.recoveryArm;
105
- else await sendWatchdog({
106
- contract: message.contract,
107
- sessionId: message.sessionId,
108
- dueAt: nextDueAt
109
- });
110
- return;
111
- }
112
- throw new RecoveryUnsettled(`a2 recovery: session '${message.sessionId}' of '${message.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`);
113
- })(req);
114
- };
115
- }
116
- };
117
- }
118
- //#endregion
119
- export { RecoveryUnsettled, vercelQueues };