experimental-a2 0.6.0 → 0.8.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 +41 -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 +20 -17
  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 +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
  27. package/dist/server-B2XNevQA.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
  29. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
  34. package/dist/store-RJO35BMj.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +80 -78
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.d.ts.map +1 -1
  43. package/dist/store-postgres.js +231 -182
  44. package/dist/store-postgres.js.map +1 -1
  45. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
  46. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  47. package/dist/store-redis-http.d.ts +1 -1
  48. package/dist/store-redis-http.js +3 -4
  49. package/dist/store-redis-http.js.map +1 -1
  50. package/dist/store-redis.d.ts +1 -1
  51. package/dist/store-redis.js +4 -5
  52. package/dist/store-redis.js.map +1 -1
  53. package/dist/store-sqlite.d.ts +1 -1
  54. package/dist/store-sqlite.d.ts.map +1 -1
  55. package/dist/store-sqlite.js +104 -91
  56. package/dist/store-sqlite.js.map +1 -1
  57. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  58. package/dist/wire-B6te_wns.js.map +1 -0
  59. package/docs/concepts/02-handlers.mdx +4 -0
  60. package/docs/concepts/04-state.mdx +57 -9
  61. package/docs/guides/03-react.mdx +20 -28
  62. package/docs/guides/05-production.mdx +9 -11
  63. package/docs/guides/06-ai-agents.mdx +10 -15
  64. package/docs/guides/09-presence.mdx +14 -19
  65. package/docs/guides/10-transports.mdx +104 -86
  66. package/docs/reference/01-api.mdx +182 -293
  67. package/docs/reference/02-errors.mdx +5 -7
  68. package/package.json +1 -14
  69. package/src/ai-server.ts +36 -15
  70. package/src/client.ts +2 -2
  71. package/src/errors.ts +1 -0
  72. package/src/internal.ts +3 -62
  73. package/src/push-envelope.ts +24 -21
  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 +315 -312
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +138 -101
  81. package/src/store-postgres.ts +355 -238
  82. package/src/store-redis-core.ts +247 -237
  83. package/src/store-redis-http.ts +1 -2
  84. package/src/store-redis.ts +1 -2
  85. package/src/store-sqlite.ts +191 -153
  86. package/src/store.ts +24 -66
  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-CdMqi7mC.d.ts +0 -202
  99. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  100. package/dist/client-Dj5d3SP_.js +0 -1026
  101. package/dist/client-Dj5d3SP_.js.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
@@ -95,14 +95,13 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
95
95
  completeAttempt: core.completeAttempt,
96
96
  failAttempt: core.failAttempt,
97
97
  readState: core.readState,
98
- putSnapshot: core.putSnapshot,
98
+ putSnapshots: core.putSnapshots,
99
99
  presence: {
100
100
  async set(ns, participant, values, meta) {
101
101
  await core.presence.set(ns, participant, values, meta)
102
102
  },
103
103
  read: core.presence.read,
104
104
  },
105
- inspect: core.inspect,
106
105
 
107
106
  stream(sessionId, opts) {
108
107
  return pollingStream(
@@ -291,8 +291,7 @@ export function redis(options: RedisStoreOptions = {}): RedisStore {
291
291
  completeAttempt: core.completeAttempt,
292
292
  failAttempt: core.failAttempt,
293
293
  readState: core.readState,
294
- putSnapshot: core.putSnapshot,
295
- inspect: core.inspect,
294
+ putSnapshots: core.putSnapshots,
296
295
 
297
296
  presence: {
298
297
  async set(ns, participant, values, meta) {
@@ -92,6 +92,26 @@ create table if not exists a2_snapshots (
92
92
  primary key (session_id, reducer_name)
93
93
  ) strict;
94
94
 
95
+ create table if not exists a2_snapshot_history (
96
+ session_id text not null,
97
+ reducer_name text not null,
98
+ up_to_index integer not null,
99
+ state text not null,
100
+ updated_at integer not null,
101
+ primary key (session_id, reducer_name, up_to_index)
102
+ ) strict;
103
+
104
+ create table if not exists a2_snapshot_pins (
105
+ session_id text not null,
106
+ event_index integer not null,
107
+ reducer_name text not null,
108
+ up_to_index integer not null,
109
+ primary key (session_id, event_index, reducer_name, up_to_index)
110
+ ) strict;
111
+
112
+ create index if not exists a2_snapshot_pins_checkpoint
113
+ on a2_snapshot_pins (session_id, reducer_name, up_to_index);
114
+
95
115
  create table if not exists a2_presence (
96
116
  ns text not null,
97
117
  participant text not null,
@@ -231,23 +251,6 @@ type EventRow = {
231
251
  claim_expires_at: number | bigint | null
232
252
  }
233
253
 
234
- type SessionSummaryRow = {
235
- session_id: string
236
- event_count: number | bigint
237
- pending_count: number | bigint
238
- failed_count: number | bigint
239
- attempt_count: number | bigint
240
- failure_count: number | bigint
241
- first_event_at: number | bigint
242
- updated_at: number | bigint
243
- }
244
-
245
- type SnapshotRow = {
246
- reducer_name: string
247
- up_to_index: number | bigint
248
- updated_at: number | bigint
249
- }
250
-
251
254
  type PresenceDbRow = {
252
255
  participant: string
253
256
  field: string
@@ -259,10 +262,17 @@ type PresenceDbRow = {
259
262
 
260
263
  type StateReadRow = Partial<EventRow> & {
261
264
  row_kind: 'snapshot' | 'event'
265
+ head_index: number | bigint | null
262
266
  snapshot_index: number | bigint | null
263
267
  snapshot_state: string | null
264
268
  }
265
269
 
270
+ type SnapshotHeadRow = {
271
+ up_to_index: number | bigint
272
+ state: string
273
+ updated_at: number | bigint
274
+ }
275
+
266
276
  type AppendStatusRow = {
267
277
  max: number | bigint
268
278
  has_pending: number | bigint
@@ -764,6 +774,19 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
764
774
  claim_expires_at = null
765
775
  where session_id = ? and idx = ?`,
766
776
  ).run(now, attempt, JSON.stringify(ids), sessionId, index)
777
+ db.prepare(
778
+ 'delete from a2_snapshot_pins where session_id = ? and event_index = ?',
779
+ ).run(sessionId, index)
780
+ db.prepare(
781
+ `delete from a2_snapshot_history
782
+ where session_id = ?
783
+ and not exists (
784
+ select 1 from a2_snapshot_pins pin
785
+ where pin.session_id = a2_snapshot_history.session_id
786
+ and pin.reducer_name = a2_snapshot_history.reducer_name
787
+ and pin.up_to_index = a2_snapshot_history.up_to_index
788
+ )`,
789
+ ).run(sessionId)
767
790
  if (parent.lane !== null) {
768
791
  promoteLaneHead.run(sessionId, sessionId, parent.lane)
769
792
  }
@@ -852,6 +875,21 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
852
875
  sessionId,
853
876
  index,
854
877
  )
878
+ if (deadLettered) {
879
+ db.prepare(
880
+ 'delete from a2_snapshot_pins where session_id = ? and event_index = ?',
881
+ ).run(sessionId, index)
882
+ db.prepare(
883
+ `delete from a2_snapshot_history
884
+ where session_id = ?
885
+ and not exists (
886
+ select 1 from a2_snapshot_pins pin
887
+ where pin.session_id = a2_snapshot_history.session_id
888
+ and pin.reducer_name = a2_snapshot_history.reducer_name
889
+ and pin.up_to_index = a2_snapshot_history.up_to_index
890
+ )`,
891
+ ).run(sessionId)
892
+ }
855
893
  return {
856
894
  outcome: deadLettered ? 'dead_lettered' : 'failed',
857
895
  failureCount: nextFailureCount,
@@ -860,18 +898,32 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
860
898
  )
861
899
  },
862
900
 
863
- async readState(sessionId, reducerName) {
901
+ async readState(sessionId, reducerName, stateOptions) {
864
902
  return wrap(() => {
903
+ const throughIndex = stateOptions?.throughIndex ?? null
904
+ const snapshotThroughIndex =
905
+ stateOptions?.snapshotThroughIndex ?? throughIndex
865
906
  const rows = db
866
907
  .prepare(
867
- `with snapshot as materialized (
868
- select up_to_index, state
908
+ `with head as materialized (
909
+ select up_to_index, state, updated_at
869
910
  from a2_snapshots
870
- where session_id = ? and reducer_name = ?
911
+ where session_id = ? and reducer_name = ?
912
+ ), snapshot as materialized (
913
+ select up_to_index, state from (
914
+ select up_to_index, state from head
915
+ where (? is null or up_to_index <= ?)
916
+ union all
917
+ select up_to_index, state
918
+ from a2_snapshot_history
919
+ where session_id = ? and reducer_name = ?
920
+ and (? is null or up_to_index <= ?)
921
+ ) order by up_to_index desc limit 1
871
922
  )
872
923
  select 0 as row_order, 'snapshot' as row_kind,
873
- snapshot.up_to_index as snapshot_index,
874
- snapshot.state as snapshot_state,
924
+ (select up_to_index from head) as head_index,
925
+ (select up_to_index from snapshot) as snapshot_index,
926
+ (select state from snapshot) as snapshot_state,
875
927
  null as session_id, null as idx, null as event_type,
876
928
  null as payload, null as event_id, null as created_at,
877
929
  null as cause, null as lane, null as lane_ready,
@@ -883,25 +935,44 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
883
935
  null as last_failed_attempt, null as last_error,
884
936
  null as failed_at, null as claim_holder,
885
937
  null as claim_expires_at
886
- from snapshot
887
938
  union all
888
939
  select 1 as row_order, 'event' as row_kind,
940
+ (select up_to_index from head) as head_index,
889
941
  null as snapshot_index, null as snapshot_state,
890
942
  event.*
891
943
  from a2_events as event
892
944
  where event.session_id = ?
893
945
  and event.idx > coalesce((select up_to_index from snapshot), 0)
946
+ and (? is null or event.idx <= ?)
894
947
  order by row_order, idx`,
895
948
  )
896
- .all(sessionId, reducerName, sessionId) as unknown as StateReadRow[]
949
+ .all(
950
+ sessionId,
951
+ reducerName,
952
+ snapshotThroughIndex,
953
+ snapshotThroughIndex,
954
+ sessionId,
955
+ reducerName,
956
+ snapshotThroughIndex,
957
+ snapshotThroughIndex,
958
+ sessionId,
959
+ throughIndex,
960
+ throughIndex,
961
+ ) as unknown as StateReadRow[]
897
962
  const snapshot = rows.find((row) => row.row_kind === 'snapshot')
898
963
  return {
899
- snapshot: snapshot
900
- ? {
901
- index: Number(snapshot.snapshot_index),
902
- state: JSON.parse(snapshot.snapshot_state!),
903
- }
904
- : null,
964
+ headIndex:
965
+ snapshot?.head_index === null || snapshot?.head_index === undefined
966
+ ? null
967
+ : Number(snapshot.head_index),
968
+ snapshot:
969
+ snapshot?.snapshot_index !== null &&
970
+ snapshot?.snapshot_index !== undefined
971
+ ? {
972
+ index: Number(snapshot.snapshot_index),
973
+ state: JSON.parse(snapshot.snapshot_state!),
974
+ }
975
+ : null,
905
976
  events: rows
906
977
  .filter(
907
978
  (row): row is StateReadRow & EventRow => row.row_kind === 'event',
@@ -911,26 +982,95 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
911
982
  })
912
983
  },
913
984
 
914
- async putSnapshot(sessionId, reducerName, index, state) {
915
- wrap(() => {
916
- // Guarded upsert: a slower concurrent writer must never clobber
917
- // a further-along snapshot (spec §3).
918
- db.prepare(
919
- `insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)
920
- values (?, ?, ?, ?, ?)
921
- on conflict (session_id, reducer_name) do update set
922
- up_to_index = excluded.up_to_index,
923
- state = excluded.state,
924
- updated_at = excluded.updated_at
925
- where excluded.up_to_index > a2_snapshots.up_to_index`,
926
- ).run(
927
- sessionId,
928
- reducerName,
929
- index,
930
- JSON.stringify(state) ?? 'null',
931
- clock.now().getTime(),
932
- )
933
- })
985
+ async putSnapshots(sessionId, reducerName, writes) {
986
+ await wrap(() =>
987
+ tx(() => {
988
+ const selectHead = db.prepare(
989
+ `select up_to_index, state, updated_at from a2_snapshots
990
+ where session_id = ? and reducer_name = ?`,
991
+ )
992
+ const unfinished = db.prepare(
993
+ `select 1 from a2_events
994
+ where session_id = ? and idx = ?
995
+ and processed_at is null and failed_at is null`,
996
+ )
997
+ const insertPin = db.prepare(
998
+ `insert or ignore into a2_snapshot_pins
999
+ (session_id, event_index, reducer_name, up_to_index)
1000
+ values (?, ?, ?, ?)`,
1001
+ )
1002
+ const hasPin = db.prepare(
1003
+ `select 1 from a2_snapshot_pins
1004
+ where session_id = ? and reducer_name = ? and up_to_index = ?
1005
+ limit 1`,
1006
+ )
1007
+ const putHistory = db.prepare(
1008
+ `insert into a2_snapshot_history
1009
+ (session_id, reducer_name, up_to_index, state, updated_at)
1010
+ values (?, ?, ?, ?, ?)
1011
+ on conflict (session_id, reducer_name, up_to_index) do update set
1012
+ state = excluded.state, updated_at = excluded.updated_at`,
1013
+ )
1014
+ const putHead = db.prepare(
1015
+ `insert into a2_snapshots
1016
+ (session_id, reducer_name, up_to_index, state, updated_at)
1017
+ values (?, ?, ?, ?, ?)
1018
+ on conflict (session_id, reducer_name) do update set
1019
+ up_to_index = excluded.up_to_index,
1020
+ state = excluded.state,
1021
+ updated_at = excluded.updated_at
1022
+ where excluded.up_to_index >= a2_snapshots.up_to_index`,
1023
+ )
1024
+ const deleteHistory = db.prepare(
1025
+ `delete from a2_snapshot_history
1026
+ where session_id = ? and reducer_name = ? and up_to_index = ?`,
1027
+ )
1028
+ for (const write of writes.toSorted((a, b) => a.index - b.index)) {
1029
+ for (const eventIndex of write.pinEventIndexes ?? []) {
1030
+ if (unfinished.get(sessionId, eventIndex)) {
1031
+ insertPin.run(sessionId, eventIndex, reducerName, write.index)
1032
+ }
1033
+ }
1034
+ const head = selectHead.get(sessionId, reducerName) as
1035
+ SnapshotHeadRow | undefined
1036
+ const now = clock.now().getTime()
1037
+ if (!head || write.index >= Number(head.up_to_index)) {
1038
+ if (
1039
+ head &&
1040
+ write.index > Number(head.up_to_index) &&
1041
+ hasPin.get(sessionId, reducerName, Number(head.up_to_index))
1042
+ ) {
1043
+ putHistory.run(
1044
+ sessionId,
1045
+ reducerName,
1046
+ Number(head.up_to_index),
1047
+ head.state,
1048
+ Number(head.updated_at),
1049
+ )
1050
+ }
1051
+ putHead.run(
1052
+ sessionId,
1053
+ reducerName,
1054
+ write.index,
1055
+ JSON.stringify(write.state) ?? 'null',
1056
+ now,
1057
+ )
1058
+ deleteHistory.run(sessionId, reducerName, write.index)
1059
+ } else if (
1060
+ write.index < Number(head.up_to_index) &&
1061
+ hasPin.get(sessionId, reducerName, write.index)
1062
+ ) {
1063
+ putHistory.run(
1064
+ sessionId,
1065
+ reducerName,
1066
+ write.index,
1067
+ JSON.stringify(write.state) ?? 'null',
1068
+ now,
1069
+ )
1070
+ }
1071
+ }
1072
+ }),
1073
+ )
934
1074
  },
935
1075
 
936
1076
  presence: {
@@ -983,108 +1123,6 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
983
1123
  },
984
1124
  },
985
1125
 
986
- inspect: {
987
- async listSessions(inspectionOptions) {
988
- return wrap(() => {
989
- const conditions = ['substr(session_id, 1, ?) = ?']
990
- const params: (string | number)[] = [
991
- inspectionOptions.prefix.length,
992
- inspectionOptions.prefix,
993
- ]
994
- if (inspectionOptions.cursor !== undefined) {
995
- conditions.push('session_id > ?')
996
- params.push(inspectionOptions.cursor)
997
- }
998
- params.push(inspectionOptions.limit + 1)
999
- const rows = db
1000
- .prepare(
1001
- `select
1002
- session_id,
1003
- count(*) as event_count,
1004
- sum(case when processed_at is null and failed_at is null then 1 else 0 end) as pending_count,
1005
- sum(case when failed_at is not null then 1 else 0 end) as failed_count,
1006
- sum(attempt_count) as attempt_count,
1007
- sum(failure_count) as failure_count,
1008
- min(created_at) as first_event_at,
1009
- max(max(
1010
- created_at,
1011
- coalesce(first_claimed_at, 0),
1012
- coalesce(last_claimed_at, 0),
1013
- coalesce(last_failed_at, 0),
1014
- coalesce(processed_at, 0),
1015
- coalesce(failed_at, 0)
1016
- )) as updated_at
1017
- from a2_events
1018
- where ${conditions.join(' and ')}
1019
- group by session_id
1020
- order by session_id
1021
- limit ?`,
1022
- )
1023
- .all(...params) as unknown as SessionSummaryRow[]
1024
- const hasMore = rows.length > inspectionOptions.limit
1025
- const visible = rows.slice(0, inspectionOptions.limit)
1026
- return {
1027
- sessions: visible.map((row) => ({
1028
- sessionId: row.session_id,
1029
- eventCount: Number(row.event_count),
1030
- pendingCount: Number(row.pending_count),
1031
- failedCount: Number(row.failed_count),
1032
- attemptCount: Number(row.attempt_count),
1033
- failureCount: Number(row.failure_count),
1034
- firstEventAt: new Date(Number(row.first_event_at)),
1035
- updatedAt: new Date(Number(row.updated_at)),
1036
- })),
1037
- cursor: hasMore ? (visible.at(-1)?.session_id ?? null) : null,
1038
- }
1039
- })
1040
- },
1041
- async readEvents(sessionId, pageOptions) {
1042
- return wrap(() => {
1043
- const currentFrontier = Number(
1044
- (
1045
- db
1046
- .prepare(
1047
- 'select coalesce(max(idx), 0) as frontier from a2_events where session_id = ?',
1048
- )
1049
- .get(sessionId) as { frontier: number }
1050
- ).frontier,
1051
- )
1052
- const throughIndex = Math.min(
1053
- pageOptions.throughIndex ?? currentFrontier,
1054
- currentFrontier,
1055
- )
1056
- const rows = db
1057
- .prepare(
1058
- `select * from a2_events
1059
- where session_id = ? and idx > ? and idx <= ?
1060
- order by idx limit ?`,
1061
- )
1062
- .all(
1063
- sessionId,
1064
- pageOptions.afterIndex,
1065
- throughIndex,
1066
- pageOptions.limit,
1067
- ) as unknown as EventRow[]
1068
- return { events: rows.map(toStored), throughIndex }
1069
- })
1070
- },
1071
- async listSnapshots(sessionId) {
1072
- return wrap(() => {
1073
- const rows = db
1074
- .prepare(
1075
- `select reducer_name, up_to_index, updated_at
1076
- from a2_snapshots where session_id = ? order by reducer_name`,
1077
- )
1078
- .all(sessionId) as unknown as SnapshotRow[]
1079
- return rows.map((row) => ({
1080
- reducerName: row.reducer_name,
1081
- index: Number(row.up_to_index),
1082
- updatedAt: new Date(Number(row.updated_at)),
1083
- }))
1084
- })
1085
- },
1086
- },
1087
-
1088
1126
  stream(sessionId, opts) {
1089
1127
  const readAfter = (afterIndex: number): StoredEvent[] =>
1090
1128
  wrap(() => {
package/src/store.ts CHANGED
@@ -67,20 +67,6 @@ export type StoredEvent = Event & {
67
67
  failedAt: Date | null
68
68
  }
69
69
 
70
- /** Durable, read-only session metadata for administrative inspection. */
71
- export type StoredSessionSummary = {
72
- sessionId: string
73
- eventCount: number
74
- pendingCount: number
75
- failedCount: number
76
- /** Durable dispatch claims across every event in the session. */
77
- attemptCount: number
78
- /** Caught handler failures across every event in the session. */
79
- failureCount: number
80
- firstEventAt: Date
81
- updatedAt: Date
82
- }
83
-
84
70
  /**
85
71
  * One live presence value, as `A2Store.presence.read` returns it. The
86
72
  * row is the LWW unit — one participant's one field. `at` is the
@@ -97,57 +83,28 @@ export type PresenceRow = {
97
83
  expiresAt: Date
98
84
  }
99
85
 
100
- /** Metadata for the latest cached fold of one reducer. */
101
- export type StoredSnapshot = {
102
- reducerName: string
103
- index: number
104
- updatedAt: Date
105
- }
106
-
107
86
  /** One consistent cache-plus-tail read for a reducer fold. */
108
87
  export type StoreStateRead = {
109
- /** The latest cached fold for this reducer, if one exists. */
88
+ /** The current head checkpoint, even when an older historical snapshot is selected. */
89
+ headIndex: number | null
90
+ /** The greatest cached fold at or before the requested snapshot frontier. */
110
91
  snapshot: { index: number; state: unknown } | null
111
92
  /** Immutable events strictly after `snapshot.index`, or the full log on a miss. */
112
93
  events: Event[]
113
94
  }
114
95
 
115
- export type StoredSessionPage = {
116
- sessions: StoredSessionSummary[]
117
- /** Opaque backend cursor; null means there is no next page. */
118
- cursor: string | null
119
- }
120
-
121
- export type StoredEventPage = {
122
- events: StoredEvent[]
123
- /** Fixed inclusive log frontier shared by every page in one inspection. */
124
- throughIndex: number
96
+ export type StoreSnapshotWrite = {
97
+ index: number
98
+ state: unknown
99
+ /** Unfinished trigger events that durably retain this exact checkpoint. */
100
+ pinEventIndexes?: readonly number[]
125
101
  }
126
102
 
127
- /**
128
- * Optional read-only administration implemented by A2's shipped stores.
129
- * It is separate from the correctness contract so custom stores do not
130
- * need to expose storage-wide discovery merely to process events.
131
- */
132
- export type A2StoreInspection = {
133
- listSessions(options: {
134
- /** Storage namespace prefix, including the contract separator. */
135
- prefix: string
136
- cursor?: string
137
- limit: number
138
- }): Promise<StoredSessionPage>
139
- /**
140
- * A bounded event page for read-only administration. The first page captures
141
- * `throughIndex`; callers pass it back so concurrent appends cannot extend a
142
- * walk indefinitely. Optional for custom stores; A2 falls back to `read()`.
143
- */
144
- readEvents?(
145
- sessionId: string,
146
- options: { afterIndex: number; throughIndex?: number; limit: number },
147
- ): Promise<StoredEventPage>
148
- listSnapshots(sessionId: string): Promise<StoredSnapshot[]>
103
+ export type StoreStateReadRequest = {
104
+ sessionId: string
105
+ throughIndex?: number
106
+ snapshotThroughIndex?: number
149
107
  }
150
-
151
108
  /** The result of atomically claiming every currently eligible event. */
152
109
  export type StoreClaimAvailableResult =
153
110
  | { outcome: 'claimed'; events: StoredEvent[] }
@@ -308,7 +265,11 @@ export interface A2Store {
308
265
  * log. The snapshot is untrusted; core may reject it and issue a full
309
266
  * `read()` when its state schema no longer accepts the cached value.
310
267
  */
311
- readState(sessionId: string, reducerName: string): Promise<StoreStateRead>
268
+ readState(
269
+ sessionId: string,
270
+ reducerName: string,
271
+ options?: { throughIndex?: number; snapshotThroughIndex?: number },
272
+ ): Promise<StoreStateRead>
312
273
 
313
274
  /**
314
275
  * Optional batched `readState` — one consistent snapshot-plus-tail read
@@ -318,20 +279,20 @@ export interface A2Store {
318
279
  * `readState` calls when absent.
319
280
  */
320
281
  readStates?(
321
- sessionIds: string[],
282
+ requests: readonly StoreStateReadRequest[],
322
283
  reducerName: string,
323
284
  ): Promise<StoreStateRead[]>
324
285
 
325
286
  /**
326
- * Writes a disposable reducer cache. Guard this operation so a slower
327
- * concurrent writer can never clobber a further-along snapshot
328
- * (`where up_to_index < excluded.up_to_index`).
287
+ * Atomically writes disposable reducer checkpoints. The greatest index
288
+ * advances the head cache. Older checkpoints survive only when at least one
289
+ * listed trigger event is still unfinished; completion and dead-lettering
290
+ * release that event's pins and collect unreferenced historical checkpoints.
329
291
  */
330
- putSnapshot(
292
+ putSnapshots(
331
293
  sessionId: string,
332
294
  reducerName: string,
333
- index: number,
334
- state: unknown,
295
+ snapshots: readonly StoreSnapshotWrite[],
335
296
  ): Promise<void>
336
297
 
337
298
  /**
@@ -368,9 +329,6 @@ export interface A2Store {
368
329
  subscribe?(ns: string, onPatch: (patch: PresencePatch) => void): () => void
369
330
  }
370
331
 
371
- /** Read-only operational data used by `experimental-a2/devtools/server`. */
372
- inspect?: A2StoreInspection
373
-
374
332
  /**
375
333
  * A live feed of one session's events, starting after `startAfter`
376
334
  * (exclusive). Transport is the backend's choice — in-process pub/sub,
package/src/wire.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The wire format shared by experimental-a2/http (server side) and experimental-a2/client
2
+ * The wire format shared by server.fetch and experimental-a2/client
3
3
  * (browser side): events as JSON with ISO timestamps, and the A2Error
4
4
  * envelope. Internal module — both entry points re-export what's
5
5
  * public.
@@ -176,6 +176,7 @@ export type WireError = {
176
176
 
177
177
  const ERROR_STATUS: Record<A2ErrorCode, number> = {
178
178
  INVALID_PAYLOAD: 400,
179
+ FORBIDDEN: 403,
179
180
  UNKNOWN_EVENT_TYPE: 400,
180
181
  PARTIAL_DUPLICATE_BATCH: 400,
181
182
  SUPERSEDED_ATTEMPT: 409,