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.
- package/CHANGELOG.md +41 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +20 -17
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +80 -78
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +231 -182
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -4
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +104 -91
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +10 -15
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +182 -293
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +36 -15
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +315 -312
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +138 -101
- package/src/store-postgres.ts +355 -238
- package/src/store-redis-core.ts +247 -237
- package/src/store-redis-http.ts +1 -2
- package/src/store-redis.ts +1 -2
- package/src/store-sqlite.ts +191 -153
- package/src/store.ts +24 -66
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
package/src/store-redis-http.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
package/src/store-redis.ts
CHANGED
|
@@ -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
|
-
|
|
295
|
-
inspect: core.inspect,
|
|
294
|
+
putSnapshots: core.putSnapshots,
|
|
296
295
|
|
|
297
296
|
presence: {
|
|
298
297
|
async set(ns, participant, values, meta) {
|
package/src/store-sqlite.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
874
|
-
snapshot
|
|
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(
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
915
|
-
wrap(() =>
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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(
|
|
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
|
-
|
|
282
|
+
requests: readonly StoreStateReadRequest[],
|
|
322
283
|
reducerName: string,
|
|
323
284
|
): Promise<StoreStateRead[]>
|
|
324
285
|
|
|
325
286
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
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
|
-
|
|
292
|
+
putSnapshots(
|
|
331
293
|
sessionId: string,
|
|
332
294
|
reducerName: string,
|
|
333
|
-
|
|
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
|
|
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,
|