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-postgres.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
type EventCause,
|
|
32
32
|
type IdSource,
|
|
33
33
|
type StoredEvent,
|
|
34
|
+
type StoreStateReadRequest,
|
|
34
35
|
type StoreStateRead,
|
|
35
36
|
} from './store.ts'
|
|
36
37
|
|
|
@@ -120,6 +121,26 @@ create table if not exists a2_snapshots (
|
|
|
120
121
|
primary key (session_id, reducer_name)
|
|
121
122
|
);
|
|
122
123
|
|
|
124
|
+
create table if not exists a2_snapshot_history (
|
|
125
|
+
session_id text not null,
|
|
126
|
+
reducer_name text not null,
|
|
127
|
+
up_to_index bigint not null,
|
|
128
|
+
state jsonb not null,
|
|
129
|
+
updated_at bigint not null,
|
|
130
|
+
primary key (session_id, reducer_name, up_to_index)
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
create table if not exists a2_snapshot_pins (
|
|
134
|
+
session_id text not null,
|
|
135
|
+
event_index bigint not null,
|
|
136
|
+
reducer_name text not null,
|
|
137
|
+
up_to_index bigint not null,
|
|
138
|
+
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
create index if not exists a2_snapshot_pins_checkpoint
|
|
142
|
+
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
143
|
+
|
|
123
144
|
create table if not exists a2_presence (
|
|
124
145
|
ns text not null,
|
|
125
146
|
participant text not null,
|
|
@@ -287,6 +308,103 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
287
308
|
})
|
|
288
309
|
const client = connection.get
|
|
289
310
|
|
|
311
|
+
const readStateBatch = async (
|
|
312
|
+
requests: readonly StoreStateReadRequest[],
|
|
313
|
+
reducerName: string,
|
|
314
|
+
): Promise<StoreStateRead[]> => {
|
|
315
|
+
if (requests.length === 0) return []
|
|
316
|
+
const c = await client()
|
|
317
|
+
const input = requests.map((request, position) => ({
|
|
318
|
+
position,
|
|
319
|
+
sessionId: request.sessionId,
|
|
320
|
+
throughIndex: request.throughIndex ?? null,
|
|
321
|
+
snapshotThroughIndex:
|
|
322
|
+
request.snapshotThroughIndex ?? request.throughIndex ?? null,
|
|
323
|
+
}))
|
|
324
|
+
const { rows } = await c.query(
|
|
325
|
+
`with request as materialized (
|
|
326
|
+
select * from jsonb_to_recordset($1::jsonb) as value(
|
|
327
|
+
position integer,
|
|
328
|
+
"sessionId" text,
|
|
329
|
+
"throughIndex" bigint,
|
|
330
|
+
"snapshotThroughIndex" bigint
|
|
331
|
+
)
|
|
332
|
+
), selected as materialized (
|
|
333
|
+
select request.position,
|
|
334
|
+
request."sessionId" as session_id,
|
|
335
|
+
request."throughIndex" as through_index,
|
|
336
|
+
head.up_to_index as head_index,
|
|
337
|
+
candidate.up_to_index as snapshot_index,
|
|
338
|
+
candidate.state as snapshot_state
|
|
339
|
+
from request
|
|
340
|
+
left join a2_snapshots head
|
|
341
|
+
on head.session_id = request."sessionId"
|
|
342
|
+
and head.reducer_name = $2
|
|
343
|
+
left join lateral (
|
|
344
|
+
select up_to_index, state from (
|
|
345
|
+
select head.up_to_index, head.state
|
|
346
|
+
where request."snapshotThroughIndex" is null
|
|
347
|
+
or head.up_to_index <= request."snapshotThroughIndex"
|
|
348
|
+
union all
|
|
349
|
+
select history.up_to_index, history.state
|
|
350
|
+
from a2_snapshot_history history
|
|
351
|
+
where history.session_id = request."sessionId"
|
|
352
|
+
and history.reducer_name = $2
|
|
353
|
+
and (
|
|
354
|
+
request."snapshotThroughIndex" is null
|
|
355
|
+
or history.up_to_index <= request."snapshotThroughIndex"
|
|
356
|
+
)
|
|
357
|
+
) candidates
|
|
358
|
+
order by up_to_index desc
|
|
359
|
+
limit 1
|
|
360
|
+
) candidate on true
|
|
361
|
+
)
|
|
362
|
+
select selected.position, 0 as row_order, selected.session_id,
|
|
363
|
+
selected.head_index, selected.snapshot_index,
|
|
364
|
+
selected.snapshot_state,
|
|
365
|
+
null::bigint as idx, null::text as event_type,
|
|
366
|
+
null::jsonb as payload, null::text as event_id,
|
|
367
|
+
null::bigint as created_at
|
|
368
|
+
from selected
|
|
369
|
+
union all
|
|
370
|
+
select selected.position, 1 as row_order, event.session_id,
|
|
371
|
+
selected.head_index, null::bigint as snapshot_index,
|
|
372
|
+
null::jsonb as snapshot_state,
|
|
373
|
+
event.idx, event.event_type, event.payload,
|
|
374
|
+
event.event_id, event.created_at
|
|
375
|
+
from selected
|
|
376
|
+
join a2_events event on event.session_id = selected.session_id
|
|
377
|
+
where event.idx > coalesce(selected.snapshot_index, 0)
|
|
378
|
+
and (
|
|
379
|
+
selected.through_index is null
|
|
380
|
+
or event.idx <= selected.through_index
|
|
381
|
+
)
|
|
382
|
+
order by position, row_order, idx`,
|
|
383
|
+
[jsonb(input), reducerName],
|
|
384
|
+
)
|
|
385
|
+
const reads = requests.map<StoreStateRead>(() => ({
|
|
386
|
+
headIndex: null,
|
|
387
|
+
snapshot: null,
|
|
388
|
+
events: [],
|
|
389
|
+
}))
|
|
390
|
+
for (const row of rows) {
|
|
391
|
+
const read = reads[asNumber(row['position'])]!
|
|
392
|
+
if (row['row_order'] === 0 || row['row_order'] === '0') {
|
|
393
|
+
read.headIndex =
|
|
394
|
+
row['head_index'] === null ? null : asNumber(row['head_index'])
|
|
395
|
+
if (row['snapshot_index'] !== null) {
|
|
396
|
+
read.snapshot = {
|
|
397
|
+
index: asNumber(row['snapshot_index']),
|
|
398
|
+
state: row['snapshot_state'],
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
read.events.push(toEvent(row))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return reads
|
|
406
|
+
}
|
|
407
|
+
|
|
290
408
|
// Transactions run on one connection. Pools check one out; a
|
|
291
409
|
// single-connection client (PGlite) serializes on this mutex instead.
|
|
292
410
|
let mutex: Promise<unknown> = Promise.resolve()
|
|
@@ -797,6 +915,22 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
797
915
|
where session_id = $3 and idx = $4`,
|
|
798
916
|
[now, attempt, sessionId, index, jsonb(ids)],
|
|
799
917
|
)
|
|
918
|
+
await query(
|
|
919
|
+
`delete from a2_snapshot_pins
|
|
920
|
+
where session_id = $1 and event_index = $2`,
|
|
921
|
+
[sessionId, index],
|
|
922
|
+
)
|
|
923
|
+
await query(
|
|
924
|
+
`delete from a2_snapshot_history history
|
|
925
|
+
where history.session_id = $1
|
|
926
|
+
and not exists (
|
|
927
|
+
select 1 from a2_snapshot_pins pin
|
|
928
|
+
where pin.session_id = history.session_id
|
|
929
|
+
and pin.reducer_name = history.reducer_name
|
|
930
|
+
and pin.up_to_index = history.up_to_index
|
|
931
|
+
)`,
|
|
932
|
+
[sessionId],
|
|
933
|
+
)
|
|
800
934
|
const parentLane = (parent['lane'] as string | null) ?? null
|
|
801
935
|
if (parentLane !== null) {
|
|
802
936
|
await query(
|
|
@@ -816,10 +950,10 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
816
950
|
},
|
|
817
951
|
|
|
818
952
|
async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
|
|
819
|
-
return wrap(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
953
|
+
return wrap(() =>
|
|
954
|
+
withTx(async (query) => {
|
|
955
|
+
const { rows } = await query(
|
|
956
|
+
`with current as materialized (
|
|
823
957
|
select e.* from a2_events e
|
|
824
958
|
where e.session_id = $1 and e.idx = $2
|
|
825
959
|
for update of e
|
|
@@ -867,148 +1001,231 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
867
1001
|
(select failure_count from updated),
|
|
868
1002
|
(select failure_count from current)
|
|
869
1003
|
) as failure_count`,
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
)
|
|
879
|
-
const row = rows[0]
|
|
880
|
-
if (!row || row['outcome'] === 'missing') {
|
|
881
|
-
throw new TypeError(
|
|
882
|
-
`no event at index ${index} in session '${sessionId}'`,
|
|
1004
|
+
[
|
|
1005
|
+
sessionId,
|
|
1006
|
+
index,
|
|
1007
|
+
attempt,
|
|
1008
|
+
error,
|
|
1009
|
+
maxFailures,
|
|
1010
|
+
clock.now().getTime(),
|
|
1011
|
+
],
|
|
883
1012
|
)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1013
|
+
const row = rows[0]
|
|
1014
|
+
if (!row || row['outcome'] === 'missing') {
|
|
1015
|
+
throw new TypeError(
|
|
1016
|
+
`no event at index ${index} in session '${sessionId}'`,
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
if (row['outcome'] === 'dead_lettered') {
|
|
1020
|
+
await query(
|
|
1021
|
+
`delete from a2_snapshot_pins
|
|
1022
|
+
where session_id = $1 and event_index = $2`,
|
|
1023
|
+
[sessionId, index],
|
|
1024
|
+
)
|
|
1025
|
+
await query(
|
|
1026
|
+
`delete from a2_snapshot_history history
|
|
1027
|
+
where history.session_id = $1
|
|
1028
|
+
and not exists (
|
|
1029
|
+
select 1 from a2_snapshot_pins pin
|
|
1030
|
+
where pin.session_id = history.session_id
|
|
1031
|
+
and pin.reducer_name = history.reducer_name
|
|
1032
|
+
and pin.up_to_index = history.up_to_index
|
|
1033
|
+
)`,
|
|
1034
|
+
[sessionId],
|
|
1035
|
+
)
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
outcome: row['outcome'] as
|
|
1039
|
+
'failed' | 'dead_lettered' | 'superseded',
|
|
1040
|
+
failureCount: asNumber(row['failure_count']),
|
|
1041
|
+
}
|
|
1042
|
+
}),
|
|
1043
|
+
)
|
|
890
1044
|
},
|
|
891
1045
|
|
|
892
|
-
async readState(sessionId, reducerName) {
|
|
893
|
-
return wrap(
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
from a2_events event
|
|
915
|
-
where event.session_id = $1
|
|
916
|
-
and event.idx > coalesce((select up_to_index from snapshot), 0)
|
|
917
|
-
order by row_order, idx`,
|
|
918
|
-
[sessionId, reducerName],
|
|
919
|
-
)
|
|
920
|
-
const snapshot = rows.find((row) => row['row_kind'] === 'snapshot')
|
|
921
|
-
return {
|
|
922
|
-
snapshot: snapshot
|
|
923
|
-
? {
|
|
924
|
-
index: asNumber(snapshot['snapshot_index']),
|
|
925
|
-
state: snapshot['snapshot_state'],
|
|
926
|
-
}
|
|
927
|
-
: null,
|
|
928
|
-
events: rows
|
|
929
|
-
.filter((row) => row['row_kind'] === 'event')
|
|
930
|
-
.map(toEvent),
|
|
931
|
-
}
|
|
932
|
-
})
|
|
1046
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
1047
|
+
return wrap(
|
|
1048
|
+
async () =>
|
|
1049
|
+
(
|
|
1050
|
+
await readStateBatch(
|
|
1051
|
+
[
|
|
1052
|
+
{
|
|
1053
|
+
sessionId,
|
|
1054
|
+
...(stateOptions?.throughIndex === undefined
|
|
1055
|
+
? {}
|
|
1056
|
+
: { throughIndex: stateOptions.throughIndex }),
|
|
1057
|
+
...(stateOptions?.snapshotThroughIndex === undefined
|
|
1058
|
+
? {}
|
|
1059
|
+
: {
|
|
1060
|
+
snapshotThroughIndex: stateOptions.snapshotThroughIndex,
|
|
1061
|
+
}),
|
|
1062
|
+
},
|
|
1063
|
+
],
|
|
1064
|
+
reducerName,
|
|
1065
|
+
)
|
|
1066
|
+
)[0]!,
|
|
1067
|
+
)
|
|
933
1068
|
},
|
|
934
1069
|
|
|
935
|
-
async readStates(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1070
|
+
async readStates(requests, reducerName) {
|
|
1071
|
+
return wrap(() => readStateBatch(requests, reducerName))
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
1075
|
+
await wrap(() =>
|
|
1076
|
+
withTx(async (query) => {
|
|
1077
|
+
const normalized = [
|
|
1078
|
+
...new Map(writes.map((write) => [write.index, write])).values(),
|
|
1079
|
+
].toSorted((a, b) => a.index - b.index)
|
|
1080
|
+
if (normalized.length === 0) return
|
|
1081
|
+
|
|
1082
|
+
await query(
|
|
1083
|
+
'select pg_advisory_xact_lock(hashtext($1), hashtext($2))',
|
|
1084
|
+
[sessionId, reducerName],
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
const requestedPins = normalized.flatMap((write) =>
|
|
1088
|
+
(write.pinEventIndexes ?? []).map((eventIndex) => ({
|
|
1089
|
+
eventIndex,
|
|
1090
|
+
snapshotIndex: write.index,
|
|
1091
|
+
})),
|
|
1092
|
+
)
|
|
1093
|
+
if (requestedPins.length > 0) {
|
|
1094
|
+
const eventIndexes = [
|
|
1095
|
+
...new Set(requestedPins.map((pin) => pin.eventIndex)),
|
|
1096
|
+
]
|
|
1097
|
+
const { rows: events } = await query(
|
|
1098
|
+
`select idx, processed_at, failed_at from a2_events
|
|
1099
|
+
where session_id = $1 and idx = any($2::bigint[])
|
|
1100
|
+
for update`,
|
|
1101
|
+
[sessionId, eventIndexes],
|
|
1102
|
+
)
|
|
1103
|
+
const unfinished = new Set(
|
|
1104
|
+
events
|
|
1105
|
+
.filter(
|
|
1106
|
+
(row) =>
|
|
1107
|
+
row['processed_at'] === null && row['failed_at'] === null,
|
|
1108
|
+
)
|
|
1109
|
+
.map((row) => asNumber(row['idx'])),
|
|
1110
|
+
)
|
|
1111
|
+
const accepted = requestedPins.filter((pin) =>
|
|
1112
|
+
unfinished.has(pin.eventIndex),
|
|
1113
|
+
)
|
|
1114
|
+
if (accepted.length > 0) {
|
|
1115
|
+
await query(
|
|
1116
|
+
`insert into a2_snapshot_pins
|
|
1117
|
+
(session_id, event_index, reducer_name, up_to_index)
|
|
1118
|
+
select $1, pin."eventIndex", $2, pin."snapshotIndex"
|
|
1119
|
+
from jsonb_to_recordset($3::jsonb) as pin(
|
|
1120
|
+
"eventIndex" bigint,
|
|
1121
|
+
"snapshotIndex" bigint
|
|
1122
|
+
)
|
|
1123
|
+
on conflict do nothing`,
|
|
1124
|
+
[sessionId, reducerName, jsonb(accepted)],
|
|
1125
|
+
)
|
|
984
1126
|
}
|
|
985
|
-
} else {
|
|
986
|
-
read.events.push(toEvent(row))
|
|
987
1127
|
}
|
|
988
|
-
}
|
|
989
|
-
return sessionIds.map(
|
|
990
|
-
(sessionId) =>
|
|
991
|
-
bySession.get(sessionId) ?? { snapshot: null, events: [] },
|
|
992
|
-
)
|
|
993
|
-
})
|
|
994
|
-
},
|
|
995
1128
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1129
|
+
const { rows: headRows } = await query(
|
|
1130
|
+
`select up_to_index, state, updated_at from a2_snapshots
|
|
1131
|
+
where session_id = $1 and reducer_name = $2
|
|
1132
|
+
for update`,
|
|
1133
|
+
[sessionId, reducerName],
|
|
1134
|
+
)
|
|
1135
|
+
const currentHead = headRows[0]
|
|
1136
|
+
const latestWrite = normalized.at(-1)!
|
|
1137
|
+
const writesHead =
|
|
1138
|
+
!currentHead ||
|
|
1139
|
+
latestWrite.index >= asNumber(currentHead['up_to_index'])
|
|
1140
|
+
const advancesHead =
|
|
1141
|
+
!currentHead ||
|
|
1142
|
+
latestWrite.index > asNumber(currentHead['up_to_index'])
|
|
1143
|
+
const finalHeadIndex = advancesHead
|
|
1144
|
+
? latestWrite.index
|
|
1145
|
+
: asNumber(currentHead['up_to_index'])
|
|
1146
|
+
const relevantIndexes = [
|
|
1147
|
+
...new Set([
|
|
1148
|
+
...normalized.map((write) => write.index),
|
|
1149
|
+
...(currentHead ? [asNumber(currentHead['up_to_index'])] : []),
|
|
1150
|
+
]),
|
|
1151
|
+
]
|
|
1152
|
+
const { rows: pinRows } = await query(
|
|
1153
|
+
`select distinct up_to_index from a2_snapshot_pins
|
|
1154
|
+
where session_id = $1 and reducer_name = $2
|
|
1155
|
+
and up_to_index = any($3::bigint[])`,
|
|
1156
|
+
[sessionId, reducerName, relevantIndexes],
|
|
1157
|
+
)
|
|
1158
|
+
const pinned = new Set(
|
|
1159
|
+
pinRows.map((row) => asNumber(row['up_to_index'])),
|
|
1160
|
+
)
|
|
1161
|
+
const historical = new Map<
|
|
1162
|
+
number,
|
|
1163
|
+
{ index: number; state: unknown; updatedAt: number }
|
|
1164
|
+
>()
|
|
1165
|
+
if (
|
|
1166
|
+
advancesHead &&
|
|
1167
|
+
currentHead &&
|
|
1168
|
+
pinned.has(asNumber(currentHead['up_to_index']))
|
|
1169
|
+
) {
|
|
1170
|
+
const index = asNumber(currentHead['up_to_index'])
|
|
1171
|
+
historical.set(index, {
|
|
1172
|
+
index,
|
|
1173
|
+
state: currentHead['state'],
|
|
1174
|
+
updatedAt: asNumber(currentHead['updated_at']),
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
const updatedAt = clock.now().getTime()
|
|
1178
|
+
for (const write of normalized) {
|
|
1179
|
+
if (write.index < finalHeadIndex && pinned.has(write.index)) {
|
|
1180
|
+
historical.set(write.index, {
|
|
1181
|
+
index: write.index,
|
|
1182
|
+
state: write.state,
|
|
1183
|
+
updatedAt,
|
|
1184
|
+
})
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (historical.size > 0) {
|
|
1188
|
+
await query(
|
|
1189
|
+
`insert into a2_snapshot_history
|
|
1190
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
1191
|
+
select $1, $2, snapshot.index, snapshot.state, snapshot."updatedAt"
|
|
1192
|
+
from jsonb_to_recordset($3::jsonb) as snapshot(
|
|
1193
|
+
index bigint,
|
|
1194
|
+
state jsonb,
|
|
1195
|
+
"updatedAt" bigint
|
|
1196
|
+
)
|
|
1197
|
+
on conflict (session_id, reducer_name, up_to_index) do update set
|
|
1198
|
+
state = excluded.state,
|
|
1199
|
+
updated_at = excluded.updated_at`,
|
|
1200
|
+
[sessionId, reducerName, jsonb([...historical.values()])],
|
|
1201
|
+
)
|
|
1202
|
+
}
|
|
1203
|
+
if (writesHead) {
|
|
1204
|
+
await query(
|
|
1205
|
+
`insert into a2_snapshots
|
|
1206
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
1207
|
+
values ($1, $2, $3, $4::jsonb, $5)
|
|
1208
|
+
on conflict (session_id, reducer_name) do update set
|
|
1209
|
+
up_to_index = excluded.up_to_index,
|
|
1210
|
+
state = excluded.state,
|
|
1211
|
+
updated_at = excluded.updated_at
|
|
1212
|
+
where excluded.up_to_index >= a2_snapshots.up_to_index`,
|
|
1213
|
+
[
|
|
1214
|
+
sessionId,
|
|
1215
|
+
reducerName,
|
|
1216
|
+
latestWrite.index,
|
|
1217
|
+
jsonb(latestWrite.state),
|
|
1218
|
+
clock.now().getTime(),
|
|
1219
|
+
],
|
|
1220
|
+
)
|
|
1221
|
+
await query(
|
|
1222
|
+
`delete from a2_snapshot_history
|
|
1223
|
+
where session_id = $1 and reducer_name = $2 and up_to_index = $3`,
|
|
1224
|
+
[sessionId, reducerName, latestWrite.index],
|
|
1225
|
+
)
|
|
1226
|
+
}
|
|
1227
|
+
}),
|
|
1228
|
+
)
|
|
1012
1229
|
},
|
|
1013
1230
|
|
|
1014
1231
|
presence: {
|
|
@@ -1086,106 +1303,6 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1086
1303
|
},
|
|
1087
1304
|
},
|
|
1088
1305
|
|
|
1089
|
-
inspect: {
|
|
1090
|
-
async listSessions(inspectionOptions) {
|
|
1091
|
-
return wrap(async () => {
|
|
1092
|
-
const c = await client()
|
|
1093
|
-
const conditions = ['left(session_id, length($1)) = $1']
|
|
1094
|
-
const params: unknown[] = [inspectionOptions.prefix]
|
|
1095
|
-
if (inspectionOptions.cursor !== undefined) {
|
|
1096
|
-
params.push(inspectionOptions.cursor)
|
|
1097
|
-
conditions.push(`session_id > $${params.length}`)
|
|
1098
|
-
}
|
|
1099
|
-
params.push(inspectionOptions.limit + 1)
|
|
1100
|
-
const { rows } = await c.query(
|
|
1101
|
-
`select
|
|
1102
|
-
session_id,
|
|
1103
|
-
count(*) as event_count,
|
|
1104
|
-
count(*) filter (where processed_at is null and failed_at is null) as pending_count,
|
|
1105
|
-
count(*) filter (where failed_at is not null) as failed_count,
|
|
1106
|
-
sum(attempt_count) as attempt_count,
|
|
1107
|
-
sum(failure_count) as failure_count,
|
|
1108
|
-
min(created_at) as first_event_at,
|
|
1109
|
-
max(greatest(
|
|
1110
|
-
created_at,
|
|
1111
|
-
coalesce(first_claimed_at, 0),
|
|
1112
|
-
coalesce(last_claimed_at, 0),
|
|
1113
|
-
coalesce(last_failed_at, 0),
|
|
1114
|
-
coalesce(processed_at, 0),
|
|
1115
|
-
coalesce(failed_at, 0)
|
|
1116
|
-
)) as updated_at
|
|
1117
|
-
from a2_events
|
|
1118
|
-
where ${conditions.join(' and ')}
|
|
1119
|
-
group by session_id
|
|
1120
|
-
order by session_id
|
|
1121
|
-
limit $${params.length}`,
|
|
1122
|
-
params,
|
|
1123
|
-
)
|
|
1124
|
-
const hasMore = rows.length > inspectionOptions.limit
|
|
1125
|
-
const visible = rows.slice(0, inspectionOptions.limit)
|
|
1126
|
-
return {
|
|
1127
|
-
sessions: visible.map((row) => ({
|
|
1128
|
-
sessionId: row['session_id'] as string,
|
|
1129
|
-
eventCount: asNumber(row['event_count']),
|
|
1130
|
-
pendingCount: asNumber(row['pending_count']),
|
|
1131
|
-
failedCount: asNumber(row['failed_count']),
|
|
1132
|
-
attemptCount: asNumber(row['attempt_count']),
|
|
1133
|
-
failureCount: asNumber(row['failure_count']),
|
|
1134
|
-
firstEventAt: new Date(asNumber(row['first_event_at'])),
|
|
1135
|
-
updatedAt: new Date(asNumber(row['updated_at'])),
|
|
1136
|
-
})),
|
|
1137
|
-
cursor: hasMore
|
|
1138
|
-
? ((visible.at(-1)?.['session_id'] as string | undefined) ?? null)
|
|
1139
|
-
: null,
|
|
1140
|
-
}
|
|
1141
|
-
})
|
|
1142
|
-
},
|
|
1143
|
-
async readEvents(sessionId, pageOptions) {
|
|
1144
|
-
return wrap(async () => {
|
|
1145
|
-
const c = await client()
|
|
1146
|
-
const currentFrontier = asNumber(
|
|
1147
|
-
(
|
|
1148
|
-
await c.query(
|
|
1149
|
-
'select coalesce(max(idx), 0) as frontier from a2_events where session_id = $1',
|
|
1150
|
-
[sessionId],
|
|
1151
|
-
)
|
|
1152
|
-
).rows[0]?.['frontier'] ?? 0,
|
|
1153
|
-
)
|
|
1154
|
-
const throughIndex = Math.min(
|
|
1155
|
-
pageOptions.throughIndex ?? currentFrontier,
|
|
1156
|
-
currentFrontier,
|
|
1157
|
-
)
|
|
1158
|
-
const { rows } = await c.query(
|
|
1159
|
-
`select * from a2_events
|
|
1160
|
-
where session_id = $1 and idx > $2 and idx <= $3
|
|
1161
|
-
order by idx limit $4`,
|
|
1162
|
-
[
|
|
1163
|
-
sessionId,
|
|
1164
|
-
pageOptions.afterIndex,
|
|
1165
|
-
throughIndex,
|
|
1166
|
-
pageOptions.limit,
|
|
1167
|
-
],
|
|
1168
|
-
)
|
|
1169
|
-
return { events: rows.map(toStored), throughIndex }
|
|
1170
|
-
})
|
|
1171
|
-
},
|
|
1172
|
-
async listSnapshots(sessionId) {
|
|
1173
|
-
return wrap(async () => {
|
|
1174
|
-
const c = await client()
|
|
1175
|
-
const { rows } = await c.query(
|
|
1176
|
-
`select reducer_name, up_to_index, updated_at
|
|
1177
|
-
from a2_snapshots where session_id = $1 order by reducer_name`,
|
|
1178
|
-
[sessionId],
|
|
1179
|
-
)
|
|
1180
|
-
return rows.map((row) => ({
|
|
1181
|
-
reducerName: row['reducer_name'] as string,
|
|
1182
|
-
index: asNumber(row['up_to_index']),
|
|
1183
|
-
updatedAt: new Date(asNumber(row['updated_at'])),
|
|
1184
|
-
}))
|
|
1185
|
-
})
|
|
1186
|
-
},
|
|
1187
|
-
},
|
|
1188
|
-
|
|
1189
1306
|
stream(sessionId, opts) {
|
|
1190
1307
|
const readAfter = (afterIndex: number): Promise<StoredEvent[]> =>
|
|
1191
1308
|
wrap(async () => {
|