@requence/event-sourcing 1.4.1 → 1.5.1
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/README.md +6 -5
- package/dist/{chunk-24xjphpk.js → chunk-06anxtk6.js} +6 -6
- package/dist/{chunk-24xjphpk.js.map → chunk-06anxtk6.js.map} +2 -2
- package/dist/{chunk-263c07sn.js → chunk-1mr4cwpw.js} +8 -8
- package/dist/{chunk-263c07sn.js.map → chunk-1mr4cwpw.js.map} +2 -2
- package/dist/{chunk-3ejt4h7x.js → chunk-2wqpktzp.js} +78 -66
- package/dist/chunk-2wqpktzp.js.map +10 -0
- package/dist/{chunk-dgzw1nj9.js → chunk-467qtkth.js} +8 -3
- package/dist/chunk-467qtkth.js.map +10 -0
- package/dist/chunk-6dn6wxg7.js +11 -0
- package/dist/{chunk-jd4q8fcf.js.map → chunk-6dn6wxg7.js.map} +2 -2
- package/dist/{chunk-pc1q4se9.js → chunk-9qtjz725.js} +2 -2
- package/dist/{chunk-pc1q4se9.js.map → chunk-9qtjz725.js.map} +2 -2
- package/dist/{chunk-tfy01fah.js → chunk-c7sytwpp.js} +4 -4
- package/dist/{chunk-tfy01fah.js.map → chunk-c7sytwpp.js.map} +2 -2
- package/dist/{chunk-vvv14qxy.js → chunk-d6jqbrq2.js} +33 -31
- package/dist/{chunk-vvv14qxy.js.map → chunk-d6jqbrq2.js.map} +3 -3
- package/dist/{chunk-vep2byx4.js → chunk-f95c3da4.js} +77 -64
- package/dist/chunk-f95c3da4.js.map +10 -0
- package/dist/chunk-fpjnk143.js +13 -0
- package/dist/{chunk-6q8mr1sg.js.map → chunk-fpjnk143.js.map} +2 -2
- package/dist/{chunk-8qn1nqnh.js → chunk-fxb9fbvz.js} +9 -9
- package/dist/{chunk-8qn1nqnh.js.map → chunk-fxb9fbvz.js.map} +2 -2
- package/dist/chunk-hmvwn02w.js +22 -0
- package/dist/{chunk-6yrb30rx.js.map → chunk-hmvwn02w.js.map} +2 -2
- package/dist/{chunk-ty9bhdvx.js → chunk-j4mwjx07.js} +5 -3
- package/dist/chunk-j4mwjx07.js.map +10 -0
- package/dist/{chunk-391m4mfp.js → chunk-jw9e0fr0.js} +13 -13
- package/dist/{chunk-391m4mfp.js.map → chunk-jw9e0fr0.js.map} +2 -2
- package/dist/chunk-mbxxvskx.js +67 -0
- package/dist/chunk-mbxxvskx.js.map +10 -0
- package/dist/{chunk-yn2sjcg3.js → chunk-nrhw3kde.js} +57 -8
- package/dist/chunk-nrhw3kde.js.map +10 -0
- package/dist/{chunk-57b88rt5.js → chunk-pcggjrxr.js} +32 -32
- package/dist/{chunk-57b88rt5.js.map → chunk-pcggjrxr.js.map} +2 -2
- package/dist/chunk-tkwe4r0y.js +22 -0
- package/dist/{chunk-2fe8jjdc.js.map → chunk-tkwe4r0y.js.map} +2 -2
- package/dist/{chunk-z388cd25.js → chunk-zkz6ew8c.js} +5 -5
- package/dist/{chunk-z388cd25.js.map → chunk-zkz6ew8c.js.map} +2 -2
- package/dist/createAggregateRoot.d.ts +1 -1
- package/dist/createAggregateRoot.d.ts.map +1 -1
- package/dist/createAggregateRoot.js +15 -14
- package/dist/createAggregateRoot.js.map +1 -1
- package/dist/createCheckpointApi.d.ts +30 -0
- package/dist/createCheckpointApi.d.ts.map +1 -1
- package/dist/createCheckpointApi.js +5 -5
- package/dist/createCheckpointApi.js.map +1 -1
- package/dist/createEventListener.js +8 -8
- package/dist/createEventListener.js.map +1 -1
- package/dist/createEventStore.js +16 -16
- package/dist/createEventStore.js.map +1 -1
- package/dist/createProcessManager.d.ts.map +1 -1
- package/dist/createProcessManager.js +12 -12
- package/dist/createProcessManager.js.map +1 -1
- package/dist/createProjection.d.ts.map +1 -1
- package/dist/createProjection.js +12 -12
- package/dist/createProjection.js.map +1 -1
- package/dist/drizzle/index.js +132 -132
- package/dist/drizzle/index.js.map +2 -2
- package/dist/drizzle/postgres.js +14 -14
- package/dist/drizzle/postgres.js.map +1 -1
- package/dist/errors.js +24 -24
- package/dist/errors.js.map +1 -1
- package/dist/index.js +63 -63
- package/dist/index.js.map +1 -1
- package/dist/lock.d.ts +11 -3
- package/dist/lock.d.ts.map +1 -1
- package/dist/lock.js +4 -2
- package/dist/lock.js.map +1 -1
- package/dist/memory/index.d.ts +14 -3
- package/dist/memory/index.d.ts.map +1 -1
- package/dist/memory/index.js +52 -45
- package/dist/memory/index.js.map +3 -3
- package/dist/memory/index.spec.d.ts +2 -0
- package/dist/memory/index.spec.d.ts.map +1 -0
- package/dist/redis/index.js +5 -4
- package/dist/redis/index.js.map +1 -1
- package/dist/redis/redisLock.d.ts +1 -1
- package/dist/redis/redisLock.d.ts.map +1 -1
- package/dist/redis/redisLock.js +5 -4
- package/dist/redis/redisLock.js.map +1 -1
- package/dist/refresh.js +9 -9
- package/dist/refresh.js.map +1 -1
- package/dist/replay.js +9 -9
- package/dist/replay.js.map +1 -1
- package/dist/snapshotGenerator.js +2 -2
- package/dist/superjson.js +8 -8
- package/dist/superjson.js.map +1 -1
- package/dist/surreal/index.d.ts +35 -0
- package/dist/surreal/index.d.ts.map +1 -0
- package/dist/surreal/index.js +415 -0
- package/dist/surreal/index.js.map +10 -0
- package/dist/surreal/index.spec.d.ts +2 -0
- package/dist/surreal/index.spec.d.ts.map +1 -0
- package/dist/surreal/schema.d.ts +12 -0
- package/dist/surreal/schema.d.ts.map +1 -0
- package/dist/surreal/schema.js +12 -0
- package/dist/surreal/schema.js.map +9 -0
- package/dist/tests/projectionMultiInstance.spec.d.ts +2 -0
- package/dist/tests/projectionMultiInstance.spec.d.ts.map +1 -0
- package/dist/tests/storageDriverContract.d.ts +19 -0
- package/dist/tests/storageDriverContract.d.ts.map +1 -0
- package/dist/transaction.js +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/dist/wrappedEventBuilder.js +8 -8
- package/dist/wrappedEventBuilder.js.map +1 -1
- package/package.json +19 -10
- package/src/createAggregateRoot.ts +24 -5
- package/src/createCheckpointApi.ts +98 -0
- package/src/createProcessManager.ts +7 -6
- package/src/createProjection.ts +91 -65
- package/src/lock.ts +17 -3
- package/src/memory/index.spec.ts +4 -0
- package/src/memory/index.ts +55 -29
- package/src/redis/redisLock.ts +6 -2
- package/src/surreal/index.spec.ts +152 -0
- package/src/surreal/index.ts +604 -0
- package/src/surreal/schema.ts +68 -0
- package/src/tests/aggregateRoot.spec.ts +48 -3
- package/src/tests/eventStore.spec.ts +1 -3
- package/src/tests/processManagerMultiInstance.spec.ts +18 -7
- package/src/tests/projectionMultiInstance.spec.ts +283 -0
- package/src/tests/storageDriverContract.ts +612 -0
- package/dist/chunk-2fe8jjdc.js +0 -22
- package/dist/chunk-3ejt4h7x.js.map +0 -10
- package/dist/chunk-6q8mr1sg.js +0 -13
- package/dist/chunk-6yrb30rx.js +0 -22
- package/dist/chunk-dgzw1nj9.js.map +0 -10
- package/dist/chunk-jd4q8fcf.js +0 -11
- package/dist/chunk-ty9bhdvx.js.map +0 -10
- package/dist/chunk-vep2byx4.js.map +0 -10
- package/dist/chunk-yn2sjcg3.js.map +0 -10
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectionMultiInstance.spec.d.ts","sourceRoot":"","sources":["../../src/tests/projectionMultiInstance.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AggregateRootSnapshotMethods } from '../createAggregateRoot.ts';
|
|
2
|
+
import type { CheckpointMethods } from '../createCheckpointApi.ts';
|
|
3
|
+
import type { LoadEvents } from '../createEventStore.ts';
|
|
4
|
+
import type { ProjectionSnapshotMethods } from '../createProjection.ts';
|
|
5
|
+
import type { BaseInputEvent, BaseOutputEvent, MaybePromise, StreamId } from '../utilityTypes.ts';
|
|
6
|
+
export type StorageDriver = {
|
|
7
|
+
loadEvents: LoadEvents;
|
|
8
|
+
appendEvents: (streamId: StreamId, events: BaseInputEvent[], expectedVersion: number) => MaybePromise<BaseOutputEvent[]>;
|
|
9
|
+
checkpoint: CheckpointMethods;
|
|
10
|
+
projectionSnapshot: ProjectionSnapshotMethods;
|
|
11
|
+
aggregateRootSnapshot: AggregateRootSnapshotMethods;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Reusable contract suite exercising the raw storage methods every driver
|
|
15
|
+
* must supply to the core `createEventStore`. Call it once per driver with a
|
|
16
|
+
* factory producing a fresh, empty storage instance.
|
|
17
|
+
*/
|
|
18
|
+
export declare function runStorageDriverContract(name: string, makeStorage: () => MaybePromise<StorageDriver>): void;
|
|
19
|
+
//# sourceMappingURL=storageDriverContract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storageDriverContract.d.ts","sourceRoot":"","sources":["../../src/tests/storageDriverContract.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,4BAA4B,EAC7B,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAc,iBAAiB,EAAE,MAAM,2BAA2B,CAAA;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAEV,yBAAyB,EAC1B,MAAM,wBAAwB,CAAA;AAE/B,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,QAAQ,EACT,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,UAAU,CAAA;IACtB,YAAY,EAAE,CACZ,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,cAAc,EAAE,EACxB,eAAe,EAAE,MAAM,KACpB,YAAY,CAAC,eAAe,EAAE,CAAC,CAAA;IACpC,UAAU,EAAE,iBAAiB,CAAA;IAC7B,kBAAkB,EAAE,yBAAyB,CAAA;IAC7C,qBAAqB,EAAE,4BAA4B,CAAA;CACpD,CAAA;AA8CD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,YAAY,CAAC,aAAa,CAAC,QAghB/C"}
|
package/dist/transaction.js
CHANGED
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
2
|
+
normalizeArray2,
|
|
3
|
+
awaitAll2
|
|
4
|
+
} from "./chunk-c7sytwpp.js";
|
|
5
5
|
export {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
awaitAll2 as awaitAll,
|
|
7
|
+
normalizeArray2 as normalizeArray
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
//# debugId=
|
|
10
|
+
//# debugId=27F4432A2CD67A7364756E2164756E21
|
package/dist/utils.js.map
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
2
|
+
unwrapEvent2,
|
|
3
|
+
handleEventError2,
|
|
4
|
+
wrappedEventBuilder2
|
|
5
|
+
} from "./chunk-zkz6ew8c.js";
|
|
6
6
|
export {
|
|
7
|
-
|
|
8
|
-
unwrapEvent,
|
|
9
|
-
|
|
7
|
+
handleEventError2 as handleEventError,
|
|
8
|
+
unwrapEvent2 as unwrapEvent,
|
|
9
|
+
wrappedEventBuilder2 as wrappedEventBuilder
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
//# debugId=
|
|
12
|
+
//# debugId=13D61F145C1E7AE864756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@requence/event-sourcing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Event sourcing primitives — aggregate roots, projections, process managers, and event listeners",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -37,6 +37,12 @@
|
|
|
37
37
|
"types": "./dist/redis/index.d.ts",
|
|
38
38
|
"import": "./dist/redis/index.js",
|
|
39
39
|
"default": "./dist/redis/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./surreal": {
|
|
42
|
+
"bun": "./src/surreal/index.ts",
|
|
43
|
+
"types": "./dist/surreal/index.d.ts",
|
|
44
|
+
"import": "./dist/surreal/index.js",
|
|
45
|
+
"default": "./dist/surreal/index.js"
|
|
40
46
|
}
|
|
41
47
|
},
|
|
42
48
|
"files": [
|
|
@@ -49,6 +55,9 @@
|
|
|
49
55
|
"build": "bun run build.ts",
|
|
50
56
|
"test": "bun test",
|
|
51
57
|
"check-types": "tsc --noEmit",
|
|
58
|
+
"lint": "oxlint",
|
|
59
|
+
"format": "oxfmt",
|
|
60
|
+
"format:check": "oxfmt --check",
|
|
52
61
|
"cs": "changeset",
|
|
53
62
|
"version": "changeset version",
|
|
54
63
|
"release": "bun run build && changeset publish",
|
|
@@ -62,7 +71,8 @@
|
|
|
62
71
|
"peerDependencies": {
|
|
63
72
|
"zod": "^4.3.6",
|
|
64
73
|
"drizzle-orm": "^0.45.1",
|
|
65
|
-
"ioredis": "^5.9.3"
|
|
74
|
+
"ioredis": "^5.9.3",
|
|
75
|
+
"surrealdb": "^2.0.0"
|
|
66
76
|
},
|
|
67
77
|
"peerDependenciesMeta": {
|
|
68
78
|
"drizzle-orm": {
|
|
@@ -70,24 +80,23 @@
|
|
|
70
80
|
},
|
|
71
81
|
"ioredis": {
|
|
72
82
|
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"surrealdb": {
|
|
85
|
+
"optional": true
|
|
73
86
|
}
|
|
74
87
|
},
|
|
75
88
|
"devDependencies": {
|
|
76
89
|
"@changesets/changelog-github": "^0.6.0",
|
|
77
90
|
"@changesets/cli": "^2.31.0",
|
|
78
|
-
"@
|
|
79
|
-
"@stylistic/eslint-plugin": "^5.10.0",
|
|
91
|
+
"@surrealdb/node": "^3.0.3",
|
|
80
92
|
"@types/bun": "^1.3.13",
|
|
81
93
|
"@types/node": "^25.6.0",
|
|
82
94
|
"drizzle-orm": "^0.45.1",
|
|
83
|
-
"eslint": "^10.2.1",
|
|
84
|
-
"eslint-plugin-import": "^2.32.0",
|
|
85
|
-
"eslint-plugin-no-only-tests": "^3.3.0",
|
|
86
|
-
"globals": "^17.5.0",
|
|
87
95
|
"ioredis": "^5.10.1",
|
|
88
|
-
"
|
|
96
|
+
"oxfmt": "^0.59.0",
|
|
97
|
+
"oxlint": "^1.74.0",
|
|
98
|
+
"surrealdb": "^2.0.7",
|
|
89
99
|
"typescript": "^6.0.3",
|
|
90
|
-
"typescript-eslint": "^8.59.1",
|
|
91
100
|
"zod": "^4.3.6"
|
|
92
101
|
},
|
|
93
102
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
InfiniteLoopError,
|
|
18
18
|
ValidationError,
|
|
19
19
|
} from './errors.ts'
|
|
20
|
-
import type
|
|
20
|
+
import { DEFAULT_LOCK_TTL, type LockCreator } from './lock.ts'
|
|
21
21
|
import { isRefreshing } from './refresh.ts'
|
|
22
22
|
import createSnapshotGenerator, {
|
|
23
23
|
type CompleteSnapshotGenerator,
|
|
@@ -530,6 +530,7 @@ export function createAggregateRoot<Name extends string>(type: Name) {
|
|
|
530
530
|
let downstreamSettledPromise = () => Promise.resolve()
|
|
531
531
|
let releaseLock!: () => Promise<void>
|
|
532
532
|
let extendLock!: EventHandlerOptions['extendLock']
|
|
533
|
+
let lockTtl: number | undefined
|
|
533
534
|
|
|
534
535
|
const warnReleaseLock = () => {
|
|
535
536
|
if (releasedAfterChain) {
|
|
@@ -582,8 +583,25 @@ export function createAggregateRoot<Name extends string>(type: Name) {
|
|
|
582
583
|
let commandChain = createLock([type, streamId]).then((lock) => {
|
|
583
584
|
releaseLock = lock.release
|
|
584
585
|
extendLock = lock.extend
|
|
586
|
+
lockTtl = lock.ttl
|
|
585
587
|
return [] as Error | BaseInputEvent[]
|
|
586
588
|
})
|
|
589
|
+
|
|
590
|
+
// A replay is the library's own work and takes as long as the stream is
|
|
591
|
+
// long, so unlike a slow command it must not cost the caller the lock.
|
|
592
|
+
// Extended on a timer while it runs, and never past it: a lock nobody
|
|
593
|
+
// releases still expires.
|
|
594
|
+
const keepLockAlive = async <T>(work: () => Promise<T>) => {
|
|
595
|
+
const timer = setInterval(
|
|
596
|
+
() => void extendLock().catch(() => false),
|
|
597
|
+
(lockTtl ?? DEFAULT_LOCK_TTL) / 4,
|
|
598
|
+
)
|
|
599
|
+
try {
|
|
600
|
+
return await work()
|
|
601
|
+
} finally {
|
|
602
|
+
clearInterval(timer)
|
|
603
|
+
}
|
|
604
|
+
}
|
|
587
605
|
let commandChainTouched = false
|
|
588
606
|
let releasedAfterChain = false
|
|
589
607
|
let transactionDelay: TransactionDelay | undefined
|
|
@@ -616,9 +634,8 @@ export function createAggregateRoot<Name extends string>(type: Name) {
|
|
|
616
634
|
return previousEvents
|
|
617
635
|
}
|
|
618
636
|
const events = (
|
|
619
|
-
await normalizeArray(
|
|
620
|
-
|
|
621
|
-
() => extendLock(),
|
|
637
|
+
await normalizeArray(commandFn.apply(rawCommands, args), () =>
|
|
638
|
+
extendLock(),
|
|
622
639
|
)
|
|
623
640
|
).filter((event) => Boolean(event))
|
|
624
641
|
|
|
@@ -770,7 +787,9 @@ export function createAggregateRoot<Name extends string>(type: Name) {
|
|
|
770
787
|
// ensure commands are executed after load is finished
|
|
771
788
|
commandChain = commandChain
|
|
772
789
|
.then(() =>
|
|
773
|
-
|
|
790
|
+
keepLockAlive(() =>
|
|
791
|
+
loadUntil(state, snapshotApi, streamId, undefined, filter),
|
|
792
|
+
),
|
|
774
793
|
)
|
|
775
794
|
.then((loadedVersion) => {
|
|
776
795
|
version = loadedVersion
|
|
@@ -18,6 +18,14 @@ const checkpointSchema = z.object({
|
|
|
18
18
|
|
|
19
19
|
export type Checkpoint = z.output<typeof checkpointSchema>
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The outcome of {@link CheckpointApi.claim}: either this instance owns the
|
|
23
|
+
* event and must run its work, or another instance already has it.
|
|
24
|
+
*/
|
|
25
|
+
export type ClaimResult =
|
|
26
|
+
| { claimed: false }
|
|
27
|
+
| { claimed: true; release: () => Promise<void> }
|
|
28
|
+
|
|
21
29
|
export type CheckpointMethods = {
|
|
22
30
|
get: (
|
|
23
31
|
type: 'projection' | 'processManager',
|
|
@@ -57,12 +65,16 @@ export function createCheckpointApi(
|
|
|
57
65
|
// every load/upsert so optimistic-concurrency writes can detect when another
|
|
58
66
|
// instance has advanced the same checkpoint.
|
|
59
67
|
let version: number | null = null
|
|
68
|
+
// The position that goes with `version`. Only {@link CheckpointApi.claim}
|
|
69
|
+
// needs it, to put the cursor back if the work it claimed then fails.
|
|
70
|
+
let position: number | null = null
|
|
60
71
|
|
|
61
72
|
const api = {
|
|
62
73
|
relatedStreamEvents: params.relatedStreamEvents,
|
|
63
74
|
async clear() {
|
|
64
75
|
await params.delete(params.type, params.name)
|
|
65
76
|
version = null
|
|
77
|
+
position = null
|
|
66
78
|
},
|
|
67
79
|
/**
|
|
68
80
|
* Load the persisted checkpoint and sync the local version. Returns the
|
|
@@ -72,8 +84,93 @@ export function createCheckpointApi(
|
|
|
72
84
|
async load() {
|
|
73
85
|
const checkpoint = await params.get(params.type, params.name)
|
|
74
86
|
version = checkpoint?.version ?? null
|
|
87
|
+
position = checkpoint?.lastEventPosition ?? null
|
|
75
88
|
return checkpoint ?? null
|
|
76
89
|
},
|
|
90
|
+
/**
|
|
91
|
+
* Takes ownership of `event` *before* its work runs, so that exactly one
|
|
92
|
+
* instance does that work.
|
|
93
|
+
*
|
|
94
|
+
* Every instance runs its own copy of a projection against shared storage,
|
|
95
|
+
* and each decides what it has already applied from a cursor only it can
|
|
96
|
+
* see. Two instances catching up over the same backlog — or one catching up
|
|
97
|
+
* while the other applies an event it just appended — therefore both reach
|
|
98
|
+
* the same handler. Writing the cursor first, through the same
|
|
99
|
+
* compare-and-swap the ordinary write uses, is what breaks the tie: the
|
|
100
|
+
* loser is told so and skips the event instead of repeating its side
|
|
101
|
+
* effects.
|
|
102
|
+
*
|
|
103
|
+
* Returns `{ claimed: false }` when another writer already covers this
|
|
104
|
+
* position. On success the caller gets a `release` to call if the work then
|
|
105
|
+
* fails, which puts the cursor back so the event is picked up again rather
|
|
106
|
+
* than silently skipped — without it, claiming would turn every failed
|
|
107
|
+
* handler into a lost event.
|
|
108
|
+
*/
|
|
109
|
+
async claim(
|
|
110
|
+
event: BaseOutputEvent,
|
|
111
|
+
metadata: Record<string, any> = {},
|
|
112
|
+
): Promise<ClaimResult> {
|
|
113
|
+
while (true) {
|
|
114
|
+
const expectedVersion = version
|
|
115
|
+
const previousPosition = position
|
|
116
|
+
const nextVersion = (expectedVersion ?? -1) + 1
|
|
117
|
+
|
|
118
|
+
const committed = await params.upsert(
|
|
119
|
+
{
|
|
120
|
+
lastEventPosition: event.position,
|
|
121
|
+
type: params.type,
|
|
122
|
+
name: params.name,
|
|
123
|
+
metadata,
|
|
124
|
+
version: nextVersion,
|
|
125
|
+
},
|
|
126
|
+
expectedVersion,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if (committed) {
|
|
130
|
+
version = nextVersion
|
|
131
|
+
position = event.position
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
claimed: true,
|
|
135
|
+
async release() {
|
|
136
|
+
// Only undo a claim that is still the newest write. Once anything
|
|
137
|
+
// else has moved the cursor, rewinding it would drop that work.
|
|
138
|
+
if (version !== nextVersion) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (previousPosition === null) {
|
|
143
|
+
await api.clear()
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const restored = await params.upsert(
|
|
148
|
+
{
|
|
149
|
+
lastEventPosition: previousPosition,
|
|
150
|
+
type: params.type,
|
|
151
|
+
name: params.name,
|
|
152
|
+
metadata,
|
|
153
|
+
version: nextVersion + 1,
|
|
154
|
+
},
|
|
155
|
+
nextVersion,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if (restored) {
|
|
159
|
+
version = nextVersion + 1
|
|
160
|
+
position = previousPosition
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Another writer advanced this checkpoint. Refresh our view of it.
|
|
167
|
+
const current = await api.load()
|
|
168
|
+
|
|
169
|
+
if (current && current.lastEventPosition >= event.position) {
|
|
170
|
+
return { claimed: false }
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
77
174
|
/**
|
|
78
175
|
* Persist the checkpoint position (and optional metadata) for the given
|
|
79
176
|
* event.
|
|
@@ -108,6 +205,7 @@ export function createCheckpointApi(
|
|
|
108
205
|
|
|
109
206
|
if (committed) {
|
|
110
207
|
version = nextVersion
|
|
208
|
+
position = event.position
|
|
111
209
|
return
|
|
112
210
|
}
|
|
113
211
|
|
|
@@ -293,7 +293,9 @@ export function buildProcessManagerCreator(params: {
|
|
|
293
293
|
await hydrated
|
|
294
294
|
},
|
|
295
295
|
async beginSession() {
|
|
296
|
-
if (!hasState)
|
|
296
|
+
if (!hasState) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
297
299
|
// Load through the checkpoint API so the local version is synced with
|
|
298
300
|
// the store; the subsequent fold relies on it for compare-and-swap.
|
|
299
301
|
const checkpoint = await checkpointApi.load()
|
|
@@ -310,7 +312,9 @@ export function buildProcessManagerCreator(params: {
|
|
|
310
312
|
sessionActive = true
|
|
311
313
|
},
|
|
312
314
|
endSession() {
|
|
313
|
-
if (!hasState)
|
|
315
|
+
if (!hasState) {
|
|
316
|
+
return
|
|
317
|
+
}
|
|
314
318
|
state = null
|
|
315
319
|
sessionActive = false
|
|
316
320
|
},
|
|
@@ -379,10 +383,7 @@ export function buildProcessManagerCreator(params: {
|
|
|
379
383
|
async state() {
|
|
380
384
|
await hydrated
|
|
381
385
|
if (!sessionActive && hasState) {
|
|
382
|
-
const checkpoint = await params.checkpoint.get(
|
|
383
|
-
'processManager',
|
|
384
|
-
name,
|
|
385
|
-
)
|
|
386
|
+
const checkpoint = await params.checkpoint.get('processManager', name)
|
|
386
387
|
return checkpoint?.metadata?.state ?? clone(initialState)
|
|
387
388
|
}
|
|
388
389
|
return state
|
package/src/createProjection.ts
CHANGED
|
@@ -266,86 +266,112 @@ export function buildProjectionCreator(params: {
|
|
|
266
266
|
|
|
267
267
|
const runner = (async () => {
|
|
268
268
|
await unlocked
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
269
|
+
|
|
270
|
+
// Take ownership of the event before anything observable happens, so a
|
|
271
|
+
// peer applying the same event does not apply it twice. `claimed` is
|
|
272
|
+
// null while replaying: a replay deliberately re-applies everything and
|
|
273
|
+
// `beginReplay` leaves the stored cursor where it was, so a claim would
|
|
274
|
+
// read that not-yet-rewound position and skip the whole run.
|
|
275
|
+
const claimed = isReplaying() ? null : await checkpointApi.claim(event)
|
|
276
|
+
|
|
277
|
+
if (claimed && !claimed.claimed) {
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const release = claimed?.claimed ? claimed.release : null
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await withProjectionInfo({ name, event }, async () => {
|
|
285
|
+
try {
|
|
286
|
+
await handler?.(event, {
|
|
287
|
+
replaying,
|
|
288
|
+
async replayUntil(position, filterEvents) {
|
|
289
|
+
if (position >= event.position) {
|
|
290
|
+
throw new ProjectionReplayLoopError(
|
|
291
|
+
`Event handler on${event.type} of projection ${name} tried to replay to the future`,
|
|
292
|
+
{ name, eventHandler: `on${event.type}` },
|
|
293
|
+
event,
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
locks.delete(event.streamId)
|
|
298
|
+
return api.replayUntil(event.streamId, position, filterEvents)
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error instanceof ReplaySkipError) {
|
|
303
|
+
if (isReplaying()) {
|
|
304
|
+
return
|
|
280
305
|
}
|
|
281
306
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
})
|
|
286
|
-
} catch (error) {
|
|
287
|
-
if (error instanceof ReplaySkipError) {
|
|
288
|
-
if (isReplaying()) {
|
|
289
|
-
return
|
|
307
|
+
throw new Error(
|
|
308
|
+
'ReplaySkipError should never be thrown outside replay',
|
|
309
|
+
)
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
|
|
293
|
-
|
|
312
|
+
if (
|
|
313
|
+
!(error instanceof Error) ||
|
|
314
|
+
error instanceof ProjectionReplayLoopError
|
|
315
|
+
) {
|
|
316
|
+
throw error
|
|
317
|
+
}
|
|
318
|
+
throw new ProjectionEventHandlerExecutionError(
|
|
319
|
+
name,
|
|
320
|
+
`on${event.type}`,
|
|
321
|
+
event,
|
|
322
|
+
error,
|
|
294
323
|
)
|
|
295
324
|
}
|
|
325
|
+
try {
|
|
326
|
+
await globalEventHandler?.(event)
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (!(error instanceof Error)) {
|
|
329
|
+
throw error
|
|
330
|
+
}
|
|
296
331
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
throw new ProjectionEventHandlerExecutionError(
|
|
304
|
-
name,
|
|
305
|
-
`on${event.type}`,
|
|
306
|
-
event,
|
|
307
|
-
error,
|
|
308
|
-
)
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
await globalEventHandler?.(event)
|
|
312
|
-
} catch (error) {
|
|
313
|
-
if (!(error instanceof Error)) {
|
|
314
|
-
throw error
|
|
332
|
+
throw new ProjectionEventHandlerExecutionError(
|
|
333
|
+
name,
|
|
334
|
+
'GLOBAL',
|
|
335
|
+
event,
|
|
336
|
+
error,
|
|
337
|
+
)
|
|
315
338
|
}
|
|
339
|
+
})
|
|
316
340
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
341
|
+
if (params.snapshot && snapshotting) {
|
|
342
|
+
const appliedEvents = await params.snapshot.incrementAppliedCount({
|
|
343
|
+
projection: name,
|
|
344
|
+
id: event.streamId,
|
|
345
|
+
})
|
|
346
|
+
const snapshot = await snapshotting.internalCapture(
|
|
320
347
|
event,
|
|
321
|
-
|
|
348
|
+
appliedEvents,
|
|
322
349
|
)
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
if (params.snapshot && snapshotting) {
|
|
327
|
-
const appliedEvents = await params.snapshot.incrementAppliedCount({
|
|
328
|
-
projection: name,
|
|
329
|
-
id: event.streamId,
|
|
330
|
-
})
|
|
331
|
-
const snapshot = await snapshotting.internalCapture(
|
|
332
|
-
event,
|
|
333
|
-
appliedEvents,
|
|
334
|
-
)
|
|
335
350
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
351
|
+
if (snapshot) {
|
|
352
|
+
await params.snapshot.put({
|
|
353
|
+
projectionType: name,
|
|
354
|
+
projectionId: event.streamId,
|
|
355
|
+
lastEventPosition: event.position,
|
|
356
|
+
state: snapshot.state,
|
|
357
|
+
schemaVersion: snapshot.version,
|
|
358
|
+
createdAt: new Date(),
|
|
359
|
+
})
|
|
360
|
+
}
|
|
345
361
|
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// The cursor was moved before this work ran, so it has to go back or
|
|
364
|
+
// the event would never be retried — a claim must not turn a failing
|
|
365
|
+
// handler into a silently skipped one.
|
|
366
|
+
await release?.()
|
|
367
|
+
throw error
|
|
346
368
|
}
|
|
347
369
|
|
|
348
|
-
|
|
370
|
+
// A claim already wrote the cursor. A replay did not take one, so it
|
|
371
|
+
// still advances the cursor the ordinary way.
|
|
372
|
+
if (!release) {
|
|
373
|
+
await checkpointApi.upsert(event)
|
|
374
|
+
}
|
|
349
375
|
})()
|
|
350
376
|
|
|
351
377
|
if (exclusive) {
|
package/src/lock.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex'
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export const DEFAULT_LOCK_TTL = 5000
|
|
4
|
+
|
|
5
|
+
export type Lock = {
|
|
6
|
+
/** Moves the expiry to `ttl` from now (default: half the lock's TTL). */
|
|
7
|
+
extend(ttl?: number): Promise<boolean>
|
|
8
|
+
release(): Promise<void>
|
|
9
|
+
/**
|
|
10
|
+
* The TTL the lock was taken with. The aggregate root keeps a replay's lock
|
|
11
|
+
* alive at a quarter of it, and assumes {@link DEFAULT_LOCK_TTL} without it.
|
|
12
|
+
*/
|
|
13
|
+
ttl?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type LockCreator = (key: string | string[]) => Promise<Lock>
|
|
17
|
+
|
|
18
|
+
export default function lock(defaultTtl = DEFAULT_LOCK_TTL): LockCreator {
|
|
4
19
|
const locks = new Map<string, Mutex>()
|
|
5
20
|
return async (key: string | string[]) => {
|
|
6
21
|
const k = Array.isArray(key) ? key.join(':') : key
|
|
@@ -30,6 +45,7 @@ export default function lock(defaultTtl = 5000) {
|
|
|
30
45
|
startTimer(defaultTtl)
|
|
31
46
|
|
|
32
47
|
return {
|
|
48
|
+
ttl: defaultTtl,
|
|
33
49
|
async extend(ttl?: number) {
|
|
34
50
|
if (isReleased) {
|
|
35
51
|
return false
|
|
@@ -54,5 +70,3 @@ export default function lock(defaultTtl = 5000) {
|
|
|
54
70
|
}
|
|
55
71
|
}
|
|
56
72
|
}
|
|
57
|
-
|
|
58
|
-
export type LockCreator = ReturnType<typeof lock>
|