experimental-a2 0.10.0 → 0.12.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 (121) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/actor-DJi3RsNu.d.ts +310 -0
  3. package/dist/actor-DJi3RsNu.d.ts.map +1 -0
  4. package/dist/actor-client.d.ts +13 -4
  5. package/dist/actor-client.d.ts.map +1 -1
  6. package/dist/actor-client.js +63 -7
  7. package/dist/actor-client.js.map +1 -1
  8. package/dist/actor-react.d.ts +5 -4
  9. package/dist/actor-react.d.ts.map +1 -1
  10. package/dist/actor-react.js +16 -2
  11. package/dist/actor-react.js.map +1 -1
  12. package/dist/actor.d.ts +2 -176
  13. package/dist/actor.js +13 -2
  14. package/dist/actor.js.map +1 -1
  15. package/dist/ai-server.d.ts +1 -1
  16. package/dist/ai-server.js +2 -2
  17. package/dist/ai.d.ts +1 -1
  18. package/dist/ai.js +1 -1
  19. package/dist/client-Bf6uSEAk.js +1342 -0
  20. package/dist/client-Bf6uSEAk.js.map +1 -0
  21. package/dist/client-P_NNNRM-.d.ts +243 -0
  22. package/dist/client-P_NNNRM-.d.ts.map +1 -0
  23. package/dist/client.d.ts +2 -208
  24. package/dist/client.js +2 -1206
  25. package/dist/errors-DCk6ch5n.js.map +1 -1
  26. package/dist/errors-DvhSXnxk.d.ts +28 -0
  27. package/dist/errors-DvhSXnxk.d.ts.map +1 -0
  28. package/dist/index.d.ts +3 -35
  29. package/dist/{internal-DRXJ56EI.js → internal-Dq2qYxou.js} +2 -2
  30. package/dist/{internal-DRXJ56EI.js.map → internal-Dq2qYxou.js.map} +1 -1
  31. package/dist/platform-B4TnJtWu.js +34 -0
  32. package/dist/platform-B4TnJtWu.js.map +1 -0
  33. package/dist/react.d.ts +3 -1
  34. package/dist/react.d.ts.map +1 -1
  35. package/dist/react.js +2 -1
  36. package/dist/react.js.map +1 -1
  37. package/dist/scheduler-qstash.d.ts +2 -2
  38. package/dist/scheduler-qstash.js +3 -2
  39. package/dist/scheduler-qstash.js.map +1 -1
  40. package/dist/scheduler-vercel.d.ts +2 -2
  41. package/dist/scheduler-vercel.js +2 -2
  42. package/dist/{server-DlLyvaSH.js → server-BeNADlCI.js} +171 -65
  43. package/dist/server-BeNADlCI.js.map +1 -0
  44. package/dist/{server-DgCrSuhB.d.ts → server-DjZZa1wr.d.ts} +9 -3
  45. package/dist/{server-DgCrSuhB.d.ts.map → server-DjZZa1wr.d.ts.map} +1 -1
  46. package/dist/server.d.ts +3 -3
  47. package/dist/server.js +2 -2
  48. package/dist/{store-DGHeBtIQ.d.ts → store-DtDOWLSn.d.ts} +4 -5
  49. package/dist/{store-DGHeBtIQ.d.ts.map → store-DtDOWLSn.d.ts.map} +1 -1
  50. package/dist/store-N8PXxDAS.js.map +1 -1
  51. package/dist/store-memory.d.ts +1 -1
  52. package/dist/store-memory.js +1 -1
  53. package/dist/{store-polling-6DW7F1DT.js → store-polling-CmxUbV93.js} +56 -7
  54. package/dist/store-polling-CmxUbV93.js.map +1 -0
  55. package/dist/store-postgres.d.ts +6 -4
  56. package/dist/store-postgres.d.ts.map +1 -1
  57. package/dist/store-postgres.js +701 -53
  58. package/dist/store-postgres.js.map +1 -1
  59. package/dist/store-presence-polling-C7-XZyW9.js +94 -0
  60. package/dist/store-presence-polling-C7-XZyW9.js.map +1 -0
  61. package/dist/store-redis-http.d.ts +2 -2
  62. package/dist/store-redis-http.d.ts.map +1 -1
  63. package/dist/store-redis-http.js +184 -38
  64. package/dist/store-redis-http.js.map +1 -1
  65. package/dist/{store-redis-core-z-ykbyMg.js → store-redis-notify-BUCyXOn0.js} +491 -27
  66. package/dist/store-redis-notify-BUCyXOn0.js.map +1 -0
  67. package/dist/store-redis.d.ts +5 -13
  68. package/dist/store-redis.d.ts.map +1 -1
  69. package/dist/store-redis.js +27 -271
  70. package/dist/store-redis.js.map +1 -1
  71. package/dist/store-sqlite.d.ts +1 -1
  72. package/dist/store-sqlite.js +1 -1
  73. package/dist/{wire--yji6mO3.js → wire-BO5wWCb1.js} +18 -16
  74. package/dist/wire-BO5wWCb1.js.map +1 -0
  75. package/docs/actors/04-routes.mdx +40 -8
  76. package/docs/guides/03-react.mdx +52 -10
  77. package/docs/guides/05-production.mdx +54 -5
  78. package/docs/guides/09-presence.mdx +12 -7
  79. package/docs/guides/10-transports.mdx +15 -2
  80. package/docs/reference/01-api.mdx +124 -17
  81. package/docs/reference/02-errors.mdx +45 -11
  82. package/examples/playground/package.json +1 -1
  83. package/package.json +1 -1
  84. package/src/actor-client.ts +114 -14
  85. package/src/actor-react.ts +18 -3
  86. package/src/actor.ts +16 -1
  87. package/src/client-errors.ts +51 -0
  88. package/src/client.ts +253 -96
  89. package/src/errors.ts +4 -9
  90. package/src/index.ts +1 -1
  91. package/src/internal.ts +1 -1
  92. package/src/postgres-notification-scope.ts +134 -0
  93. package/src/postgres-notifications.ts +244 -0
  94. package/src/postgres-pool.ts +65 -0
  95. package/src/postgres-resources.ts +185 -0
  96. package/src/presence-recovery.ts +120 -0
  97. package/src/react.ts +3 -0
  98. package/src/redis-http-subscriptions.ts +199 -0
  99. package/src/server.ts +160 -22
  100. package/src/session-socket.ts +25 -7
  101. package/src/sse.ts +17 -0
  102. package/src/store-polling.ts +32 -12
  103. package/src/store-postgres.ts +292 -75
  104. package/src/store-presence-polling.ts +125 -0
  105. package/src/store-redis-core.ts +37 -30
  106. package/src/store-redis-http.ts +51 -45
  107. package/src/store-redis-notify.ts +464 -0
  108. package/src/store-redis.ts +9 -364
  109. package/src/store.ts +3 -4
  110. package/src/stream-activity.ts +32 -0
  111. package/src/wire.ts +39 -15
  112. package/dist/actor-shared-USo5MyuF.d.ts +0 -136
  113. package/dist/actor-shared-USo5MyuF.d.ts.map +0 -1
  114. package/dist/actor.d.ts.map +0 -1
  115. package/dist/client.d.ts.map +0 -1
  116. package/dist/client.js.map +0 -1
  117. package/dist/index.d.ts.map +0 -1
  118. package/dist/server-DlLyvaSH.js.map +0 -1
  119. package/dist/store-polling-6DW7F1DT.js.map +0 -1
  120. package/dist/store-redis-core-z-ykbyMg.js.map +0 -1
  121. package/dist/wire--yji6mO3.js.map +0 -1
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { A2Error } from './errors.ts'
10
10
  import { SOCKET_TIMINGS, STREAM_TIMINGS } from './internal.ts'
11
+ import { streamActivities } from './stream-activity.ts'
11
12
  import type { Event } from './store.ts'
12
13
  import type { EventDefs, PresencePatch, PresenceSnapshot } from './contract.ts'
13
14
  import type { ParsedPushEvent, ParsedPushPresence } from './push-envelope.ts'
@@ -312,6 +313,7 @@ const handleSocketPush = async (
312
313
  req: number,
313
314
  ackSessionId: string | undefined,
314
315
  gatePush: SessionsSocketOptions['gatePush'],
316
+ activate: () => void,
315
317
  ): Promise<void> => {
316
318
  try {
317
319
  const events = parsePushEvents(frame['events'])
@@ -320,8 +322,10 @@ const handleSocketPush = async (
320
322
  shell.send(socketErrorAckFor(req, denial, ackSessionId))
321
323
  return
322
324
  }
325
+ activate()
323
326
  // oxlint-disable-next-line a2/await-handler-append -- this transport append is awaited before its ack
324
327
  const appended = await target.append(...events)
328
+ activate()
325
329
  shell.send(socketAckFor(req, appended, ackSessionId))
326
330
  } catch (error) {
327
331
  shell.send(socketErrorAckFor(req, asA2Error(error), ackSessionId))
@@ -418,6 +422,7 @@ export function sessionSocket<D extends EventDefs>(
418
422
  req,
419
423
  undefined,
420
424
  options?.gatePush,
425
+ () => streamActivities.get(subscription.iterator)?.activate(),
421
426
  ),
422
427
  )
423
428
  return
@@ -511,10 +516,10 @@ export function sessionsSocket<D extends EventDefs>(
511
516
  if (shell.torn() || subscription.stopped) return
512
517
  retire(id, subscription)
513
518
  shell.send(socketUnsubscribedFor(id))
514
- } catch {
519
+ } catch (error) {
515
520
  if (shell.torn() || subscription.stopped) return
516
521
  retire(id, subscription)
517
- shell.send(socketUnsubscribedFor(id, 'stream failed'))
522
+ shell.send(socketUnsubscribedFor(id, 'stream failed', asA2Error(error)))
518
523
  }
519
524
  }
520
525
 
@@ -522,23 +527,32 @@ export function sessionsSocket<D extends EventDefs>(
522
527
  id: string,
523
528
  index: number,
524
529
  ): Promise<void> => {
530
+ const existing = subscriptions.get(id)
531
+ if (existing) {
532
+ stopSubscription(existing)
533
+ subscriptions.delete(id)
534
+ }
525
535
  let session: Pick<Session<D>, 'stream'> | null
526
536
  try {
527
537
  session = await resolve(id, index)
528
- } catch {
538
+ } catch (error) {
529
539
  if (!shell.torn()) {
530
- shell.send(socketUnsubscribedFor(id, 'stream failed'))
540
+ shell.send(socketUnsubscribedFor(id, 'stream failed', asA2Error(error)))
531
541
  }
532
542
  return
533
543
  }
534
544
  if (shell.torn()) return
535
545
  if (session === null) {
536
- shell.send(socketUnsubscribedFor(id, 'subscribe rejected'))
546
+ shell.send(
547
+ socketUnsubscribedFor(
548
+ id,
549
+ 'subscribe rejected',
550
+ new A2Error('FORBIDDEN', 'the subscription is not authorized'),
551
+ ),
552
+ )
537
553
  return
538
554
  }
539
555
  const target = session as unknown as SocketSession
540
- const existing = subscriptions.get(id)
541
- if (existing) stopSubscription(existing)
542
556
  const subscription = socketSubscription(target, index, presence)
543
557
  subscriptions.set(id, subscription)
544
558
  shell.send(socketSubscribedFor(id))
@@ -582,6 +596,10 @@ export function sessionsSocket<D extends EventDefs>(
582
596
  req,
583
597
  sessionId,
584
598
  options?.gatePush,
599
+ () => {
600
+ const current = subscriptions.get(sessionId)
601
+ if (current) streamActivities.get(current.iterator)?.activate()
602
+ },
585
603
  )
586
604
  }
587
605
 
package/src/sse.ts CHANGED
@@ -10,6 +10,7 @@ import { STREAM_TIMINGS } from './internal.ts'
10
10
  import type { Event } from './store.ts'
11
11
  import type { PresencePatch, PresenceSnapshot } from './contract.ts'
12
12
  import { invocationDeadlineMs } from './platform.ts'
13
+ import { streamActivities } from './stream-activity.ts'
13
14
  import {
14
15
  eventToWire,
15
16
  presencePatchToWire,
@@ -41,7 +42,10 @@ export function sseResponse(
41
42
  let deadlineTimer: ReturnType<typeof setTimeout> | undefined
42
43
  let closed = false
43
44
  let iteratorReturned = false
45
+ let stopActivity: (() => void) | undefined
44
46
  const stopTimers = (): void => {
47
+ stopActivity?.()
48
+ stopActivity = undefined
45
49
  if (heartbeat !== undefined) clearInterval(heartbeat)
46
50
  if (deadlineTimer !== undefined) clearTimeout(deadlineTimer)
47
51
  heartbeat = undefined
@@ -61,6 +65,19 @@ export function sseResponse(
61
65
  // early proof this is a stream. Comment frames are invisible to
62
66
  // SSE consumers.
63
67
  controller.enqueue(encoder.encode(': connected\n\n'))
68
+ const activity = streamActivities.get(iterator)
69
+ if (activity) {
70
+ const announce = (idle: boolean): void => {
71
+ if (closed) return
72
+ controller.enqueue(
73
+ encoder.encode(
74
+ `event: a2-activity\ndata: ${idle ? 'idle' : 'active'}\n\n`,
75
+ ),
76
+ )
77
+ }
78
+ stopActivity = activity.subscribe(announce)
79
+ if (activity.idle) announce(true)
80
+ }
64
81
  // Heartbeat: comment frames are ignored by SSE parsers but are
65
82
  // bytes on the wire — liveness proof for the client's watchdog
66
83
  // and for idle-timeout-happy intermediaries.
@@ -14,7 +14,8 @@
14
14
  */
15
15
 
16
16
  import { POLL_TIMINGS } from './internal.ts'
17
- import type { StoredEvent } from './store.ts'
17
+ import type { Event } from './store.ts'
18
+ import { createStreamActivity, streamActivities } from './stream-activity.ts'
18
19
 
19
20
  /** One cancellable sleep — the injection seam for cadence tests. */
20
21
  export type Sleeper = { promise: Promise<void>; cancel: () => void }
@@ -56,47 +57,66 @@ export type PollingStreamOptions = {
56
57
  * SSE client), waking any pending sleep immediately.
57
58
  */
58
59
  export function pollingStream(
59
- readAfter: (afterIndex: number) => StoredEvent[] | Promise<StoredEvent[]>,
60
+ readAfter: (afterIndex: number) => Event[] | Promise<Event[]>,
60
61
  options: PollingStreamOptions,
61
- ): AsyncIterable<StoredEvent> {
62
+ ): AsyncIterable<Event> {
62
63
  const startAfter = options.startAfter ?? 0
63
64
  const sleep = options.sleep ?? defaultSleep
64
65
 
65
66
  return {
66
- [Symbol.asyncIterator](): AsyncIterator<StoredEvent> {
67
+ [Symbol.asyncIterator](): AsyncIterator<Event> {
67
68
  let last = startAfter
68
- let buffer: StoredEvent[] = []
69
+ let buffer: Event[] = []
69
70
  let closed = false
70
71
  let pending: Sleeper | null = null
71
72
  let interval = floor()
73
+ let activityVersion = 0
74
+ const activity = createStreamActivity(() => {
75
+ if (closed) return
76
+ activityVersion += 1
77
+ interval = floor()
78
+ pending?.cancel()
79
+ })
72
80
 
73
- return {
74
- async next(): Promise<IteratorResult<StoredEvent>> {
81
+ const iterator: AsyncIterator<Event> = {
82
+ async next(): Promise<IteratorResult<Event>> {
75
83
  for (;;) {
76
84
  if (closed) return { value: undefined, done: true }
77
- // oxlint-disable-next-line no-await-in-loop -- poll loop
78
- if (buffer.length === 0) buffer = await readAfter(last)
85
+ try {
86
+ // oxlint-disable-next-line no-await-in-loop -- poll loop
87
+ if (buffer.length === 0) buffer = await readAfter(last)
88
+ } catch (error) {
89
+ if (closed) return { value: undefined, done: true }
90
+ throw error
91
+ }
92
+ if (closed) return { value: undefined, done: true }
79
93
  const row = buffer.shift()
80
94
  if (row) {
81
95
  // Activity: tighten back to the floor.
82
- interval = floor()
96
+ activity.activate()
83
97
  last = row.index
84
98
  return { value: row, done: false }
85
99
  }
86
100
  // Empty poll: wait, then back off toward the idle ceiling.
101
+ activity.setIdle(interval === ceiling())
102
+ const version = activityVersion
87
103
  pending = sleep(interval)
88
104
  // oxlint-disable-next-line no-await-in-loop -- poll loop
89
105
  await pending.promise
90
106
  pending = null
91
- interval = Math.min(interval * 2, ceiling())
107
+ if (version === activityVersion) {
108
+ interval = Math.min(interval * 2, ceiling())
109
+ }
92
110
  }
93
111
  },
94
- async return(): Promise<IteratorResult<StoredEvent>> {
112
+ async return(): Promise<IteratorResult<Event>> {
95
113
  closed = true
96
114
  pending?.cancel()
97
115
  return { value: undefined, done: true }
98
116
  },
99
117
  }
118
+ streamActivities.set(iterator, activity)
119
+ return iterator
100
120
  },
101
121
  }
102
122
  }