@ddd-ts/event-sourcing-firestore 0.0.38 → 0.0.39
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/dist/projection/event-coordinator.d.ts +16 -0
- package/dist/projection/event-coordinator.d.ts.map +1 -0
- package/dist/projection/event-coordinator.js +47 -0
- package/dist/projection/event-coordinator.mjs +47 -0
- package/dist/projection/firestore.projector.d.ts +28 -30
- package/dist/projection/firestore.projector.d.ts.map +1 -1
- package/dist/projection/firestore.projector.js +77 -23
- package/dist/projection/firestore.projector.mjs +78 -24
- package/dist/utils/promise-with-resolvers.d.ts +7 -0
- package/dist/utils/promise-with-resolvers.d.ts.map +1 -0
- package/dist/utils/promise-with-resolvers.js +17 -0
- package/dist/utils/promise-with-resolvers.mjs +16 -0
- package/package.json +10 -10
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IEsEvent, ISavedChange } from "@ddd-ts/core";
|
|
2
|
+
export declare class EventCoordinator {
|
|
3
|
+
private eventProcessing;
|
|
4
|
+
private currentEventId;
|
|
5
|
+
private lastEvent;
|
|
6
|
+
private isRunning;
|
|
7
|
+
private _onEmpty;
|
|
8
|
+
addEvent(event: ISavedChange<IEsEvent>): void;
|
|
9
|
+
start(event: ISavedChange<IEsEvent>): () => void;
|
|
10
|
+
waitCurrentEvent(): Promise<void>;
|
|
11
|
+
cleanEvent(event: ISavedChange<IEsEvent>): void;
|
|
12
|
+
canProceed(event: ISavedChange<IEsEvent>): boolean;
|
|
13
|
+
onEmpty(callback: () => void): void;
|
|
14
|
+
checkEmpty(): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=event-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-coordinator.d.ts","sourceRoot":"","sources":["../../src/projection/event-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG3D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,SAAS,CAAuC;IACxD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAY;IAE5B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAYtC,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAU7B,gBAAgB;IAMtB,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAYxC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAMxC,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI;IAI5B,UAAU;CAKX"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const require_promise_with_resolvers = require('../utils/promise-with-resolvers.js');
|
|
2
|
+
|
|
3
|
+
//#region src/projection/event-coordinator.ts
|
|
4
|
+
var EventCoordinator = class {
|
|
5
|
+
eventProcessing = /* @__PURE__ */ new Map();
|
|
6
|
+
currentEventId = null;
|
|
7
|
+
lastEvent = null;
|
|
8
|
+
isRunning = false;
|
|
9
|
+
_onEmpty = () => {};
|
|
10
|
+
addEvent(event) {
|
|
11
|
+
const eventId = event.id.serialize();
|
|
12
|
+
this.eventProcessing.set(eventId, require_promise_with_resolvers.promiseWithResolvers());
|
|
13
|
+
if (this.lastEvent === null || this.lastEvent.revision < event.revision) this.lastEvent = event;
|
|
14
|
+
}
|
|
15
|
+
start(event) {
|
|
16
|
+
if (this.isRunning) throw new Error("Event processing already in progress");
|
|
17
|
+
this.currentEventId = event.id.serialize();
|
|
18
|
+
this.isRunning = true;
|
|
19
|
+
return () => this.cleanEvent(event);
|
|
20
|
+
}
|
|
21
|
+
async waitCurrentEvent() {
|
|
22
|
+
if (!this.currentEventId) return;
|
|
23
|
+
await this.eventProcessing.get(this.currentEventId)?.promise;
|
|
24
|
+
}
|
|
25
|
+
cleanEvent(event) {
|
|
26
|
+
const eventId = event.id.serialize();
|
|
27
|
+
this.eventProcessing.get(eventId)?.resolve();
|
|
28
|
+
this.eventProcessing.delete(eventId);
|
|
29
|
+
this.isRunning = false;
|
|
30
|
+
this.currentEventId = null;
|
|
31
|
+
this.checkEmpty();
|
|
32
|
+
}
|
|
33
|
+
canProceed(event) {
|
|
34
|
+
if (this.isRunning) return false;
|
|
35
|
+
const eventId = event.id.serialize();
|
|
36
|
+
return this.lastEvent?.id.serialize() === eventId;
|
|
37
|
+
}
|
|
38
|
+
onEmpty(callback) {
|
|
39
|
+
this._onEmpty = callback;
|
|
40
|
+
}
|
|
41
|
+
checkEmpty() {
|
|
42
|
+
if (this.eventProcessing.size === 0) this._onEmpty();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
exports.EventCoordinator = EventCoordinator;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { promiseWithResolvers } from "../utils/promise-with-resolvers.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/projection/event-coordinator.ts
|
|
4
|
+
var EventCoordinator = class {
|
|
5
|
+
eventProcessing = /* @__PURE__ */ new Map();
|
|
6
|
+
currentEventId = null;
|
|
7
|
+
lastEvent = null;
|
|
8
|
+
isRunning = false;
|
|
9
|
+
_onEmpty = () => {};
|
|
10
|
+
addEvent(event) {
|
|
11
|
+
const eventId = event.id.serialize();
|
|
12
|
+
this.eventProcessing.set(eventId, promiseWithResolvers());
|
|
13
|
+
if (this.lastEvent === null || this.lastEvent.revision < event.revision) this.lastEvent = event;
|
|
14
|
+
}
|
|
15
|
+
start(event) {
|
|
16
|
+
if (this.isRunning) throw new Error("Event processing already in progress");
|
|
17
|
+
this.currentEventId = event.id.serialize();
|
|
18
|
+
this.isRunning = true;
|
|
19
|
+
return () => this.cleanEvent(event);
|
|
20
|
+
}
|
|
21
|
+
async waitCurrentEvent() {
|
|
22
|
+
if (!this.currentEventId) return;
|
|
23
|
+
await this.eventProcessing.get(this.currentEventId)?.promise;
|
|
24
|
+
}
|
|
25
|
+
cleanEvent(event) {
|
|
26
|
+
const eventId = event.id.serialize();
|
|
27
|
+
this.eventProcessing.get(eventId)?.resolve();
|
|
28
|
+
this.eventProcessing.delete(eventId);
|
|
29
|
+
this.isRunning = false;
|
|
30
|
+
this.currentEventId = null;
|
|
31
|
+
this.checkEmpty();
|
|
32
|
+
}
|
|
33
|
+
canProceed(event) {
|
|
34
|
+
if (this.isRunning) return false;
|
|
35
|
+
const eventId = event.id.serialize();
|
|
36
|
+
return this.lastEvent?.id.serialize() === eventId;
|
|
37
|
+
}
|
|
38
|
+
onEmpty(callback) {
|
|
39
|
+
this._onEmpty = callback;
|
|
40
|
+
}
|
|
41
|
+
checkEmpty() {
|
|
42
|
+
if (this.eventProcessing.size === 0) this._onEmpty();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { EventCoordinator };
|
|
@@ -1,39 +1,30 @@
|
|
|
1
1
|
import { Firestore, WriteBatch } from "firebase-admin/firestore";
|
|
2
2
|
import { type IEsEvent, type ISavedChange, EventId, ProjectedStreamReader, Cursor, CheckpointId, ESProjection, type IFact, type Serialized, Lock } from "@ddd-ts/core";
|
|
3
|
-
import { MicrosecondTimestamp } from "@ddd-ts/shape";
|
|
3
|
+
import { Mapping, MicrosecondTimestamp } from "@ddd-ts/shape";
|
|
4
4
|
import { DefaultConverter, FirestoreTransaction } from "@ddd-ts/store-firestore";
|
|
5
|
+
interface FirestoreProjectorConfig {
|
|
6
|
+
retry: {
|
|
7
|
+
attempts: number;
|
|
8
|
+
minDelay: number;
|
|
9
|
+
maxDelay: number;
|
|
10
|
+
backoff: number;
|
|
11
|
+
};
|
|
12
|
+
enqueue: {
|
|
13
|
+
batchSize: number;
|
|
14
|
+
};
|
|
15
|
+
onProcessError: (error: Error) => void;
|
|
16
|
+
onEnqueueError: (error: Error) => void;
|
|
17
|
+
}
|
|
5
18
|
export declare class FirestoreProjector {
|
|
6
19
|
readonly projection: ESProjection<IEsEvent>;
|
|
7
20
|
readonly reader: ProjectedStreamReader<IEsEvent>;
|
|
8
21
|
readonly queue: FirestoreQueueStore;
|
|
9
|
-
config:
|
|
10
|
-
retry: {
|
|
11
|
-
attempts: number;
|
|
12
|
-
minDelay: number;
|
|
13
|
-
maxDelay: number;
|
|
14
|
-
backoff: number;
|
|
15
|
-
};
|
|
16
|
-
enqueue: {
|
|
17
|
-
batchSize: number;
|
|
18
|
-
};
|
|
19
|
-
onProcessError: (error: Error) => void;
|
|
20
|
-
onEnqueueError: (error: Error) => void;
|
|
21
|
-
};
|
|
22
|
+
config: FirestoreProjectorConfig;
|
|
22
23
|
_unclaim: boolean;
|
|
23
|
-
constructor(projection: ESProjection<IEsEvent>, reader: ProjectedStreamReader<IEsEvent>, queue: FirestoreQueueStore, config?:
|
|
24
|
-
retry: {
|
|
25
|
-
attempts: number;
|
|
26
|
-
minDelay: number;
|
|
27
|
-
maxDelay: number;
|
|
28
|
-
backoff: number;
|
|
29
|
-
};
|
|
30
|
-
enqueue: {
|
|
31
|
-
batchSize: number;
|
|
32
|
-
};
|
|
33
|
-
onProcessError: (error: Error) => void;
|
|
34
|
-
onEnqueueError: (error: Error) => void;
|
|
35
|
-
});
|
|
24
|
+
constructor(projection: ESProjection<IEsEvent>, reader: ProjectedStreamReader<IEsEvent>, queue: FirestoreQueueStore, config?: FirestoreProjectorConfig);
|
|
36
25
|
breathe(): AsyncGenerator<readonly [number, () => void], void, unknown>;
|
|
26
|
+
private eventCoordinators;
|
|
27
|
+
private getEventCoordinator;
|
|
37
28
|
handle(savedChange: ISavedChange<IEsEvent>): Promise<void>;
|
|
38
29
|
private getCursor;
|
|
39
30
|
private attempt;
|
|
@@ -45,6 +36,7 @@ export declare class FirestoreProjector {
|
|
|
45
36
|
private getUnprocessed;
|
|
46
37
|
private claimTasks;
|
|
47
38
|
private processEvents;
|
|
39
|
+
private assertBeforeInsert;
|
|
48
40
|
}
|
|
49
41
|
export declare class AlreadyEnqueuedError extends Error {
|
|
50
42
|
constructor();
|
|
@@ -72,7 +64,7 @@ export declare class FirestoreQueueStore {
|
|
|
72
64
|
checkpoint(id: CheckpointId): FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
73
65
|
queue(id: CheckpointId): FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
74
66
|
queued(id: CheckpointId, eventId: EventId): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
75
|
-
processed(id: CheckpointId, eventIds: EventId[], context?: {
|
|
67
|
+
processed(claimerId: ClaimerId, id: CheckpointId, eventIds: EventId[], context?: {
|
|
76
68
|
transaction?: FirestoreTransaction;
|
|
77
69
|
batchWriter?: WriteBatch;
|
|
78
70
|
}): Promise<void>;
|
|
@@ -100,8 +92,13 @@ declare const Task_base: import("@ddd-ts/shape").IDict<{
|
|
|
100
92
|
readonly revision: NumberConstructor;
|
|
101
93
|
readonly attempts: NumberConstructor;
|
|
102
94
|
readonly processed: BooleanConstructor;
|
|
103
|
-
readonly claimer: import("@ddd-ts/shape").IOptional<StringConstructor, typeof import("@ddd-ts/shape").Empty>;
|
|
104
|
-
readonly claimedAt: import("@ddd-ts/shape").IOptional<typeof MicrosecondTimestamp, typeof import("@ddd-ts/shape").Empty>;
|
|
95
|
+
/** @deprecated */ readonly claimer: import("@ddd-ts/shape").IOptional<StringConstructor, typeof import("@ddd-ts/shape").Empty>;
|
|
96
|
+
/** @deprecated */ readonly claimedAt: import("@ddd-ts/shape").IOptional<typeof MicrosecondTimestamp, typeof import("@ddd-ts/shape").Empty>;
|
|
97
|
+
readonly claimsMetadata: Mapping<[{
|
|
98
|
+
claimedAt: typeof MicrosecondTimestamp;
|
|
99
|
+
processedAt: import("@ddd-ts/shape").IOptional<typeof MicrosecondTimestamp, typeof import("@ddd-ts/shape").Empty>;
|
|
100
|
+
}], typeof import("@ddd-ts/shape").Empty>;
|
|
101
|
+
readonly claimIds: [StringConstructor];
|
|
105
102
|
readonly lock: typeof Lock;
|
|
106
103
|
readonly skipAfter: NumberConstructor;
|
|
107
104
|
readonly remaining: NumberConstructor;
|
|
@@ -118,6 +115,7 @@ export declare class Task<Stored extends boolean> extends Task_base {
|
|
|
118
115
|
skipAfter: number;
|
|
119
116
|
isolateAfter: number;
|
|
120
117
|
}): Task<false>;
|
|
118
|
+
get currentClaimId(): string | undefined;
|
|
121
119
|
get isProcessing(): boolean;
|
|
122
120
|
get isProcessed(): boolean;
|
|
123
121
|
get shouldSkip(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EAET,UAAU,EACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,OAAO,EACP,qBAAqB,EACrB,MAAM,EACN,YAAY,EACZ,YAAY,EAEZ,KAAK,KAAK,EACV,KAAK,UAAU,EACf,IAAI,EACL,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAmB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,EAET,UAAU,EACX,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,OAAO,EACP,qBAAqB,EACrB,MAAM,EACN,YAAY,EACZ,YAAY,EAEZ,KAAK,KAAK,EACV,KAAK,UAAU,EACf,IAAI,EACL,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAmB,MAAM,eAAe,CAAC;AAC/E,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AAqBjC,UAAU,wBAAwB;IAChC,KAAK,EAAE;QACL,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,OAAO,EAAE;QACP,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACvC,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CACxC;AAED,qBAAa,kBAAkB;aAIX,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC;aAClC,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC;aACvC,KAAK,EAAE,mBAAmB;IACnC,MAAM,EAAE,wBAAwB;IANzC,QAAQ,UAAQ;gBAGE,UAAU,EAAE,YAAY,CAAC,QAAQ,CAAC,EAClC,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC,EACvC,KAAK,EAAE,mBAAmB,EACnC,MAAM,GAAE,wBASd;IAGI,OAAO;IAoBd,OAAO,CAAC,iBAAiB,CAA4C;IACrE,OAAO,CAAC,mBAAmB;IAmBrB,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,QAAQ,CAAC;YAuDlC,SAAS;YAKT,OAAO;YAkEP,YAAY;YAKZ,gBAAgB;YAahB,OAAO;YAqBP,UAAU;YAYV,gBAAgB;YAKhB,cAAc;YAKd,UAAU;YAcV,aAAa;YAgDb,kBAAkB;CAmBjC;AAED,qBAAa,oBAAqB,SAAQ,KAAK;;CAK9C;AAED,qBAAa,mBAAmB;IAIX,EAAE,EAAE,SAAS;IAHhC,SAAS,mDAA0B;IACnC,UAAU,EAAE,iBAAiB,CAAC,mBAAmB,CAAC;gBAE/B,EAAE,EAAE,SAAS;IAMhC,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,uBAAuB;IAQzB,OAAO,CAAC,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE;IA0BxD,KAAK,CACT,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,SAAS,EAClB,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;IA8Bf,IAAI,CAAC,YAAY,EAAE,YAAY;IAyB/B,WAAW,CAAC,YAAY,EAAE,YAAY;IAmDtC,OAAO,CACX,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,SAAS,GACjB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAkBlB,OAAO,CAAC,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;IAmB7D;;;;;OAKG;IACG,WAAW,CAAC,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM;IAgB5D,UAAU,CAAC,EAAE,EAAE,YAAY;IAI3B,KAAK,CAAC,EAAE,EAAE,YAAY;IAItB,MAAM,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO;IAInC,SAAS,CACb,SAAS,EAAE,SAAS,EACpB,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,GAAE;QACP,WAAW,CAAC,EAAE,oBAAoB,CAAC;QACnC,WAAW,CAAC,EAAE,UAAU,CAAC;KACrB;IA2BF,aAAa,CAAC,EAAE,EAAE,YAAY;IA0B9B,OAAO,CAAC,EAAE,EAAE,YAAY;IA6BxB,KAAK,CAAC,EAAE,EAAE,YAAY;IAO5B;;;;;;;;;OASG;IACG,IAAI,CAAC,YAAY,EAAE,YAAY;CAsCtC;AAED,qBAAa,SAAU,SAAQ,OAAO;CAAG;;;;;;;;IAQvC,kBAAkB;IAClB,kBAAkB;;;;;;;;;;;;;AARpB,qBAAa,IAAI,CAAC,MAAM,SAAS,OAAO,CAAE,SAAQ,SAoBhD;IACQ,cAAc,EAAE,MAAM,SAAS,IAAI,GACvC,oBAAoB,GACpB,SAAS,CAAC;IAEd,IAAI,MAAM,WAOT;IAED,MAAM,CAAC,GAAG,CACR,IAAI,EAAE,KAAK,EACX,MAAM,EAAE;QACN,IAAI,EAAE,IAAI,CAAC;QACX,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;KACtB,GACA,IAAI,CAAC,KAAK,CAAC;IAqBd,IAAI,cAAc,uBAGjB;IAED,IAAI,YAAY,YAEf;IAED,IAAI,WAAW,YAEd;IAED,IAAI,UAAU,YAEb;IAED,IAAI,aAAa,YAEhB;IAED,YAAY;IAgBZ,MAAM,CAAC,6BAA6B,CAClC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,gBAAgB,CAAC,EACpD,SAAS,CAAC,EAAE,oBAAoB,GAC/B,IAAI,CAAC,IAAI,CAAC;IASb,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;CA0DjC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_runtime = require('../_virtual/_rolldown/runtime.js');
|
|
2
|
+
const require_event_coordinator = require('./event-coordinator.js');
|
|
2
3
|
let _ddd_ts_core = require("@ddd-ts/core");
|
|
3
4
|
let _ddd_ts_store_firestore = require("@ddd-ts/store-firestore");
|
|
4
5
|
let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
@@ -51,10 +52,32 @@ var FirestoreProjector = class {
|
|
|
51
52
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
52
53
|
}
|
|
53
54
|
}
|
|
55
|
+
eventCoordinators = /* @__PURE__ */ new Map();
|
|
56
|
+
getEventCoordinator(checkpointId) {
|
|
57
|
+
const key = checkpointId.serialize();
|
|
58
|
+
let coordinator = this.eventCoordinators.get(key);
|
|
59
|
+
if (!coordinator) {
|
|
60
|
+
coordinator = new require_event_coordinator.EventCoordinator();
|
|
61
|
+
coordinator.onEmpty(() => this.eventCoordinators.delete(key));
|
|
62
|
+
this.eventCoordinators.set(key, coordinator);
|
|
63
|
+
}
|
|
64
|
+
return coordinator;
|
|
65
|
+
}
|
|
54
66
|
async handle(savedChange) {
|
|
55
67
|
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
68
|
+
const eventCoordinator = this.getEventCoordinator(checkpointId);
|
|
69
|
+
eventCoordinator.addEvent(savedChange);
|
|
70
|
+
await eventCoordinator.waitCurrentEvent();
|
|
71
|
+
if (!eventCoordinator.canProceed(savedChange)) {
|
|
72
|
+
eventCoordinator.cleanEvent(savedChange);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const disposeEventCoordinator = eventCoordinator.start(savedChange);
|
|
56
76
|
const target = await this.getCursor(savedChange);
|
|
57
|
-
if (!target)
|
|
77
|
+
if (!target) {
|
|
78
|
+
disposeEventCoordinator();
|
|
79
|
+
throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
80
|
+
}
|
|
58
81
|
const errors = [];
|
|
59
82
|
for await (const [attempt, reset] of this.breathe()) {
|
|
60
83
|
const source = this.projection.getSource(savedChange);
|
|
@@ -65,10 +88,12 @@ var FirestoreProjector = class {
|
|
|
65
88
|
}
|
|
66
89
|
if (status === Status.SUCCESS) {
|
|
67
90
|
await this.queue.cleanup(checkpointId);
|
|
91
|
+
disposeEventCoordinator();
|
|
68
92
|
return;
|
|
69
93
|
}
|
|
70
94
|
errors.push(message);
|
|
71
95
|
}
|
|
96
|
+
disposeEventCoordinator();
|
|
72
97
|
throw new Error(`Failed to handle event ${savedChange.id.serialize()}: ${errors.join(", ")}`);
|
|
73
98
|
}
|
|
74
99
|
async getCursor(savedChange) {
|
|
@@ -140,8 +165,9 @@ var FirestoreProjector = class {
|
|
|
140
165
|
const filtered = (await Promise.all(tasks.map((t) => this.reader.get(t.cursor)))).filter((t) => !!t);
|
|
141
166
|
if (!filtered.length) return [Status.DEFERRED, "No events to process in claimed tasks"];
|
|
142
167
|
const context = {
|
|
143
|
-
onProcessed: this.queue.processed.bind(this.queue),
|
|
144
|
-
checkpointId
|
|
168
|
+
onProcessed: this.queue.processed.bind(this.queue, claimer),
|
|
169
|
+
checkpointId,
|
|
170
|
+
assertBeforeInsert: this.assertBeforeInsert.bind(this, checkpointId, claimer, filtered)
|
|
145
171
|
};
|
|
146
172
|
const hasTarget = tasks.some((t) => t.id.equals(targetEventId));
|
|
147
173
|
try {
|
|
@@ -154,6 +180,15 @@ var FirestoreProjector = class {
|
|
|
154
180
|
return [Status.FAILURE, e];
|
|
155
181
|
}
|
|
156
182
|
}
|
|
183
|
+
async assertBeforeInsert(checkpointId, claimer, events) {
|
|
184
|
+
const claimedTasks = await this.queue.claimed(checkpointId, claimer);
|
|
185
|
+
const claimedTasksMap = new Map(claimedTasks.map((t) => [t.id.serialize(), t]));
|
|
186
|
+
for (const event of events) {
|
|
187
|
+
const task = claimedTasksMap.get(event.id.serialize());
|
|
188
|
+
if (!task) throw new Error(`Task not found for event ${event.id.serialize()} in claimer ${claimer.serialize()}`);
|
|
189
|
+
if (task.claimIds?.[0] !== claimer.serialize()) throw new Error(`Task ${task.id.serialize()} claimer mismatch: expected ${claimer.serialize()}, found ${task.claimIds?.[0]}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
157
192
|
};
|
|
158
193
|
var AlreadyEnqueuedError = class extends Error {
|
|
159
194
|
constructor() {
|
|
@@ -196,10 +231,13 @@ var FirestoreQueueStore = class {
|
|
|
196
231
|
async claim(checkpointId, claimer, tasks) {
|
|
197
232
|
const batch = this.collection.firestore.batch();
|
|
198
233
|
for (const task of tasks) {
|
|
234
|
+
if (task.claimIds.length > 0) throw new Error(`Task ${task.id.serialize()} is already claimed by ${task.claimIds.join(", ")}`);
|
|
199
235
|
const ref = this.queued(checkpointId, task.id);
|
|
200
236
|
batch.update(ref, {
|
|
201
237
|
claimer: claimer.serialize(),
|
|
202
238
|
claimedAt: firebase_admin_firestore.FieldValue.serverTimestamp(),
|
|
239
|
+
[`claimsMetadata.${claimer.serialize()}`]: { claimedAt: firebase_admin_firestore.FieldValue.serverTimestamp() },
|
|
240
|
+
claimIds: firebase_admin_firestore.FieldValue.arrayUnion(claimer.serialize()),
|
|
203
241
|
attempts: firebase_admin_firestore.FieldValue.increment(1),
|
|
204
242
|
remaining: firebase_admin_firestore.FieldValue.increment(-1)
|
|
205
243
|
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
@@ -226,9 +264,9 @@ var FirestoreQueueStore = class {
|
|
|
226
264
|
});
|
|
227
265
|
const expiredTasks = [];
|
|
228
266
|
for (const task of tasks) {
|
|
229
|
-
const
|
|
267
|
+
const originalClaimIds = task.claimIds;
|
|
230
268
|
task.checkTimeout();
|
|
231
|
-
if (
|
|
269
|
+
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
232
270
|
}
|
|
233
271
|
if (expiredTasks.length > 0) {
|
|
234
272
|
const batch = this.collection.firestore.batch();
|
|
@@ -237,20 +275,19 @@ var FirestoreQueueStore = class {
|
|
|
237
275
|
batch.update(ref, {
|
|
238
276
|
claimer: firebase_admin_firestore.FieldValue.delete(),
|
|
239
277
|
claimedAt: firebase_admin_firestore.FieldValue.delete(),
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
});
|
|
278
|
+
claimIds: task.claimIds
|
|
279
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
243
280
|
}
|
|
244
281
|
await batch.commit();
|
|
245
282
|
}
|
|
246
283
|
return tasks;
|
|
247
284
|
}
|
|
248
285
|
async claimed(checkpointId, claimer) {
|
|
249
|
-
return (await this.queue(checkpointId).where("
|
|
286
|
+
return (await this.queue(checkpointId).where("claimIds", "array-contains", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
|
|
250
287
|
const data = this.converter.fromFirestoreSnapshot(doc);
|
|
251
288
|
const timestamp = doc.updateTime ? this.timestampToMicroseconds(doc.updateTime) : void 0;
|
|
252
289
|
return Task.deserializeWithLastUpdateTime(data, timestamp);
|
|
253
|
-
});
|
|
290
|
+
}).filter((task) => task.claimIds[0] === claimer.serialize());
|
|
254
291
|
}
|
|
255
292
|
async unclaim(checkpointId, tasks) {
|
|
256
293
|
const batch = this.collection.firestore.batch();
|
|
@@ -258,8 +295,9 @@ var FirestoreQueueStore = class {
|
|
|
258
295
|
const ref = this.queued(checkpointId, task.id);
|
|
259
296
|
batch.update(ref, {
|
|
260
297
|
claimer: firebase_admin_firestore.FieldValue.delete(),
|
|
261
|
-
claimedAt: firebase_admin_firestore.FieldValue.delete()
|
|
262
|
-
|
|
298
|
+
claimedAt: firebase_admin_firestore.FieldValue.delete(),
|
|
299
|
+
claimIds: firebase_admin_firestore.FieldValue.arrayRemove(task.currentClaimId)
|
|
300
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
263
301
|
}
|
|
264
302
|
await batch.commit();
|
|
265
303
|
}
|
|
@@ -290,16 +328,22 @@ var FirestoreQueueStore = class {
|
|
|
290
328
|
queued(id, eventId) {
|
|
291
329
|
return this.queue(id).doc(eventId.serialize());
|
|
292
330
|
}
|
|
293
|
-
async processed(id, eventIds, context = {}) {
|
|
331
|
+
async processed(claimerId, id, eventIds, context = {}) {
|
|
294
332
|
const { transaction: trx, batchWriter } = context;
|
|
295
333
|
if (trx) {
|
|
296
334
|
for (const eventId of eventIds) {
|
|
297
335
|
const ref = this.queued(id, eventId);
|
|
298
|
-
trx.transaction.update(ref, {
|
|
336
|
+
trx.transaction.update(ref, {
|
|
337
|
+
processed: true,
|
|
338
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: firebase_admin_firestore.FieldValue.serverTimestamp()
|
|
339
|
+
});
|
|
299
340
|
}
|
|
300
341
|
return;
|
|
301
342
|
}
|
|
302
|
-
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
343
|
+
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
344
|
+
processed: true,
|
|
345
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: firebase_admin_firestore.FieldValue.serverTimestamp()
|
|
346
|
+
})));
|
|
303
347
|
}
|
|
304
348
|
async getTailCursor(id) {
|
|
305
349
|
const tailDoc = (await this.queue(id).where("remaining", ">", 0).orderBy("occurredAt", "asc").orderBy("revision", "asc").limit(1).get()).docs[0];
|
|
@@ -359,6 +403,8 @@ var FirestoreQueueStore = class {
|
|
|
359
403
|
processed: true,
|
|
360
404
|
claimer: void 0,
|
|
361
405
|
claimedAt: void 0,
|
|
406
|
+
claimsMetadata: {},
|
|
407
|
+
claimIds: [],
|
|
362
408
|
lock: new _ddd_ts_core.Lock({}),
|
|
363
409
|
remaining: 1,
|
|
364
410
|
claimTimeout: 0,
|
|
@@ -385,6 +431,11 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
385
431
|
processed: Boolean,
|
|
386
432
|
claimer: (0, _ddd_ts_shape.Optional)(String),
|
|
387
433
|
claimedAt: (0, _ddd_ts_shape.Optional)(_ddd_ts_shape.MicrosecondTimestamp),
|
|
434
|
+
claimsMetadata: (0, _ddd_ts_shape.Mapping)([{
|
|
435
|
+
claimedAt: _ddd_ts_shape.MicrosecondTimestamp,
|
|
436
|
+
processedAt: (0, _ddd_ts_shape.Optional)(_ddd_ts_shape.MicrosecondTimestamp)
|
|
437
|
+
}]),
|
|
438
|
+
claimIds: [String],
|
|
388
439
|
lock: _ddd_ts_core.Lock,
|
|
389
440
|
skipAfter: Number,
|
|
390
441
|
remaining: Number,
|
|
@@ -407,6 +458,8 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
407
458
|
claimer: void 0,
|
|
408
459
|
processed: false,
|
|
409
460
|
claimedAt: void 0,
|
|
461
|
+
claimsMetadata: {},
|
|
462
|
+
claimIds: [],
|
|
410
463
|
lock: config.lock,
|
|
411
464
|
claimTimeout: config.claimTimeout,
|
|
412
465
|
skipAfter: config.skipAfter,
|
|
@@ -418,8 +471,11 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
418
471
|
lastUpdateTime: void 0
|
|
419
472
|
});
|
|
420
473
|
}
|
|
474
|
+
get currentClaimId() {
|
|
475
|
+
return this.claimIds.at(-1);
|
|
476
|
+
}
|
|
421
477
|
get isProcessing() {
|
|
422
|
-
return
|
|
478
|
+
return this.currentClaimId !== void 0;
|
|
423
479
|
}
|
|
424
480
|
get isProcessed() {
|
|
425
481
|
return !!this.processed;
|
|
@@ -431,13 +487,11 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
431
487
|
return this.attempts > this.isolateAfter;
|
|
432
488
|
}
|
|
433
489
|
checkTimeout() {
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.remaining -= 1;
|
|
440
|
-
}
|
|
490
|
+
const claimer = this.currentClaimId;
|
|
491
|
+
if (!claimer) return;
|
|
492
|
+
const claimInfo = this.claimsMetadata[claimer];
|
|
493
|
+
if (!claimInfo || !claimInfo.claimedAt) return;
|
|
494
|
+
if (_ddd_ts_shape.MicrosecondTimestamp.now().micros - claimInfo.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) this.claimIds = this.claimIds.filter((id) => id !== claimer);
|
|
441
495
|
}
|
|
442
496
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
443
497
|
return Task.deserialize({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { EventCoordinator } from "./event-coordinator.mjs";
|
|
1
2
|
import { Cursor, EventId, Lock, ProjectedStreamReader } from "@ddd-ts/core";
|
|
2
3
|
import { DefaultConverter } from "@ddd-ts/store-firestore";
|
|
3
4
|
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
4
|
-
import { MicrosecondTimestamp, Optional, Shape } from "@ddd-ts/shape";
|
|
5
|
+
import { Mapping, MicrosecondTimestamp, Optional, Shape } from "@ddd-ts/shape";
|
|
5
6
|
|
|
6
7
|
//#region src/projection/firestore.projector.ts
|
|
7
8
|
const Status = {
|
|
@@ -50,10 +51,32 @@ var FirestoreProjector = class {
|
|
|
50
51
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
eventCoordinators = /* @__PURE__ */ new Map();
|
|
55
|
+
getEventCoordinator(checkpointId) {
|
|
56
|
+
const key = checkpointId.serialize();
|
|
57
|
+
let coordinator = this.eventCoordinators.get(key);
|
|
58
|
+
if (!coordinator) {
|
|
59
|
+
coordinator = new EventCoordinator();
|
|
60
|
+
coordinator.onEmpty(() => this.eventCoordinators.delete(key));
|
|
61
|
+
this.eventCoordinators.set(key, coordinator);
|
|
62
|
+
}
|
|
63
|
+
return coordinator;
|
|
64
|
+
}
|
|
53
65
|
async handle(savedChange) {
|
|
54
66
|
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
67
|
+
const eventCoordinator = this.getEventCoordinator(checkpointId);
|
|
68
|
+
eventCoordinator.addEvent(savedChange);
|
|
69
|
+
await eventCoordinator.waitCurrentEvent();
|
|
70
|
+
if (!eventCoordinator.canProceed(savedChange)) {
|
|
71
|
+
eventCoordinator.cleanEvent(savedChange);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const disposeEventCoordinator = eventCoordinator.start(savedChange);
|
|
55
75
|
const target = await this.getCursor(savedChange);
|
|
56
|
-
if (!target)
|
|
76
|
+
if (!target) {
|
|
77
|
+
disposeEventCoordinator();
|
|
78
|
+
throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
79
|
+
}
|
|
57
80
|
const errors = [];
|
|
58
81
|
for await (const [attempt, reset] of this.breathe()) {
|
|
59
82
|
const source = this.projection.getSource(savedChange);
|
|
@@ -64,10 +87,12 @@ var FirestoreProjector = class {
|
|
|
64
87
|
}
|
|
65
88
|
if (status === Status.SUCCESS) {
|
|
66
89
|
await this.queue.cleanup(checkpointId);
|
|
90
|
+
disposeEventCoordinator();
|
|
67
91
|
return;
|
|
68
92
|
}
|
|
69
93
|
errors.push(message);
|
|
70
94
|
}
|
|
95
|
+
disposeEventCoordinator();
|
|
71
96
|
throw new Error(`Failed to handle event ${savedChange.id.serialize()}: ${errors.join(", ")}`);
|
|
72
97
|
}
|
|
73
98
|
async getCursor(savedChange) {
|
|
@@ -139,8 +164,9 @@ var FirestoreProjector = class {
|
|
|
139
164
|
const filtered = (await Promise.all(tasks.map((t) => this.reader.get(t.cursor)))).filter((t) => !!t);
|
|
140
165
|
if (!filtered.length) return [Status.DEFERRED, "No events to process in claimed tasks"];
|
|
141
166
|
const context = {
|
|
142
|
-
onProcessed: this.queue.processed.bind(this.queue),
|
|
143
|
-
checkpointId
|
|
167
|
+
onProcessed: this.queue.processed.bind(this.queue, claimer),
|
|
168
|
+
checkpointId,
|
|
169
|
+
assertBeforeInsert: this.assertBeforeInsert.bind(this, checkpointId, claimer, filtered)
|
|
144
170
|
};
|
|
145
171
|
const hasTarget = tasks.some((t) => t.id.equals(targetEventId));
|
|
146
172
|
try {
|
|
@@ -153,6 +179,15 @@ var FirestoreProjector = class {
|
|
|
153
179
|
return [Status.FAILURE, e];
|
|
154
180
|
}
|
|
155
181
|
}
|
|
182
|
+
async assertBeforeInsert(checkpointId, claimer, events) {
|
|
183
|
+
const claimedTasks = await this.queue.claimed(checkpointId, claimer);
|
|
184
|
+
const claimedTasksMap = new Map(claimedTasks.map((t) => [t.id.serialize(), t]));
|
|
185
|
+
for (const event of events) {
|
|
186
|
+
const task = claimedTasksMap.get(event.id.serialize());
|
|
187
|
+
if (!task) throw new Error(`Task not found for event ${event.id.serialize()} in claimer ${claimer.serialize()}`);
|
|
188
|
+
if (task.claimIds?.[0] !== claimer.serialize()) throw new Error(`Task ${task.id.serialize()} claimer mismatch: expected ${claimer.serialize()}, found ${task.claimIds?.[0]}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
156
191
|
};
|
|
157
192
|
var AlreadyEnqueuedError = class extends Error {
|
|
158
193
|
constructor() {
|
|
@@ -195,10 +230,13 @@ var FirestoreQueueStore = class {
|
|
|
195
230
|
async claim(checkpointId, claimer, tasks) {
|
|
196
231
|
const batch = this.collection.firestore.batch();
|
|
197
232
|
for (const task of tasks) {
|
|
233
|
+
if (task.claimIds.length > 0) throw new Error(`Task ${task.id.serialize()} is already claimed by ${task.claimIds.join(", ")}`);
|
|
198
234
|
const ref = this.queued(checkpointId, task.id);
|
|
199
235
|
batch.update(ref, {
|
|
200
236
|
claimer: claimer.serialize(),
|
|
201
237
|
claimedAt: FieldValue.serverTimestamp(),
|
|
238
|
+
[`claimsMetadata.${claimer.serialize()}`]: { claimedAt: FieldValue.serverTimestamp() },
|
|
239
|
+
claimIds: FieldValue.arrayUnion(claimer.serialize()),
|
|
202
240
|
attempts: FieldValue.increment(1),
|
|
203
241
|
remaining: FieldValue.increment(-1)
|
|
204
242
|
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
@@ -225,9 +263,9 @@ var FirestoreQueueStore = class {
|
|
|
225
263
|
});
|
|
226
264
|
const expiredTasks = [];
|
|
227
265
|
for (const task of tasks) {
|
|
228
|
-
const
|
|
266
|
+
const originalClaimIds = task.claimIds;
|
|
229
267
|
task.checkTimeout();
|
|
230
|
-
if (
|
|
268
|
+
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
231
269
|
}
|
|
232
270
|
if (expiredTasks.length > 0) {
|
|
233
271
|
const batch = this.collection.firestore.batch();
|
|
@@ -236,20 +274,19 @@ var FirestoreQueueStore = class {
|
|
|
236
274
|
batch.update(ref, {
|
|
237
275
|
claimer: FieldValue.delete(),
|
|
238
276
|
claimedAt: FieldValue.delete(),
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
277
|
+
claimIds: task.claimIds
|
|
278
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
242
279
|
}
|
|
243
280
|
await batch.commit();
|
|
244
281
|
}
|
|
245
282
|
return tasks;
|
|
246
283
|
}
|
|
247
284
|
async claimed(checkpointId, claimer) {
|
|
248
|
-
return (await this.queue(checkpointId).where("
|
|
285
|
+
return (await this.queue(checkpointId).where("claimIds", "array-contains", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
|
|
249
286
|
const data = this.converter.fromFirestoreSnapshot(doc);
|
|
250
287
|
const timestamp = doc.updateTime ? this.timestampToMicroseconds(doc.updateTime) : void 0;
|
|
251
288
|
return Task.deserializeWithLastUpdateTime(data, timestamp);
|
|
252
|
-
});
|
|
289
|
+
}).filter((task) => task.claimIds[0] === claimer.serialize());
|
|
253
290
|
}
|
|
254
291
|
async unclaim(checkpointId, tasks) {
|
|
255
292
|
const batch = this.collection.firestore.batch();
|
|
@@ -257,8 +294,9 @@ var FirestoreQueueStore = class {
|
|
|
257
294
|
const ref = this.queued(checkpointId, task.id);
|
|
258
295
|
batch.update(ref, {
|
|
259
296
|
claimer: FieldValue.delete(),
|
|
260
|
-
claimedAt: FieldValue.delete()
|
|
261
|
-
|
|
297
|
+
claimedAt: FieldValue.delete(),
|
|
298
|
+
claimIds: FieldValue.arrayRemove(task.currentClaimId)
|
|
299
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
262
300
|
}
|
|
263
301
|
await batch.commit();
|
|
264
302
|
}
|
|
@@ -289,16 +327,22 @@ var FirestoreQueueStore = class {
|
|
|
289
327
|
queued(id, eventId) {
|
|
290
328
|
return this.queue(id).doc(eventId.serialize());
|
|
291
329
|
}
|
|
292
|
-
async processed(id, eventIds, context = {}) {
|
|
330
|
+
async processed(claimerId, id, eventIds, context = {}) {
|
|
293
331
|
const { transaction: trx, batchWriter } = context;
|
|
294
332
|
if (trx) {
|
|
295
333
|
for (const eventId of eventIds) {
|
|
296
334
|
const ref = this.queued(id, eventId);
|
|
297
|
-
trx.transaction.update(ref, {
|
|
335
|
+
trx.transaction.update(ref, {
|
|
336
|
+
processed: true,
|
|
337
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: FieldValue.serverTimestamp()
|
|
338
|
+
});
|
|
298
339
|
}
|
|
299
340
|
return;
|
|
300
341
|
}
|
|
301
|
-
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
342
|
+
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
343
|
+
processed: true,
|
|
344
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: FieldValue.serverTimestamp()
|
|
345
|
+
})));
|
|
302
346
|
}
|
|
303
347
|
async getTailCursor(id) {
|
|
304
348
|
const tailDoc = (await this.queue(id).where("remaining", ">", 0).orderBy("occurredAt", "asc").orderBy("revision", "asc").limit(1).get()).docs[0];
|
|
@@ -358,6 +402,8 @@ var FirestoreQueueStore = class {
|
|
|
358
402
|
processed: true,
|
|
359
403
|
claimer: void 0,
|
|
360
404
|
claimedAt: void 0,
|
|
405
|
+
claimsMetadata: {},
|
|
406
|
+
claimIds: [],
|
|
361
407
|
lock: new Lock({}),
|
|
362
408
|
remaining: 1,
|
|
363
409
|
claimTimeout: 0,
|
|
@@ -384,6 +430,11 @@ var Task = class Task extends Shape({
|
|
|
384
430
|
processed: Boolean,
|
|
385
431
|
claimer: Optional(String),
|
|
386
432
|
claimedAt: Optional(MicrosecondTimestamp),
|
|
433
|
+
claimsMetadata: Mapping([{
|
|
434
|
+
claimedAt: MicrosecondTimestamp,
|
|
435
|
+
processedAt: Optional(MicrosecondTimestamp)
|
|
436
|
+
}]),
|
|
437
|
+
claimIds: [String],
|
|
387
438
|
lock: Lock,
|
|
388
439
|
skipAfter: Number,
|
|
389
440
|
remaining: Number,
|
|
@@ -406,6 +457,8 @@ var Task = class Task extends Shape({
|
|
|
406
457
|
claimer: void 0,
|
|
407
458
|
processed: false,
|
|
408
459
|
claimedAt: void 0,
|
|
460
|
+
claimsMetadata: {},
|
|
461
|
+
claimIds: [],
|
|
409
462
|
lock: config.lock,
|
|
410
463
|
claimTimeout: config.claimTimeout,
|
|
411
464
|
skipAfter: config.skipAfter,
|
|
@@ -417,8 +470,11 @@ var Task = class Task extends Shape({
|
|
|
417
470
|
lastUpdateTime: void 0
|
|
418
471
|
});
|
|
419
472
|
}
|
|
473
|
+
get currentClaimId() {
|
|
474
|
+
return this.claimIds.at(-1);
|
|
475
|
+
}
|
|
420
476
|
get isProcessing() {
|
|
421
|
-
return
|
|
477
|
+
return this.currentClaimId !== void 0;
|
|
422
478
|
}
|
|
423
479
|
get isProcessed() {
|
|
424
480
|
return !!this.processed;
|
|
@@ -430,13 +486,11 @@ var Task = class Task extends Shape({
|
|
|
430
486
|
return this.attempts > this.isolateAfter;
|
|
431
487
|
}
|
|
432
488
|
checkTimeout() {
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.remaining -= 1;
|
|
439
|
-
}
|
|
489
|
+
const claimer = this.currentClaimId;
|
|
490
|
+
if (!claimer) return;
|
|
491
|
+
const claimInfo = this.claimsMetadata[claimer];
|
|
492
|
+
if (!claimInfo || !claimInfo.claimedAt) return;
|
|
493
|
+
if (MicrosecondTimestamp.now().micros - claimInfo.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) this.claimIds = this.claimIds.filter((id) => id !== claimer);
|
|
440
494
|
}
|
|
441
495
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
442
496
|
return Task.deserialize({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promise-with-resolvers.d.ts","sourceRoot":"","sources":["../../src/utils/promise-with-resolvers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,OAAK,gBAAgB,CAAC,CAAC,CAQ5D,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/utils/promise-with-resolvers.ts
|
|
3
|
+
const promiseWithResolvers = () => {
|
|
4
|
+
let resolve;
|
|
5
|
+
let reject;
|
|
6
|
+
return {
|
|
7
|
+
promise: new Promise((res, rej) => {
|
|
8
|
+
resolve = res;
|
|
9
|
+
reject = rej;
|
|
10
|
+
}),
|
|
11
|
+
resolve,
|
|
12
|
+
reject
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
exports.promiseWithResolvers = promiseWithResolvers;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/utils/promise-with-resolvers.ts
|
|
2
|
+
const promiseWithResolvers = () => {
|
|
3
|
+
let resolve;
|
|
4
|
+
let reject;
|
|
5
|
+
return {
|
|
6
|
+
promise: new Promise((res, rej) => {
|
|
7
|
+
resolve = res;
|
|
8
|
+
reject = rej;
|
|
9
|
+
}),
|
|
10
|
+
resolve,
|
|
11
|
+
reject
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { promiseWithResolvers };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddd-ts/event-sourcing-firestore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.39",
|
|
4
4
|
"types": "dist/index.d.ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@ddd-ts/core": "0.0.
|
|
14
|
-
"@ddd-ts/shape": "0.0.
|
|
15
|
-
"@ddd-ts/store-firestore": "0.0.
|
|
16
|
-
"@ddd-ts/traits": "0.0.
|
|
17
|
-
"@ddd-ts/types": "0.0.
|
|
13
|
+
"@ddd-ts/core": "0.0.39",
|
|
14
|
+
"@ddd-ts/shape": "0.0.39",
|
|
15
|
+
"@ddd-ts/store-firestore": "0.0.39",
|
|
16
|
+
"@ddd-ts/traits": "0.0.39",
|
|
17
|
+
"@ddd-ts/types": "0.0.39",
|
|
18
18
|
"@opentelemetry/api": "1.6.0",
|
|
19
19
|
"firebase-admin": "^13.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@ddd-ts/shape": "0.0.
|
|
23
|
-
"@ddd-ts/tests": "0.0.
|
|
24
|
-
"@ddd-ts/tools": "0.0.
|
|
25
|
-
"@ddd-ts/types": "0.0.
|
|
22
|
+
"@ddd-ts/shape": "0.0.39",
|
|
23
|
+
"@ddd-ts/tests": "0.0.39",
|
|
24
|
+
"@ddd-ts/tools": "0.0.39",
|
|
25
|
+
"@ddd-ts/types": "0.0.39",
|
|
26
26
|
"@types/jest": "^29.5.1"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|