experimental-a2 0.5.1 → 0.7.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 (148) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +8 -7
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +41 -7
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +74 -35
  21. package/dist/react.js.map +1 -1
  22. package/dist/scheduler-qstash.d.ts +3 -3
  23. package/dist/scheduler-qstash.js +4 -5
  24. package/dist/scheduler-qstash.js.map +1 -1
  25. package/dist/scheduler-vercel.d.ts +2 -2
  26. package/dist/scheduler-vercel.js +4 -4
  27. package/dist/scheduler-vercel.js.map +1 -1
  28. package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
  29. package/dist/server-286j79Mt.js.map +1 -0
  30. package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
  31. package/dist/server-DgXmORIq.d.ts.map +1 -0
  32. package/dist/server.d.ts +3 -3
  33. package/dist/server.js +1 -1
  34. package/dist/store-N8PXxDAS.js.map +1 -1
  35. package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
  36. package/dist/store-flRz1OWh.d.ts.map +1 -0
  37. package/dist/store-memory.d.ts +1 -1
  38. package/dist/store-memory.d.ts.map +1 -1
  39. package/dist/store-memory.js +1 -59
  40. package/dist/store-memory.js.map +1 -1
  41. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  42. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  43. package/dist/store-postgres.d.ts +1 -1
  44. package/dist/store-postgres.js +1 -81
  45. package/dist/store-postgres.js.map +1 -1
  46. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
  47. package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
  48. package/dist/store-redis-http.d.ts +1 -1
  49. package/dist/store-redis-http.js +2 -3
  50. package/dist/store-redis-http.js.map +1 -1
  51. package/dist/store-redis.d.ts +1 -1
  52. package/dist/store-redis.js +3 -4
  53. package/dist/store-redis.js.map +1 -1
  54. package/dist/store-sqlite.d.ts +1 -1
  55. package/dist/store-sqlite.d.ts.map +1 -1
  56. package/dist/store-sqlite.js +1 -72
  57. package/dist/store-sqlite.js.map +1 -1
  58. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  59. package/dist/wire-B6te_wns.js.map +1 -0
  60. package/docs/guides/03-react.mdx +118 -29
  61. package/docs/guides/05-production.mdx +9 -11
  62. package/docs/guides/06-ai-agents.mdx +8 -14
  63. package/docs/guides/09-presence.mdx +19 -21
  64. package/docs/guides/10-transports.mdx +104 -86
  65. package/docs/reference/01-api.mdx +186 -279
  66. package/docs/reference/02-errors.mdx +5 -7
  67. package/package.json +1 -14
  68. package/src/ai-server.ts +9 -5
  69. package/src/client.ts +71 -35
  70. package/src/errors.ts +1 -0
  71. package/src/internal.ts +3 -62
  72. package/src/push-envelope.ts +24 -21
  73. package/src/react.ts +118 -44
  74. package/src/scheduler-qstash.ts +3 -3
  75. package/src/scheduler-vercel.ts +2 -2
  76. package/src/server-fetch.ts +344 -0
  77. package/src/server.ts +73 -225
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +0 -81
  81. package/src/store-postgres.ts +0 -100
  82. package/src/store-redis-core.ts +47 -211
  83. package/src/store-redis-http.ts +0 -1
  84. package/src/store-redis.ts +0 -1
  85. package/src/store-sqlite.ts +0 -119
  86. package/src/store.ts +0 -60
  87. package/src/wire.ts +2 -1
  88. package/dist/ai-D_PGS-JR.d.ts +0 -334
  89. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  90. package/dist/cli-B3VuxoDe.js +0 -599
  91. package/dist/cli-B3VuxoDe.js.map +0 -1
  92. package/dist/cli-bin.d.ts +0 -1
  93. package/dist/cli-bin.js +0 -7
  94. package/dist/cli-bin.js.map +0 -1
  95. package/dist/cli.d.ts +0 -20
  96. package/dist/cli.d.ts.map +0 -1
  97. package/dist/cli.js +0 -2
  98. package/dist/client-BKlyLiOU.js +0 -1008
  99. package/dist/client-BKlyLiOU.js.map +0 -1
  100. package/dist/client-D7mvIXrF.d.ts +0 -191
  101. package/dist/client-D7mvIXrF.d.ts.map +0 -1
  102. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  103. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  104. package/dist/devtools-kJJaORn-.js +0 -340
  105. package/dist/devtools-kJJaORn-.js.map +0 -1
  106. package/dist/devtools-server.browser.d.ts +0 -1
  107. package/dist/devtools-server.browser.js +0 -6
  108. package/dist/devtools-server.browser.js.map +0 -1
  109. package/dist/devtools-server.d.ts +0 -23
  110. package/dist/devtools-server.d.ts.map +0 -1
  111. package/dist/devtools-server.js +0 -1270
  112. package/dist/devtools-server.js.map +0 -1
  113. package/dist/devtools.d.ts +0 -2
  114. package/dist/devtools.js +0 -2
  115. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  116. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  117. package/dist/http.d.ts +0 -151
  118. package/dist/http.d.ts.map +0 -1
  119. package/dist/http.js +0 -706
  120. package/dist/http.js.map +0 -1
  121. package/dist/inspection-DaxB5jM2.js +0 -13
  122. package/dist/inspection-DaxB5jM2.js.map +0 -1
  123. package/dist/internal-DstsI6Re.js.map +0 -1
  124. package/dist/platform-B4TnJtWu.js +0 -34
  125. package/dist/platform-B4TnJtWu.js.map +0 -1
  126. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  127. package/dist/server-Duw6MVlB.js.map +0 -1
  128. package/dist/store-DysUkTH3.d.ts.map +0 -1
  129. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  130. package/dist/testing.browser.d.ts +0 -1
  131. package/dist/testing.browser.js +0 -6
  132. package/dist/testing.browser.js.map +0 -1
  133. package/dist/testing.d.ts +0 -32
  134. package/dist/testing.d.ts.map +0 -1
  135. package/dist/testing.js +0 -103
  136. package/dist/testing.js.map +0 -1
  137. package/dist/wire-BFQmSJ-9.js.map +0 -1
  138. package/docs/guides/07-devtools.mdx +0 -229
  139. package/src/cli-bin.ts +0 -5
  140. package/src/cli.ts +0 -1046
  141. package/src/devtools-app.ts +0 -989
  142. package/src/devtools-server.browser.ts +0 -5
  143. package/src/devtools-server.ts +0 -604
  144. package/src/devtools.ts +0 -716
  145. package/src/http.ts +0 -394
  146. package/src/inspection.ts +0 -39
  147. package/src/testing.browser.ts +0 -5
  148. package/src/testing.ts +0 -185
@@ -0,0 +1,344 @@
1
+ import type {
2
+ AppendInput,
3
+ ContractEvent,
4
+ EventDefs,
5
+ PresenceDefs,
6
+ } from './contract.ts'
7
+ import { A2Error } from './errors.ts'
8
+ import { invocationDeadlineMs } from './platform.ts'
9
+ import {
10
+ invalidPushBody,
11
+ parsePushBody,
12
+ type ParsedPushEvent,
13
+ type ParsedPushPresence,
14
+ } from './push-envelope.ts'
15
+ import type { A2Server } from './server.ts'
16
+ import {
17
+ sessionsSocket,
18
+ type A2Socket,
19
+ type SocketSession,
20
+ } from './session-socket.ts'
21
+ import { sseResponse } from './sse.ts'
22
+ import { asA2Error, errorStatus, errorToWire, eventToWire } from './wire.ts'
23
+
24
+ type SuggestedEventType<D extends EventDefs> =
25
+ (keyof D & string) | (string & Record<never, never>)
26
+
27
+ type SuggestedPresenceValues<P extends PresenceDefs> = Readonly<
28
+ { [F in keyof P & string]?: unknown } & Record<string, unknown>
29
+ >
30
+
31
+ export type A2PushEvent<D extends EventDefs = EventDefs> = {
32
+ readonly type: SuggestedEventType<D>
33
+ readonly payload: unknown
34
+ readonly id?: string
35
+ }
36
+
37
+ export type A2PushPresence<P extends PresenceDefs = PresenceDefs> = {
38
+ readonly participant: string
39
+ readonly values: SuggestedPresenceValues<P>
40
+ readonly seen?: number
41
+ readonly at?: number
42
+ }
43
+
44
+ export type A2Operation<
45
+ D extends EventDefs = EventDefs,
46
+ P extends PresenceDefs = Record<never, never>,
47
+ > =
48
+ | {
49
+ readonly type: 'stream'
50
+ readonly sessionId: string
51
+ readonly startAfter: number
52
+ readonly transport: 'http' | 'websocket'
53
+ }
54
+ | {
55
+ readonly type: 'history'
56
+ readonly sessionId: string
57
+ readonly gte: number
58
+ readonly lte: number
59
+ readonly transport: 'http'
60
+ }
61
+ | {
62
+ readonly type: 'push'
63
+ readonly sessionId: string
64
+ readonly events: readonly A2PushEvent<D>[]
65
+ readonly presence?: A2PushPresence<P>
66
+ readonly transport: 'http' | 'websocket'
67
+ }
68
+
69
+ export type UpgradeWebSocket = (
70
+ attach: (socket: A2Socket) => void,
71
+ ) => Response | Promise<Response>
72
+
73
+ export type ServerFetchOptions<
74
+ D extends EventDefs = EventDefs,
75
+ P extends PresenceDefs = Record<never, never>,
76
+ > = {
77
+ authorize?: (operation: A2Operation<D, P>) => boolean | Promise<boolean>
78
+ upgradeWebSocket?: UpgradeWebSocket
79
+ }
80
+
81
+ export type ServerIngressContext = {
82
+ sessionId: string
83
+ events: readonly ParsedPushEvent[]
84
+ presence?: ParsedPushPresence
85
+ }
86
+
87
+ type ServerFetchHooks = {
88
+ validateIngress?(context: ServerIngressContext): void | PromiseLike<void>
89
+ }
90
+
91
+ const serverFetchHooks = new WeakMap<object, ServerFetchHooks>()
92
+
93
+ export function setServerFetchHooks<
94
+ D extends EventDefs,
95
+ P extends PresenceDefs,
96
+ >(server: Pick<A2Server<D, P>, 'fetch'>, hooks: ServerFetchHooks): void {
97
+ serverFetchHooks.set(server.fetch, hooks)
98
+ }
99
+
100
+ type FetchServer<D extends EventDefs, P extends PresenceDefs> = Pick<
101
+ A2Server<D, P>,
102
+ 'contract' | 'session'
103
+ >
104
+
105
+ type ServerFetch<D extends EventDefs, P extends PresenceDefs> = {
106
+ (request: Request): Promise<Response>
107
+ (request: Request, options: ServerFetchOptions<D, P>): Promise<Response>
108
+ }
109
+
110
+ const parseHistoryBounds = (
111
+ gte: string | null,
112
+ lte: string | null,
113
+ ): { gte: number; lte: number } => {
114
+ if (gte === null || lte === null) {
115
+ throw new A2Error(
116
+ 'INVALID_PAYLOAD',
117
+ 'a history read takes both gte and lte',
118
+ )
119
+ }
120
+ const bounds = { gte: Number(gte), lte: Number(lte) }
121
+ for (const [name, value] of Object.entries(bounds)) {
122
+ if (!Number.isSafeInteger(value) || value < 0) {
123
+ throw new A2Error(
124
+ 'INVALID_PAYLOAD',
125
+ `history ${name} must be a non-negative safe integer`,
126
+ )
127
+ }
128
+ }
129
+ if (bounds.gte > bounds.lte) {
130
+ throw new A2Error(
131
+ 'INVALID_PAYLOAD',
132
+ 'history gte must be less than or equal to lte',
133
+ )
134
+ }
135
+ return bounds
136
+ }
137
+
138
+ const parseResumeIndex = (raw: string | null): number => {
139
+ if (raw === null) return 0
140
+ const index = Number(raw)
141
+ if (!Number.isSafeInteger(index) || index < 0) {
142
+ throw new A2Error(
143
+ 'INVALID_PAYLOAD',
144
+ 'stream index must be a non-negative safe integer',
145
+ )
146
+ }
147
+ return index
148
+ }
149
+
150
+ const errorResponse = (error: unknown): Response => {
151
+ const a2error = asA2Error(error)
152
+ return Response.json(errorToWire(a2error), {
153
+ status: errorStatus(a2error.code),
154
+ })
155
+ }
156
+
157
+ const forbidden = (): A2Error =>
158
+ new A2Error('FORBIDDEN', 'the operation is not authorized')
159
+
160
+ const freezePush = <D extends EventDefs, P extends PresenceDefs>(
161
+ sessionId: string,
162
+ events: ParsedPushEvent[],
163
+ presence: ParsedPushPresence | undefined,
164
+ transport: 'http' | 'websocket',
165
+ ): A2Operation<D, P> & { type: 'push' } => {
166
+ for (const event of events) Object.freeze(event)
167
+ Object.freeze(events)
168
+ if (presence !== undefined) {
169
+ Object.freeze(presence.values)
170
+ Object.freeze(presence)
171
+ }
172
+ return {
173
+ type: 'push',
174
+ sessionId,
175
+ events,
176
+ ...(presence === undefined
177
+ ? {}
178
+ : { presence: presence as A2PushPresence<P> }),
179
+ transport,
180
+ }
181
+ }
182
+
183
+ export function createServerFetch<
184
+ D extends EventDefs,
185
+ P extends PresenceDefs = Record<never, never>,
186
+ >(server: FetchServer<D, P>): ServerFetch<D, P> {
187
+ const presence = Object.keys(server.contract.presence).length > 0
188
+
189
+ const fetch = async (
190
+ request: Request,
191
+ options?: ServerFetchOptions<D, P>,
192
+ ): Promise<Response> => {
193
+ const authorized = async (
194
+ operation: A2Operation<D, P>,
195
+ ): Promise<boolean> => {
196
+ try {
197
+ return (await options?.authorize?.(Object.freeze(operation))) !== false
198
+ } catch (cause) {
199
+ throw new A2Error('STORE_UNAVAILABLE', 'authorization failed', {
200
+ cause,
201
+ })
202
+ }
203
+ }
204
+
205
+ const authorize = async (operation: A2Operation<D, P>): Promise<void> => {
206
+ if (!(await authorized(operation))) throw forbidden()
207
+ }
208
+
209
+ const validateIngress = async (
210
+ context: ServerIngressContext,
211
+ ): Promise<void> => {
212
+ await serverFetchHooks.get(fetch)?.validateIngress?.(context)
213
+ }
214
+
215
+ const attach = (socket: A2Socket): void => {
216
+ const deadline = invocationDeadlineMs()
217
+ sessionsSocket<D>(
218
+ async (sessionId, startAfter) => {
219
+ const operation: A2Operation<D, P> = {
220
+ type: 'stream',
221
+ sessionId,
222
+ startAfter,
223
+ transport: 'websocket',
224
+ }
225
+ return (await authorized(operation))
226
+ ? server.session(sessionId)
227
+ : null
228
+ },
229
+ socket,
230
+ {
231
+ presence,
232
+ ...(deadline === null ? {} : { deadline }),
233
+ gatePush: async (sessionId, events, pushedPresence) => {
234
+ try {
235
+ await authorize(
236
+ freezePush<D, P>(
237
+ sessionId,
238
+ events,
239
+ pushedPresence,
240
+ 'websocket',
241
+ ),
242
+ )
243
+ await validateIngress({
244
+ sessionId,
245
+ events,
246
+ ...(pushedPresence === undefined
247
+ ? {}
248
+ : { presence: pushedPresence }),
249
+ })
250
+ return null
251
+ } catch (error) {
252
+ return asA2Error(error)
253
+ }
254
+ },
255
+ },
256
+ )
257
+ }
258
+
259
+ try {
260
+ if (request.method === 'GET') {
261
+ if (request.headers.get('upgrade')?.toLowerCase() === 'websocket') {
262
+ if (options?.upgradeWebSocket === undefined) {
263
+ return new Response(
264
+ 'WebSocket upgrade requested, but server.fetch() has no upgrade implementation',
265
+ { status: 426 },
266
+ )
267
+ }
268
+ return await options.upgradeWebSocket(attach)
269
+ }
270
+
271
+ const { searchParams } = new URL(request.url)
272
+ const sessionId = searchParams.get('sessionId')
273
+ if (sessionId === null || sessionId.length === 0) {
274
+ throw new A2Error('INVALID_PAYLOAD', 'missing sessionId')
275
+ }
276
+ const gteRaw = searchParams.get('gte')
277
+ const lteRaw = searchParams.get('lte')
278
+ if (gteRaw !== null || lteRaw !== null) {
279
+ const bounds = parseHistoryBounds(gteRaw, lteRaw)
280
+ await authorize({
281
+ type: 'history',
282
+ sessionId,
283
+ ...bounds,
284
+ transport: 'http',
285
+ })
286
+ const events = await server.session(sessionId).history(bounds)
287
+ const covered = events.length === bounds.lte - bounds.gte + 1
288
+ return Response.json(events.map(eventToWire), {
289
+ headers: { 'a2-history-covered': String(covered) },
290
+ })
291
+ }
292
+
293
+ const startAfter = parseResumeIndex(searchParams.get('index'))
294
+ await authorize({
295
+ type: 'stream',
296
+ sessionId,
297
+ startAfter,
298
+ transport: 'http',
299
+ })
300
+ const target = server.session(sessionId) as unknown as SocketSession
301
+ return sseResponse(
302
+ presence
303
+ ? target.stream({ startAfter, presence: true })
304
+ : target.stream({ startAfter }),
305
+ )
306
+ }
307
+
308
+ if (request.method === 'POST') {
309
+ const body = await parsePushBody(request)
310
+ await authorize(
311
+ freezePush<D, P>(body.sessionId, body.events, body.presence, 'http'),
312
+ )
313
+ await validateIngress({
314
+ sessionId: body.sessionId,
315
+ events: body.events,
316
+ ...(body.presence === undefined ? {} : { presence: body.presence }),
317
+ })
318
+ const session = server.session(body.sessionId)
319
+ if (body.presence !== undefined) {
320
+ const target = session as unknown as SocketSession
321
+ if (typeof target.setPresence !== 'function') {
322
+ throw invalidPushBody('presence is not declared by this contract')
323
+ }
324
+ await target.setPresence(body.presence)
325
+ }
326
+ const appended: ContractEvent<D>[] =
327
+ body.events.length === 0
328
+ ? []
329
+ : await session.append(
330
+ ...(body.events as unknown as AppendInput<D>[]),
331
+ )
332
+ return Response.json(appended.map(eventToWire))
333
+ }
334
+
335
+ return new Response('Method Not Allowed', {
336
+ status: 405,
337
+ headers: { allow: 'GET, POST' },
338
+ })
339
+ } catch (error) {
340
+ return errorResponse(error)
341
+ }
342
+ }
343
+ return fetch
344
+ }