experimental-a2 0.23.0 → 0.24.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 +8 -0
- package/dist/{actor-CqibLgjQ.d.ts → actor-Bqyb7gwf.d.ts} +3 -3
- package/dist/{actor-CqibLgjQ.d.ts.map → actor-Bqyb7gwf.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +2 -2
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-DFET5q16.js → actor-shared-B4OB6Qo7.js} +3 -3
- package/dist/{actor-shared-DFET5q16.js.map → actor-shared-B4OB6Qo7.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +4 -4
- package/dist/{ai-DAu77CDr.d.ts → ai-BZUu5-b7.d.ts} +2 -2
- package/dist/{ai-DAu77CDr.d.ts.map → ai-BZUu5-b7.d.ts.map} +1 -1
- package/dist/{ai-EEfKyc-z.js → ai-CCCNxd3L.js} +3 -3
- package/dist/{ai-EEfKyc-z.js.map → ai-CCCNxd3L.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/{ai-harness-DTjkAr2T.d.ts → ai-harness-C8aV869s.d.ts} +2 -2
- package/dist/{ai-harness-DTjkAr2T.d.ts.map → ai-harness-C8aV869s.d.ts.map} +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-react.d.ts +4 -4
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.js +4 -4
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-CUMbS_38.d.ts → client-CCJASjUM.d.ts} +2 -2
- package/dist/{client-CUMbS_38.d.ts.map → client-CCJASjUM.d.ts.map} +1 -1
- package/dist/{client-BQS4i_V9.js → client-sq-djxhD.js} +4 -4
- package/dist/{client-BQS4i_V9.js.map → client-sq-djxhD.js.map} +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CWukt01d.js → contract-DqfBSIqJ.js} +2 -2
- package/dist/{contract-CWukt01d.js.map → contract-DqfBSIqJ.js.map} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-D3Lve43o.d.ts → initial-snapshot-BA6xwv10.d.ts} +2 -2
- package/dist/initial-snapshot-BA6xwv10.d.ts.map +1 -0
- package/dist/{react-BoHvC3v9.d.ts → react-KC4eW7yG.d.ts} +3 -3
- package/dist/{react-BoHvC3v9.d.ts.map → react-KC4eW7yG.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CjmvZSAb.js → reducer-Bswr11ar.js} +2 -2
- package/dist/{reducer-CjmvZSAb.js.map → reducer-Bswr11ar.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-qgez0Xny.d.ts → server-B9gu0qi1.d.ts} +9 -3
- package/dist/server-B9gu0qi1.d.ts.map +1 -0
- package/dist/{server-DtFrWxi3.js → server-DX7mM6nm.js} +22 -14
- package/dist/server-DX7mM6nm.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-wHd5GCpZ.js → session-clear-XJYpmUpg.js} +14 -2
- package/dist/session-clear-XJYpmUpg.js.map +1 -0
- package/dist/{session-clear-D1PFe7xl.d.ts → session-clear-vfonou9S.d.ts} +2 -2
- package/dist/session-clear-vfonou9S.d.ts.map +1 -0
- package/dist/{store-D0MhBvg6.d.ts → store-DEzcHZOA.d.ts} +11 -1
- package/dist/store-DEzcHZOA.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-codec-DwrWlgQx.js → store-codec-pAG2oPgQ.js} +4 -4
- package/dist/store-codec-pAG2oPgQ.js.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 +11 -4
- package/dist/store-memory.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 +26 -7
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -2
- package/dist/{store-redis-notify-CBDVJxA_.js → store-redis-notify-ys_-VrSP.js} +27 -16
- package/dist/store-redis-notify-ys_-VrSP.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +11 -4
- package/dist/store-sqlite.js.map +1 -1
- package/dist/store-valkey.d.ts +1 -1
- package/dist/store-valkey.js +2 -2
- package/dist/{wire-CV-Z7wz-.js → wire-BFE9sIQf.js} +3 -2
- package/dist/wire-BFE9sIQf.js.map +1 -0
- package/docs/contract/03-sessions.mdx +51 -0
- package/docs/reference/02-server.mdx +44 -4
- package/docs/reference/06-errors.mdx +1 -0
- package/examples/playground/app/a2slack/consumers/search.ts +1 -1
- package/examples/playground/app/a2slack/db/migrations/0005_a2slack_feed_v2.sql +7 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0005_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/a2slack/db/queries.ts +1 -1
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +17 -14
- package/examples/playground/app/a2slack/search-index.ts +1 -1
- package/examples/playground/app/a2slack/server.ts +2 -2
- package/examples/playground/lib/store.ts +2 -0
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/src/errors.ts +39 -0
- package/src/idempotent-replay.ts +4 -3
- package/src/server.ts +32 -9
- package/src/store-memory.ts +26 -4
- package/src/store-postgres.ts +36 -6
- package/src/store-redis-core.ts +40 -15
- package/src/store-sqlite.ts +25 -4
- package/src/store.ts +10 -0
- package/src/wire.ts +1 -0
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +0 -1
- package/dist/server-DtFrWxi3.js.map +0 -1
- package/dist/server-qgez0Xny.d.ts.map +0 -1
- package/dist/session-clear-D1PFe7xl.d.ts.map +0 -1
- package/dist/session-clear-wHd5GCpZ.js.map +0 -1
- package/dist/store-D0MhBvg6.d.ts.map +0 -1
- package/dist/store-codec-DwrWlgQx.js.map +0 -1
- package/dist/store-redis-notify-CBDVJxA_.js.map +0 -1
- package/dist/wire-CV-Z7wz-.js.map +0 -1
package/src/store-redis-core.ts
CHANGED
|
@@ -53,7 +53,12 @@
|
|
|
53
53
|
*/
|
|
54
54
|
|
|
55
55
|
import type { PresencePatch } from './contract.ts'
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
A2Error,
|
|
58
|
+
duplicateBatchError,
|
|
59
|
+
lastIndexMismatchError,
|
|
60
|
+
supersededAttemptError,
|
|
61
|
+
} from './errors.ts'
|
|
57
62
|
import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
|
|
58
63
|
import { idempotentReplay } from './idempotent-replay.ts'
|
|
59
64
|
import { NOTIFY_TIMINGS, nullProtoRecord } from './internal.ts'
|
|
@@ -705,11 +710,12 @@ end
|
|
|
705
710
|
|
|
706
711
|
/**
|
|
707
712
|
* KEYS: counter.
|
|
708
|
-
* ARGV: sessionId, nowMs, n, served consumers, origin index or '',
|
|
709
|
-
* event: id, type, payloadJson, causeJson or '',
|
|
713
|
+
* ARGV: sessionId, nowMs, n, served consumers, origin index or '',
|
|
714
|
+
* ifLastIndex or '', then per event: id, type, payloadJson, causeJson or '',
|
|
715
|
+
* targets.
|
|
710
716
|
* Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |
|
|
711
|
-
* ['dup', hasPending, ...indexes] | ['partial', count] |
|
|
712
|
-
* ['superseded', position] | ['cleared', originIndex] |
|
|
717
|
+
* ['dup', hasPending, head, ...indexes] | ['partial', count, head] |
|
|
718
|
+
* ['superseded', position] | ['cleared', originIndex] | ['moved', head] |
|
|
713
719
|
* ['index', type, after].
|
|
714
720
|
*/
|
|
715
721
|
const APPEND_LUA = `${BASE_LUA}${CURSOR_LUA}
|
|
@@ -717,7 +723,7 @@ local now, n = ARGV[2], tonumber(ARGV[3])
|
|
|
717
723
|
use_served(ARGV[4])
|
|
718
724
|
if n == 0 then return { 'empty', has_pending() } end
|
|
719
725
|
local ids = {}
|
|
720
|
-
for i = 1, n do ids[i] = ARGV[
|
|
726
|
+
for i = 1, n do ids[i] = ARGV[6 + (i - 1) * 5 + 1] end
|
|
721
727
|
local owners = owners_of(read_bounds(ids))
|
|
722
728
|
local base = total
|
|
723
729
|
local dups = {}
|
|
@@ -725,18 +731,18 @@ for i = 1, n do
|
|
|
725
731
|
if owners[i] then dups[#dups + 1] = owners[i] end
|
|
726
732
|
end
|
|
727
733
|
if #dups == n then
|
|
728
|
-
local res = { 'dup', has_pending() }
|
|
734
|
+
local res = { 'dup', has_pending(), tostring(total) }
|
|
729
735
|
for i = 1, #dups do res[#res + 1] = dups[i] end
|
|
730
736
|
return res
|
|
731
737
|
end
|
|
732
|
-
if #dups > 0 then return { 'partial', tostring(#dups) } end
|
|
738
|
+
if #dups > 0 then return { 'partial', tostring(#dups), tostring(total) } end
|
|
733
739
|
local origin = ARGV[5]
|
|
734
740
|
if origin ~= '' and tonumber(origin) <= boundary then return { 'cleared', origin } end
|
|
735
741
|
-- The attempt-currency fence: a cause's head row must still be its cursor's
|
|
736
742
|
-- head, at that attempt, not dead-lettered.
|
|
737
743
|
local refs, fenced = {}, {}
|
|
738
744
|
for i = 1, n do
|
|
739
|
-
local raw = ARGV[
|
|
745
|
+
local raw = ARGV[6 + (i - 1) * 5 + 4]
|
|
740
746
|
if raw ~= '' then
|
|
741
747
|
local cause = cjson.decode(raw)
|
|
742
748
|
local index = tonumber(cause.index)
|
|
@@ -756,16 +762,18 @@ for k, cause in ipairs(fenced) do
|
|
|
756
762
|
end
|
|
757
763
|
if served_has(cause.consumer) then pending_fast = true end
|
|
758
764
|
end
|
|
765
|
+
local expected = ARGV[6]
|
|
766
|
+
if expected ~= '' and tonumber(expected) ~= total then return { 'moved', tostring(total) } end
|
|
759
767
|
local batch = {}
|
|
760
768
|
for i = 1, n do
|
|
761
|
-
local o =
|
|
769
|
+
local o = 6 + (i - 1) * 5
|
|
762
770
|
batch[i] = { idx = base + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 5]) }
|
|
763
771
|
end
|
|
764
772
|
local planned, missing, after = plan(batch, base)
|
|
765
773
|
if not planned then return { 'index', missing, tostring(after) } end
|
|
766
774
|
local marks = {}
|
|
767
775
|
for i = 1, n do
|
|
768
|
-
local o =
|
|
776
|
+
local o = 6 + (i - 1) * 5
|
|
769
777
|
local field = tostring(base + i)
|
|
770
778
|
redis.call('XADD', LOG, field .. '-0',
|
|
771
779
|
'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],
|
|
@@ -2408,6 +2416,11 @@ export function createRedisStoreCore(
|
|
|
2408
2416
|
)
|
|
2409
2417
|
}
|
|
2410
2418
|
const now = clock.now().getTime()
|
|
2419
|
+
const ifLastIndex = appendOptions?.ifLastIndex
|
|
2420
|
+
const conditionalAt = (head: string | undefined) =>
|
|
2421
|
+
ifLastIndex === undefined
|
|
2422
|
+
? undefined
|
|
2423
|
+
: { sessionId, ifLastIndex, head: Number(head) }
|
|
2411
2424
|
const withIds = events.map((e) => ({
|
|
2412
2425
|
id: e.id ?? generateId(),
|
|
2413
2426
|
type: e.type,
|
|
@@ -2427,6 +2440,7 @@ export function createRedisStoreCore(
|
|
|
2427
2440
|
withIds.length,
|
|
2428
2441
|
servedArg(appendOptions?.consumers),
|
|
2429
2442
|
appendOptions?.originIndex ?? '',
|
|
2443
|
+
ifLastIndex ?? '',
|
|
2430
2444
|
...withIds.flatMap((e, i) => [
|
|
2431
2445
|
e.id,
|
|
2432
2446
|
e.type,
|
|
@@ -2453,15 +2467,22 @@ export function createRedisStoreCore(
|
|
|
2453
2467
|
withIds[Number(reply[1]) - 1]!.cause!,
|
|
2454
2468
|
)
|
|
2455
2469
|
}
|
|
2470
|
+
if (verdict === 'moved') {
|
|
2471
|
+
throw lastIndexMismatchError(
|
|
2472
|
+
sessionId,
|
|
2473
|
+
ifLastIndex!,
|
|
2474
|
+
Number(reply[1]),
|
|
2475
|
+
)
|
|
2476
|
+
}
|
|
2456
2477
|
if (verdict === 'partial') {
|
|
2457
|
-
throw
|
|
2458
|
-
'PARTIAL_DUPLICATE_BATCH',
|
|
2478
|
+
throw duplicateBatchError(
|
|
2459
2479
|
`batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`,
|
|
2480
|
+
conditionalAt(reply[2]),
|
|
2460
2481
|
)
|
|
2461
2482
|
}
|
|
2462
2483
|
if (verdict === 'dup') {
|
|
2463
2484
|
const hasPending = reply[1] === '1'
|
|
2464
|
-
const firstIndex = Math.min(...reply.slice(
|
|
2485
|
+
const firstIndex = Math.min(...reply.slice(3).map(Number))
|
|
2465
2486
|
// A valid retry occupies exactly events.length consecutive indexes.
|
|
2466
2487
|
const { rows: existing, clearIndex } = await retryAfterGap(() =>
|
|
2467
2488
|
readBoundedRange(
|
|
@@ -2477,7 +2498,11 @@ export function createRedisStoreCore(
|
|
|
2477
2498
|
`session '${sessionId}' was cleared while an append retry replayed; retry it`,
|
|
2478
2499
|
)
|
|
2479
2500
|
}
|
|
2480
|
-
const replayed = idempotentReplay(
|
|
2501
|
+
const replayed = idempotentReplay(
|
|
2502
|
+
events,
|
|
2503
|
+
existing,
|
|
2504
|
+
conditionalAt(reply[2]),
|
|
2505
|
+
)
|
|
2481
2506
|
// The first attempt may have committed without publishing.
|
|
2482
2507
|
await publish(
|
|
2483
2508
|
notifyChannel(sessionId),
|
package/src/store-sqlite.ts
CHANGED
|
@@ -15,7 +15,12 @@
|
|
|
15
15
|
import { mkdirSync } from 'node:fs'
|
|
16
16
|
import { dirname, resolve } from 'node:path'
|
|
17
17
|
import { DatabaseSync } from 'node:sqlite'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
A2Error,
|
|
20
|
+
duplicateBatchError,
|
|
21
|
+
lastIndexMismatchError,
|
|
22
|
+
supersededAttemptError,
|
|
23
|
+
} from './errors.ts'
|
|
19
24
|
import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
|
|
20
25
|
import { idempotentReplay } from './idempotent-replay.ts'
|
|
21
26
|
import {
|
|
@@ -850,18 +855,27 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
850
855
|
'batch contains the same event id more than once',
|
|
851
856
|
)
|
|
852
857
|
}
|
|
858
|
+
const ifLastIndex = appendOptions?.ifLastIndex
|
|
859
|
+
const conditional =
|
|
860
|
+
ifLastIndex === undefined
|
|
861
|
+
? undefined
|
|
862
|
+
: { sessionId, ifLastIndex, head: headOf(sessionId) }
|
|
853
863
|
if (suppliedIds.length > 0) {
|
|
854
864
|
const existing = liveOwnersOf(sessionId, clearIndex, suppliedIds)
|
|
855
865
|
if (existing.length > 0) {
|
|
856
866
|
if (existing.length === events.length) {
|
|
857
867
|
return {
|
|
858
|
-
events: idempotentReplay(
|
|
868
|
+
events: idempotentReplay(
|
|
869
|
+
events,
|
|
870
|
+
existing.map(toStored),
|
|
871
|
+
conditional,
|
|
872
|
+
),
|
|
859
873
|
hasPending: hasPending(sessionId, clearIndex, consumers),
|
|
860
874
|
}
|
|
861
875
|
}
|
|
862
|
-
throw
|
|
863
|
-
'PARTIAL_DUPLICATE_BATCH',
|
|
876
|
+
throw duplicateBatchError(
|
|
864
877
|
`batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`,
|
|
878
|
+
conditional,
|
|
865
879
|
)
|
|
866
880
|
}
|
|
867
881
|
}
|
|
@@ -880,6 +894,13 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
880
894
|
if (e.cause)
|
|
881
895
|
assertCurrentCause({ sessionId, cause: e.cause, clearIndex })
|
|
882
896
|
}
|
|
897
|
+
if (conditional && conditional.head !== conditional.ifLastIndex) {
|
|
898
|
+
throw lastIndexMismatchError(
|
|
899
|
+
sessionId,
|
|
900
|
+
conditional.ifLastIndex,
|
|
901
|
+
conditional.head,
|
|
902
|
+
)
|
|
903
|
+
}
|
|
883
904
|
|
|
884
905
|
const inserted = insertBatch({
|
|
885
906
|
sessionId,
|
package/src/store.ts
CHANGED
|
@@ -326,12 +326,22 @@ export interface A2Store {
|
|
|
326
326
|
* a `cause` at or below the latest clear marker throws
|
|
327
327
|
* `SUPERSEDED_ATTEMPT`, an `originIndex` there throws `SESSION_CLEARED`,
|
|
328
328
|
* and ids whose rows lie below the marker are free to reuse.
|
|
329
|
+
*
|
|
330
|
+
* `ifLastIndex` conditions a non-empty batch on the session's head, the
|
|
331
|
+
* last index including any clear marker: when the head is anything else,
|
|
332
|
+
* throw `lastIndexMismatchError` carrying the head and write nothing. It is
|
|
333
|
+
* checked last, after the replay path and both fences, inside the same
|
|
334
|
+
* atomic operation, never as a read before it. A conditional batch whose
|
|
335
|
+
* ids conflict (mixed, or not matching their rows) reports that head
|
|
336
|
+
* instead of `PARTIAL_DUPLICATE_BATCH` when it isn't `ifLastIndex`
|
|
337
|
+
* (`duplicateBatchError`).
|
|
329
338
|
*/
|
|
330
339
|
append(
|
|
331
340
|
sessionId: string,
|
|
332
341
|
events: AppendEvent[],
|
|
333
342
|
options?: {
|
|
334
343
|
originIndex?: number
|
|
344
|
+
ifLastIndex?: number
|
|
335
345
|
consumers?: readonly StoreConsumer[]
|
|
336
346
|
},
|
|
337
347
|
): Promise<StoreAppendResult>
|
package/src/wire.ts
CHANGED
|
@@ -309,6 +309,7 @@ const ERROR_STATUS: Record<A2ServerErrorCode, number> = {
|
|
|
309
309
|
PARTIAL_DUPLICATE_BATCH: 400,
|
|
310
310
|
SUPERSEDED_ATTEMPT: 409,
|
|
311
311
|
SESSION_CLEARED: 409,
|
|
312
|
+
LAST_INDEX_MISMATCH: 409,
|
|
312
313
|
CLAIM_EXPIRED: 409,
|
|
313
314
|
STORE_UNAVAILABLE: 503,
|
|
314
315
|
STORE_NOT_CONFIGURED: 500,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"initial-snapshot-D3Lve43o.d.ts","names":[],"sources":["../src/errors.ts","../src/initial-snapshot.ts"],"mappings":";;KAAY;KAcA,cACV;cAEW,gBAAgB;WAClB,MAAM;;WAEN;EAET,YACE,MAAM,aACN,iBACA;IAAY;IAAmB;;;;;;;;;;;cA6CtB,0BAA0B;EACrC,YAAY,iBAAiB;IAAY;;;;;KCrE/B,gBAAgB,KACxB,cAAc,wBAGd,QAAQ,cAAc"}
|