@ddd-ts/event-sourcing-firestore 0.0.0-fix-build.1 → 0.0.0-fix-get-handler.2
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/projection/firestore.projector.d.ts +43 -32
- package/dist/projection/firestore.projector.d.ts.map +1 -1
- package/dist/projection/firestore.projector.js +200 -29
- package/dist/projection/firestore.projector.mjs +201 -30
- package/package.json +10 -10
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export { FirestoreEventLakeAggregateStore, MakeFirestoreEventLakeAggregateStore,
|
|
|
7
7
|
export { FirestoreProjectedStreamStorageLayer, FirestoreLakeSourceFilter, FirestoreStreamSourceFilter, } from "./firestore.projected-stream.storage-layer";
|
|
8
8
|
export { FirestoreProjectedStreamReader } from "./firestore.projected-stream.reader";
|
|
9
9
|
export { FirestoreSnapshotter } from "./firestore.snapshotter";
|
|
10
|
-
export { FirestoreProjector, FirestoreQueueStore, Task, AlreadyEnqueuedError, ClaimerId, } from "./projection/firestore.projector";
|
|
10
|
+
export { FirestoreProjector, FirestoreQueueStore, Task, AlreadyEnqueuedError, ClaimerId, type ProjectorLogger, } from "./projection/firestore.projector";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,EACL,kCAAkC,EAClC,sCAAsC,GACvC,MAAM,0CAA0C,CAAC;AAElD,OAAO,EACL,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EACL,oCAAoC,EACpC,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,IAAI,EACJ,oBAAoB,EACpB,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,EACL,kCAAkC,EAClC,sCAAsC,GACvC,MAAM,0CAA0C,CAAC;AAElD,OAAO,EACL,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EACL,oCAAoC,EACpC,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,IAAI,EACJ,oBAAoB,EACpB,SAAS,EACT,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC"}
|
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { MicrosecondTimestamp } from "@ddd-ts/shape";
|
|
1
|
+
import { CheckpointId, Cursor, ESProjection, EventId, type IEsEvent, type IFact, type ISavedChange, Lock, ProjectedStreamReader, type Serialized } from "@ddd-ts/core";
|
|
2
|
+
import { Mapping, MicrosecondTimestamp } from "@ddd-ts/shape";
|
|
4
3
|
import { DefaultConverter, FirestoreTransaction } from "@ddd-ts/store-firestore";
|
|
4
|
+
import { Firestore, WriteBatch } from "firebase-admin/firestore";
|
|
5
|
+
export interface ProjectorLogger {
|
|
6
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
7
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
8
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
9
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
10
|
+
}
|
|
11
|
+
interface FirestoreProjectorConfig {
|
|
12
|
+
retry: {
|
|
13
|
+
attempts: number;
|
|
14
|
+
minDelay: number;
|
|
15
|
+
maxDelay: number;
|
|
16
|
+
backoff: number;
|
|
17
|
+
};
|
|
18
|
+
enqueue: {
|
|
19
|
+
batchSize: number;
|
|
20
|
+
};
|
|
21
|
+
logger?: ProjectorLogger;
|
|
22
|
+
/** @deprecated Use `logger.error` instead */
|
|
23
|
+
onProcessError: (error: Error) => void;
|
|
24
|
+
/** @deprecated Use `logger.error` instead */
|
|
25
|
+
onEnqueueError: (error: Error) => void;
|
|
26
|
+
}
|
|
5
27
|
export declare class FirestoreProjector {
|
|
6
28
|
readonly projection: ESProjection<IEsEvent>;
|
|
7
29
|
readonly reader: ProjectedStreamReader<IEsEvent>;
|
|
8
30
|
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
|
-
};
|
|
31
|
+
config: FirestoreProjectorConfig;
|
|
22
32
|
_unclaim: boolean;
|
|
23
|
-
constructor(projection: ESProjection<IEsEvent>, reader: ProjectedStreamReader<IEsEvent>, queue: FirestoreQueueStore, config?:
|
|
24
|
-
|
|
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
|
-
});
|
|
33
|
+
constructor(projection: ESProjection<IEsEvent>, reader: ProjectedStreamReader<IEsEvent>, queue: FirestoreQueueStore, config?: FirestoreProjectorConfig);
|
|
34
|
+
private get logger();
|
|
36
35
|
breathe(): AsyncGenerator<readonly [number, () => void], void, unknown>;
|
|
36
|
+
private processingCheckpoints;
|
|
37
|
+
private coalescedCounts;
|
|
38
|
+
private pendingCursors;
|
|
39
|
+
private prunePendingCursors;
|
|
37
40
|
handle(savedChange: ISavedChange<IEsEvent>): Promise<void>;
|
|
41
|
+
private handleOne;
|
|
38
42
|
private getCursor;
|
|
39
43
|
private attempt;
|
|
40
44
|
private getQueueHead;
|
|
@@ -45,6 +49,7 @@ export declare class FirestoreProjector {
|
|
|
45
49
|
private getUnprocessed;
|
|
46
50
|
private claimTasks;
|
|
47
51
|
private processEvents;
|
|
52
|
+
private assertBeforeInsert;
|
|
48
53
|
}
|
|
49
54
|
export declare class AlreadyEnqueuedError extends Error {
|
|
50
55
|
constructor();
|
|
@@ -72,7 +77,7 @@ export declare class FirestoreQueueStore {
|
|
|
72
77
|
checkpoint(id: CheckpointId): FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
73
78
|
queue(id: CheckpointId): FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
74
79
|
queued(id: CheckpointId, eventId: EventId): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
75
|
-
processed(id: CheckpointId, eventIds: EventId[], context?: {
|
|
80
|
+
processed(claimerId: ClaimerId, id: CheckpointId, eventIds: EventId[], context?: {
|
|
76
81
|
transaction?: FirestoreTransaction;
|
|
77
82
|
batchWriter?: WriteBatch;
|
|
78
83
|
}): Promise<void>;
|
|
@@ -100,8 +105,13 @@ declare const Task_base: import("@ddd-ts/shape").IDict<{
|
|
|
100
105
|
readonly revision: NumberConstructor;
|
|
101
106
|
readonly attempts: NumberConstructor;
|
|
102
107
|
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>;
|
|
108
|
+
/** @deprecated */ readonly claimer: import("@ddd-ts/shape").IOptional<StringConstructor, typeof import("@ddd-ts/shape").Empty>;
|
|
109
|
+
/** @deprecated */ readonly claimedAt: import("@ddd-ts/shape").IOptional<typeof MicrosecondTimestamp, typeof import("@ddd-ts/shape").Empty>;
|
|
110
|
+
readonly claimsMetadata: Mapping<[{
|
|
111
|
+
claimedAt: typeof MicrosecondTimestamp;
|
|
112
|
+
processedAt: import("@ddd-ts/shape").IOptional<typeof MicrosecondTimestamp, typeof import("@ddd-ts/shape").Empty>;
|
|
113
|
+
}], typeof import("@ddd-ts/shape").Empty>;
|
|
114
|
+
readonly claimIds: [StringConstructor];
|
|
105
115
|
readonly lock: typeof Lock;
|
|
106
116
|
readonly skipAfter: NumberConstructor;
|
|
107
117
|
readonly remaining: NumberConstructor;
|
|
@@ -118,6 +128,7 @@ export declare class Task<Stored extends boolean> extends Task_base {
|
|
|
118
128
|
skipAfter: number;
|
|
119
129
|
isolateAfter: number;
|
|
120
130
|
}): Task<false>;
|
|
131
|
+
get currentClaimId(): string | undefined;
|
|
121
132
|
get isProcessing(): boolean;
|
|
122
133
|
get isProcessed(): boolean;
|
|
123
134
|
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;
|
|
1
|
+
{"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,6 +17,12 @@ const TaskState = {
|
|
|
17
17
|
};
|
|
18
18
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
19
|
const RETENTION = _ddd_ts_shape.MicrosecondTimestamp.MONTH;
|
|
20
|
+
const defaultLogger = {
|
|
21
|
+
debug: (msg, ctx) => console.debug(msg, ctx ?? ""),
|
|
22
|
+
info: (msg, ctx) => console.info(msg, ctx ?? ""),
|
|
23
|
+
warn: (msg, ctx) => console.warn(msg, ctx ?? ""),
|
|
24
|
+
error: (msg, ctx) => console.error(msg, ctx ?? "")
|
|
25
|
+
};
|
|
20
26
|
var FirestoreProjector = class {
|
|
21
27
|
_unclaim = true;
|
|
22
28
|
constructor(projection, reader, queue, config = {
|
|
@@ -27,6 +33,7 @@ var FirestoreProjector = class {
|
|
|
27
33
|
backoff: 1.5
|
|
28
34
|
},
|
|
29
35
|
enqueue: { batchSize: 100 },
|
|
36
|
+
logger: defaultLogger,
|
|
30
37
|
onProcessError: (error) => {
|
|
31
38
|
console.error("Error processing event:", error);
|
|
32
39
|
},
|
|
@@ -39,6 +46,9 @@ var FirestoreProjector = class {
|
|
|
39
46
|
this.queue = queue;
|
|
40
47
|
this.config = config;
|
|
41
48
|
}
|
|
49
|
+
get logger() {
|
|
50
|
+
return this.config.logger ?? defaultLogger;
|
|
51
|
+
}
|
|
42
52
|
async *breathe() {
|
|
43
53
|
const { attempts, minDelay, maxDelay, backoff } = this.config.retry;
|
|
44
54
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -51,10 +61,106 @@ var FirestoreProjector = class {
|
|
|
51
61
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
52
62
|
}
|
|
53
63
|
}
|
|
64
|
+
processingCheckpoints = /* @__PURE__ */ new Map();
|
|
65
|
+
coalescedCounts = /* @__PURE__ */ new Map();
|
|
66
|
+
pendingCursors = /* @__PURE__ */ new Map();
|
|
67
|
+
prunePendingCursors(checkpointId, eventIds) {
|
|
68
|
+
const key = checkpointId.serialize();
|
|
69
|
+
const pending = this.pendingCursors.get(key);
|
|
70
|
+
if (!pending) return;
|
|
71
|
+
for (const eventId of eventIds) pending.delete(eventId.serialize());
|
|
72
|
+
}
|
|
54
73
|
async handle(savedChange) {
|
|
74
|
+
const key = this.projection.getCheckpointId(savedChange).serialize();
|
|
75
|
+
const cursor = await this.getCursor(savedChange);
|
|
76
|
+
if (!cursor) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
77
|
+
if (this.processingCheckpoints.has(key)) {
|
|
78
|
+
if (!this.pendingCursors.has(key)) this.pendingCursors.set(key, /* @__PURE__ */ new Map());
|
|
79
|
+
const eventId = savedChange.id.serialize();
|
|
80
|
+
this.pendingCursors.get(key).set(eventId, {
|
|
81
|
+
savedChange,
|
|
82
|
+
cursor
|
|
83
|
+
});
|
|
84
|
+
const existing = this.processingCheckpoints.get(key);
|
|
85
|
+
if (!existing || cursor.isAfter(existing.cursor)) this.processingCheckpoints.set(key, {
|
|
86
|
+
savedChange,
|
|
87
|
+
cursor
|
|
88
|
+
});
|
|
89
|
+
const coalesced = (this.coalescedCounts.get(key) ?? 0) + 1;
|
|
90
|
+
this.coalescedCounts.set(key, coalesced);
|
|
91
|
+
this.logger.debug(`debounced: Checkpoint<${key}> is processing, Event<${eventId}> coalesced (${coalesced} pending)`, {
|
|
92
|
+
checkpointId: key,
|
|
93
|
+
eventId,
|
|
94
|
+
coalesced
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this.processingCheckpoints.set(key, null);
|
|
99
|
+
try {
|
|
100
|
+
await this.handleOne(savedChange, cursor);
|
|
101
|
+
const nextPending = () => {
|
|
102
|
+
const debounced = this.processingCheckpoints.get(key);
|
|
103
|
+
if (debounced) {
|
|
104
|
+
this.processingCheckpoints.set(key, null);
|
|
105
|
+
return {
|
|
106
|
+
type: "debounced",
|
|
107
|
+
savedChange: debounced.savedChange,
|
|
108
|
+
cursor: debounced.cursor,
|
|
109
|
+
eventId: debounced.savedChange.id.serialize()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const remaining = this.pendingCursors.get(key);
|
|
113
|
+
if (remaining && remaining.size > 0) {
|
|
114
|
+
const [eventId, straggler] = remaining.entries().next().value;
|
|
115
|
+
remaining.delete(eventId);
|
|
116
|
+
return {
|
|
117
|
+
type: "straggler",
|
|
118
|
+
savedChange: straggler.savedChange,
|
|
119
|
+
cursor: straggler.cursor,
|
|
120
|
+
eventId
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
let iterations = 0;
|
|
126
|
+
for (let next = nextPending(); next; next = nextPending()) {
|
|
127
|
+
iterations++;
|
|
128
|
+
if (iterations > 10) this.logger.warn(`high-iteration: Checkpoint<${key}> re-run loop at iteration ${iterations}`, {
|
|
129
|
+
checkpointId: key,
|
|
130
|
+
iterations
|
|
131
|
+
});
|
|
132
|
+
if (next.type === "debounced") this.logger.debug(`re-run: Checkpoint<${key}> iteration ${iterations}, targeting Event<${next.eventId}>`, {
|
|
133
|
+
checkpointId: key,
|
|
134
|
+
iterations,
|
|
135
|
+
eventId: next.eventId
|
|
136
|
+
});
|
|
137
|
+
else {
|
|
138
|
+
const remaining = this.pendingCursors.get(key);
|
|
139
|
+
this.logger.info(`straggler: Checkpoint<${key}> handling Event<${next.eventId}> not covered by any slice (${remaining?.size ?? 0} remaining)`, {
|
|
140
|
+
checkpointId: key,
|
|
141
|
+
eventId: next.eventId,
|
|
142
|
+
remainingStragglers: remaining?.size ?? 0
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await this.handleOne(next.savedChange, next.cursor);
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
this.processingCheckpoints.delete(key);
|
|
149
|
+
this.pendingCursors.delete(key);
|
|
150
|
+
this.coalescedCounts.delete(key);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async handleOne(savedChange, target) {
|
|
55
154
|
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
56
|
-
const
|
|
57
|
-
|
|
155
|
+
const eventId = savedChange.id.serialize();
|
|
156
|
+
const checkpointKey = checkpointId.serialize();
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
this.logger.info(`processing: Checkpoint<${checkpointKey}> targeting Event<${eventId}>`, {
|
|
159
|
+
checkpointId: checkpointKey,
|
|
160
|
+
eventId,
|
|
161
|
+
target: target.toString(),
|
|
162
|
+
targetRef: target.ref
|
|
163
|
+
});
|
|
58
164
|
const errors = [];
|
|
59
165
|
for await (const [attempt, reset] of this.breathe()) {
|
|
60
166
|
const source = this.projection.getSource(savedChange);
|
|
@@ -65,11 +171,24 @@ var FirestoreProjector = class {
|
|
|
65
171
|
}
|
|
66
172
|
if (status === Status.SUCCESS) {
|
|
67
173
|
await this.queue.cleanup(checkpointId);
|
|
174
|
+
const durationMs = Date.now() - startedAt;
|
|
175
|
+
this.logger.info(`processed: Checkpoint<${checkpointKey}> caught up to Event<${eventId}> in ${durationMs}ms`, {
|
|
176
|
+
checkpointId: checkpointKey,
|
|
177
|
+
eventId,
|
|
178
|
+
durationMs
|
|
179
|
+
});
|
|
68
180
|
return;
|
|
69
181
|
}
|
|
70
182
|
errors.push(message);
|
|
71
183
|
}
|
|
72
|
-
|
|
184
|
+
const { attempts } = this.config.retry;
|
|
185
|
+
this.logger.error(`failed: Checkpoint<${checkpointKey}> exhausted ${attempts} retries for Event<${eventId}>`, {
|
|
186
|
+
checkpointId: checkpointKey,
|
|
187
|
+
eventId,
|
|
188
|
+
attempts,
|
|
189
|
+
errors
|
|
190
|
+
});
|
|
191
|
+
throw new Error(`Failed to handle event ${eventId}: ${errors.join(", ")}`);
|
|
73
192
|
}
|
|
74
193
|
async getCursor(savedChange) {
|
|
75
194
|
return await this.reader.getCursor(savedChange);
|
|
@@ -83,7 +202,10 @@ var FirestoreProjector = class {
|
|
|
83
202
|
}
|
|
84
203
|
if (!isTargetAfterHead) {
|
|
85
204
|
const processed = await this.checkIsProcessed(checkpointId, target);
|
|
86
|
-
if (processed === TaskState.PROCESSED)
|
|
205
|
+
if (processed === TaskState.PROCESSED) {
|
|
206
|
+
this.prunePendingCursors(checkpointId, [target.eventId]);
|
|
207
|
+
return [Status.SUCCESS, "Target event already processed"];
|
|
208
|
+
}
|
|
87
209
|
if (processed === TaskState.MISSING) {
|
|
88
210
|
const [status, message] = await this.enqueueOne(checkpointId, target);
|
|
89
211
|
if (status === Status.DEFERRED) return [Status.FAILURE, message];
|
|
@@ -112,14 +234,25 @@ var FirestoreProjector = class {
|
|
|
112
234
|
const settings = this.projection.getTaskSettings(e);
|
|
113
235
|
return Task.new(e, settings);
|
|
114
236
|
});
|
|
115
|
-
|
|
237
|
+
const result = await this.queue.enqueue(checkpointId, tasks);
|
|
238
|
+
const [status] = result;
|
|
239
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, tasks.map((t) => t.id));
|
|
240
|
+
const checkpointKey = checkpointId.serialize();
|
|
241
|
+
this.logger.debug(`enqueued: Checkpoint<${checkpointKey}> added ${tasks.length} tasks to queue`, {
|
|
242
|
+
checkpointId: checkpointKey,
|
|
243
|
+
count: tasks.length
|
|
244
|
+
});
|
|
245
|
+
return result;
|
|
116
246
|
}
|
|
117
247
|
async enqueueOne(checkpointId, target) {
|
|
118
248
|
const event = await this.reader.get(target);
|
|
119
249
|
if (!event) throw new Error(`Event not found for cursor ${target.ref}`);
|
|
120
250
|
const settings = this.projection.getTaskSettings(event);
|
|
121
251
|
const task = Task.new(event, settings);
|
|
122
|
-
|
|
252
|
+
const result = await this.queue.enqueue(checkpointId, [task]);
|
|
253
|
+
const [status] = result;
|
|
254
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, [task.id]);
|
|
255
|
+
return result;
|
|
123
256
|
}
|
|
124
257
|
async checkIsProcessed(checkpointId, cursor) {
|
|
125
258
|
return await this.queue.isProcessed(checkpointId, cursor);
|
|
@@ -140,20 +273,37 @@ var FirestoreProjector = class {
|
|
|
140
273
|
const filtered = (await Promise.all(tasks.map((t) => this.reader.get(t.cursor)))).filter((t) => !!t);
|
|
141
274
|
if (!filtered.length) return [Status.DEFERRED, "No events to process in claimed tasks"];
|
|
142
275
|
const context = {
|
|
143
|
-
onProcessed: this.queue.processed.bind(this.queue),
|
|
144
|
-
checkpointId
|
|
276
|
+
onProcessed: this.queue.processed.bind(this.queue, claimer),
|
|
277
|
+
checkpointId,
|
|
278
|
+
assertBeforeInsert: this.assertBeforeInsert.bind(this, checkpointId, claimer, filtered)
|
|
145
279
|
};
|
|
146
280
|
const hasTarget = tasks.some((t) => t.id.equals(targetEventId));
|
|
147
281
|
try {
|
|
148
282
|
if ((await this.projection.process(filtered, context)).some((id) => id?.equals(targetEventId))) return [Status.SUCCESS, "Target event processed successfully"];
|
|
149
283
|
return [Status.DEFERRED, "Target event not processed yet"];
|
|
150
284
|
} catch (e) {
|
|
285
|
+
const checkpointKey = checkpointId.serialize();
|
|
286
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
287
|
+
const truncated = errorMessage.length > 200 ? `${errorMessage.slice(0, 200)}...` : errorMessage;
|
|
288
|
+
this.logger.error(`error: Checkpoint<${checkpointKey}> processing failed: ${truncated}`, {
|
|
289
|
+
checkpointId: checkpointKey,
|
|
290
|
+
error: e
|
|
291
|
+
});
|
|
151
292
|
this.config.onProcessError(e);
|
|
152
293
|
if (this._unclaim) await this.queue.unclaim(checkpointId, tasks);
|
|
153
294
|
if (!hasTarget) return [Status.DEFERRED, "Target event not in claimed batch, deferring"];
|
|
154
295
|
return [Status.FAILURE, e];
|
|
155
296
|
}
|
|
156
297
|
}
|
|
298
|
+
async assertBeforeInsert(checkpointId, claimer, events) {
|
|
299
|
+
const claimedTasks = await this.queue.claimed(checkpointId, claimer);
|
|
300
|
+
const claimedTasksMap = new Map(claimedTasks.map((t) => [t.id.serialize(), t]));
|
|
301
|
+
for (const event of events) {
|
|
302
|
+
const task = claimedTasksMap.get(event.id.serialize());
|
|
303
|
+
if (!task) throw new Error(`Task not found for event ${event.id.serialize()} in claimer ${claimer.serialize()}`);
|
|
304
|
+
if (task.claimIds?.[0] !== claimer.serialize()) throw new Error(`Task ${task.id.serialize()} claimer mismatch: expected ${claimer.serialize()}, found ${task.claimIds?.[0]}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
157
307
|
};
|
|
158
308
|
var AlreadyEnqueuedError = class extends Error {
|
|
159
309
|
constructor() {
|
|
@@ -196,10 +346,13 @@ var FirestoreQueueStore = class {
|
|
|
196
346
|
async claim(checkpointId, claimer, tasks) {
|
|
197
347
|
const batch = this.collection.firestore.batch();
|
|
198
348
|
for (const task of tasks) {
|
|
349
|
+
if (task.claimIds.length > 0) throw new Error(`Task ${task.id.serialize()} is already claimed by ${task.claimIds.join(", ")}`);
|
|
199
350
|
const ref = this.queued(checkpointId, task.id);
|
|
200
351
|
batch.update(ref, {
|
|
201
352
|
claimer: claimer.serialize(),
|
|
202
353
|
claimedAt: firebase_admin_firestore.FieldValue.serverTimestamp(),
|
|
354
|
+
[`claimsMetadata.${claimer.serialize()}`]: { claimedAt: firebase_admin_firestore.FieldValue.serverTimestamp() },
|
|
355
|
+
claimIds: firebase_admin_firestore.FieldValue.arrayUnion(claimer.serialize()),
|
|
203
356
|
attempts: firebase_admin_firestore.FieldValue.increment(1),
|
|
204
357
|
remaining: firebase_admin_firestore.FieldValue.increment(-1)
|
|
205
358
|
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
@@ -226,9 +379,9 @@ var FirestoreQueueStore = class {
|
|
|
226
379
|
});
|
|
227
380
|
const expiredTasks = [];
|
|
228
381
|
for (const task of tasks) {
|
|
229
|
-
const
|
|
382
|
+
const originalClaimIds = task.claimIds;
|
|
230
383
|
task.checkTimeout();
|
|
231
|
-
if (
|
|
384
|
+
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
232
385
|
}
|
|
233
386
|
if (expiredTasks.length > 0) {
|
|
234
387
|
const batch = this.collection.firestore.batch();
|
|
@@ -237,20 +390,19 @@ var FirestoreQueueStore = class {
|
|
|
237
390
|
batch.update(ref, {
|
|
238
391
|
claimer: firebase_admin_firestore.FieldValue.delete(),
|
|
239
392
|
claimedAt: firebase_admin_firestore.FieldValue.delete(),
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
});
|
|
393
|
+
claimIds: task.claimIds
|
|
394
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
243
395
|
}
|
|
244
396
|
await batch.commit();
|
|
245
397
|
}
|
|
246
398
|
return tasks;
|
|
247
399
|
}
|
|
248
400
|
async claimed(checkpointId, claimer) {
|
|
249
|
-
return (await this.queue(checkpointId).where("
|
|
401
|
+
return (await this.queue(checkpointId).where("claimIds", "array-contains", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
|
|
250
402
|
const data = this.converter.fromFirestoreSnapshot(doc);
|
|
251
403
|
const timestamp = doc.updateTime ? this.timestampToMicroseconds(doc.updateTime) : void 0;
|
|
252
404
|
return Task.deserializeWithLastUpdateTime(data, timestamp);
|
|
253
|
-
});
|
|
405
|
+
}).filter((task) => task.claimIds[0] === claimer.serialize());
|
|
254
406
|
}
|
|
255
407
|
async unclaim(checkpointId, tasks) {
|
|
256
408
|
const batch = this.collection.firestore.batch();
|
|
@@ -258,8 +410,9 @@ var FirestoreQueueStore = class {
|
|
|
258
410
|
const ref = this.queued(checkpointId, task.id);
|
|
259
411
|
batch.update(ref, {
|
|
260
412
|
claimer: firebase_admin_firestore.FieldValue.delete(),
|
|
261
|
-
claimedAt: firebase_admin_firestore.FieldValue.delete()
|
|
262
|
-
|
|
413
|
+
claimedAt: firebase_admin_firestore.FieldValue.delete(),
|
|
414
|
+
claimIds: firebase_admin_firestore.FieldValue.arrayRemove(task.currentClaimId)
|
|
415
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
263
416
|
}
|
|
264
417
|
await batch.commit();
|
|
265
418
|
}
|
|
@@ -290,16 +443,22 @@ var FirestoreQueueStore = class {
|
|
|
290
443
|
queued(id, eventId) {
|
|
291
444
|
return this.queue(id).doc(eventId.serialize());
|
|
292
445
|
}
|
|
293
|
-
async processed(id, eventIds, context = {}) {
|
|
446
|
+
async processed(claimerId, id, eventIds, context = {}) {
|
|
294
447
|
const { transaction: trx, batchWriter } = context;
|
|
295
448
|
if (trx) {
|
|
296
449
|
for (const eventId of eventIds) {
|
|
297
450
|
const ref = this.queued(id, eventId);
|
|
298
|
-
trx.transaction.update(ref, {
|
|
451
|
+
trx.transaction.update(ref, {
|
|
452
|
+
processed: true,
|
|
453
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: firebase_admin_firestore.FieldValue.serverTimestamp()
|
|
454
|
+
});
|
|
299
455
|
}
|
|
300
456
|
return;
|
|
301
457
|
}
|
|
302
|
-
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
458
|
+
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
459
|
+
processed: true,
|
|
460
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: firebase_admin_firestore.FieldValue.serverTimestamp()
|
|
461
|
+
})));
|
|
303
462
|
}
|
|
304
463
|
async getTailCursor(id) {
|
|
305
464
|
const tailDoc = (await this.queue(id).where("remaining", ">", 0).orderBy("occurredAt", "asc").orderBy("revision", "asc").limit(1).get()).docs[0];
|
|
@@ -359,6 +518,8 @@ var FirestoreQueueStore = class {
|
|
|
359
518
|
processed: true,
|
|
360
519
|
claimer: void 0,
|
|
361
520
|
claimedAt: void 0,
|
|
521
|
+
claimsMetadata: {},
|
|
522
|
+
claimIds: [],
|
|
362
523
|
lock: new _ddd_ts_core.Lock({}),
|
|
363
524
|
remaining: 1,
|
|
364
525
|
claimTimeout: 0,
|
|
@@ -385,6 +546,11 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
385
546
|
processed: Boolean,
|
|
386
547
|
claimer: (0, _ddd_ts_shape.Optional)(String),
|
|
387
548
|
claimedAt: (0, _ddd_ts_shape.Optional)(_ddd_ts_shape.MicrosecondTimestamp),
|
|
549
|
+
claimsMetadata: (0, _ddd_ts_shape.Mapping)([{
|
|
550
|
+
claimedAt: _ddd_ts_shape.MicrosecondTimestamp,
|
|
551
|
+
processedAt: (0, _ddd_ts_shape.Optional)(_ddd_ts_shape.MicrosecondTimestamp)
|
|
552
|
+
}]),
|
|
553
|
+
claimIds: [String],
|
|
388
554
|
lock: _ddd_ts_core.Lock,
|
|
389
555
|
skipAfter: Number,
|
|
390
556
|
remaining: Number,
|
|
@@ -407,6 +573,8 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
407
573
|
claimer: void 0,
|
|
408
574
|
processed: false,
|
|
409
575
|
claimedAt: void 0,
|
|
576
|
+
claimsMetadata: {},
|
|
577
|
+
claimIds: [],
|
|
410
578
|
lock: config.lock,
|
|
411
579
|
claimTimeout: config.claimTimeout,
|
|
412
580
|
skipAfter: config.skipAfter,
|
|
@@ -418,8 +586,11 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
418
586
|
lastUpdateTime: void 0
|
|
419
587
|
});
|
|
420
588
|
}
|
|
589
|
+
get currentClaimId() {
|
|
590
|
+
return this.claimIds.at(-1);
|
|
591
|
+
}
|
|
421
592
|
get isProcessing() {
|
|
422
|
-
return
|
|
593
|
+
return this.currentClaimId !== void 0;
|
|
423
594
|
}
|
|
424
595
|
get isProcessed() {
|
|
425
596
|
return !!this.processed;
|
|
@@ -431,18 +602,18 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
431
602
|
return this.attempts > this.isolateAfter;
|
|
432
603
|
}
|
|
433
604
|
checkTimeout() {
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
this.remaining -= 1;
|
|
440
|
-
}
|
|
605
|
+
const claimer = this.currentClaimId;
|
|
606
|
+
if (!claimer) return;
|
|
607
|
+
const claimInfo = this.claimsMetadata[claimer];
|
|
608
|
+
if (!claimInfo || !claimInfo.claimedAt) return;
|
|
609
|
+
if (_ddd_ts_shape.MicrosecondTimestamp.now().micros - claimInfo.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) this.claimIds = this.claimIds.filter((id) => id !== claimer);
|
|
441
610
|
}
|
|
442
611
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
443
612
|
return Task.deserialize({
|
|
444
613
|
...data,
|
|
445
|
-
lastUpdateTime: timestamp
|
|
614
|
+
lastUpdateTime: timestamp,
|
|
615
|
+
claimIds: data.claimIds || [],
|
|
616
|
+
claimsMetadata: data.claimsMetadata || {}
|
|
446
617
|
});
|
|
447
618
|
}
|
|
448
619
|
static batch(tasks) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Cursor, EventId, Lock, ProjectedStreamReader } from "@ddd-ts/core";
|
|
2
2
|
import { DefaultConverter } from "@ddd-ts/store-firestore";
|
|
3
3
|
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
4
|
-
import { MicrosecondTimestamp, Optional, Shape } from "@ddd-ts/shape";
|
|
4
|
+
import { Mapping, MicrosecondTimestamp, Optional, Shape } from "@ddd-ts/shape";
|
|
5
5
|
|
|
6
6
|
//#region src/projection/firestore.projector.ts
|
|
7
7
|
const Status = {
|
|
@@ -16,6 +16,12 @@ const TaskState = {
|
|
|
16
16
|
};
|
|
17
17
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
18
|
const RETENTION = MicrosecondTimestamp.MONTH;
|
|
19
|
+
const defaultLogger = {
|
|
20
|
+
debug: (msg, ctx) => console.debug(msg, ctx ?? ""),
|
|
21
|
+
info: (msg, ctx) => console.info(msg, ctx ?? ""),
|
|
22
|
+
warn: (msg, ctx) => console.warn(msg, ctx ?? ""),
|
|
23
|
+
error: (msg, ctx) => console.error(msg, ctx ?? "")
|
|
24
|
+
};
|
|
19
25
|
var FirestoreProjector = class {
|
|
20
26
|
_unclaim = true;
|
|
21
27
|
constructor(projection, reader, queue, config = {
|
|
@@ -26,6 +32,7 @@ var FirestoreProjector = class {
|
|
|
26
32
|
backoff: 1.5
|
|
27
33
|
},
|
|
28
34
|
enqueue: { batchSize: 100 },
|
|
35
|
+
logger: defaultLogger,
|
|
29
36
|
onProcessError: (error) => {
|
|
30
37
|
console.error("Error processing event:", error);
|
|
31
38
|
},
|
|
@@ -38,6 +45,9 @@ var FirestoreProjector = class {
|
|
|
38
45
|
this.queue = queue;
|
|
39
46
|
this.config = config;
|
|
40
47
|
}
|
|
48
|
+
get logger() {
|
|
49
|
+
return this.config.logger ?? defaultLogger;
|
|
50
|
+
}
|
|
41
51
|
async *breathe() {
|
|
42
52
|
const { attempts, minDelay, maxDelay, backoff } = this.config.retry;
|
|
43
53
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -50,10 +60,106 @@ var FirestoreProjector = class {
|
|
|
50
60
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
51
61
|
}
|
|
52
62
|
}
|
|
63
|
+
processingCheckpoints = /* @__PURE__ */ new Map();
|
|
64
|
+
coalescedCounts = /* @__PURE__ */ new Map();
|
|
65
|
+
pendingCursors = /* @__PURE__ */ new Map();
|
|
66
|
+
prunePendingCursors(checkpointId, eventIds) {
|
|
67
|
+
const key = checkpointId.serialize();
|
|
68
|
+
const pending = this.pendingCursors.get(key);
|
|
69
|
+
if (!pending) return;
|
|
70
|
+
for (const eventId of eventIds) pending.delete(eventId.serialize());
|
|
71
|
+
}
|
|
53
72
|
async handle(savedChange) {
|
|
73
|
+
const key = this.projection.getCheckpointId(savedChange).serialize();
|
|
74
|
+
const cursor = await this.getCursor(savedChange);
|
|
75
|
+
if (!cursor) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
76
|
+
if (this.processingCheckpoints.has(key)) {
|
|
77
|
+
if (!this.pendingCursors.has(key)) this.pendingCursors.set(key, /* @__PURE__ */ new Map());
|
|
78
|
+
const eventId = savedChange.id.serialize();
|
|
79
|
+
this.pendingCursors.get(key).set(eventId, {
|
|
80
|
+
savedChange,
|
|
81
|
+
cursor
|
|
82
|
+
});
|
|
83
|
+
const existing = this.processingCheckpoints.get(key);
|
|
84
|
+
if (!existing || cursor.isAfter(existing.cursor)) this.processingCheckpoints.set(key, {
|
|
85
|
+
savedChange,
|
|
86
|
+
cursor
|
|
87
|
+
});
|
|
88
|
+
const coalesced = (this.coalescedCounts.get(key) ?? 0) + 1;
|
|
89
|
+
this.coalescedCounts.set(key, coalesced);
|
|
90
|
+
this.logger.debug(`debounced: Checkpoint<${key}> is processing, Event<${eventId}> coalesced (${coalesced} pending)`, {
|
|
91
|
+
checkpointId: key,
|
|
92
|
+
eventId,
|
|
93
|
+
coalesced
|
|
94
|
+
});
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.processingCheckpoints.set(key, null);
|
|
98
|
+
try {
|
|
99
|
+
await this.handleOne(savedChange, cursor);
|
|
100
|
+
const nextPending = () => {
|
|
101
|
+
const debounced = this.processingCheckpoints.get(key);
|
|
102
|
+
if (debounced) {
|
|
103
|
+
this.processingCheckpoints.set(key, null);
|
|
104
|
+
return {
|
|
105
|
+
type: "debounced",
|
|
106
|
+
savedChange: debounced.savedChange,
|
|
107
|
+
cursor: debounced.cursor,
|
|
108
|
+
eventId: debounced.savedChange.id.serialize()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const remaining = this.pendingCursors.get(key);
|
|
112
|
+
if (remaining && remaining.size > 0) {
|
|
113
|
+
const [eventId, straggler] = remaining.entries().next().value;
|
|
114
|
+
remaining.delete(eventId);
|
|
115
|
+
return {
|
|
116
|
+
type: "straggler",
|
|
117
|
+
savedChange: straggler.savedChange,
|
|
118
|
+
cursor: straggler.cursor,
|
|
119
|
+
eventId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
let iterations = 0;
|
|
125
|
+
for (let next = nextPending(); next; next = nextPending()) {
|
|
126
|
+
iterations++;
|
|
127
|
+
if (iterations > 10) this.logger.warn(`high-iteration: Checkpoint<${key}> re-run loop at iteration ${iterations}`, {
|
|
128
|
+
checkpointId: key,
|
|
129
|
+
iterations
|
|
130
|
+
});
|
|
131
|
+
if (next.type === "debounced") this.logger.debug(`re-run: Checkpoint<${key}> iteration ${iterations}, targeting Event<${next.eventId}>`, {
|
|
132
|
+
checkpointId: key,
|
|
133
|
+
iterations,
|
|
134
|
+
eventId: next.eventId
|
|
135
|
+
});
|
|
136
|
+
else {
|
|
137
|
+
const remaining = this.pendingCursors.get(key);
|
|
138
|
+
this.logger.info(`straggler: Checkpoint<${key}> handling Event<${next.eventId}> not covered by any slice (${remaining?.size ?? 0} remaining)`, {
|
|
139
|
+
checkpointId: key,
|
|
140
|
+
eventId: next.eventId,
|
|
141
|
+
remainingStragglers: remaining?.size ?? 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
await this.handleOne(next.savedChange, next.cursor);
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
this.processingCheckpoints.delete(key);
|
|
148
|
+
this.pendingCursors.delete(key);
|
|
149
|
+
this.coalescedCounts.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async handleOne(savedChange, target) {
|
|
54
153
|
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
55
|
-
const
|
|
56
|
-
|
|
154
|
+
const eventId = savedChange.id.serialize();
|
|
155
|
+
const checkpointKey = checkpointId.serialize();
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
this.logger.info(`processing: Checkpoint<${checkpointKey}> targeting Event<${eventId}>`, {
|
|
158
|
+
checkpointId: checkpointKey,
|
|
159
|
+
eventId,
|
|
160
|
+
target: target.toString(),
|
|
161
|
+
targetRef: target.ref
|
|
162
|
+
});
|
|
57
163
|
const errors = [];
|
|
58
164
|
for await (const [attempt, reset] of this.breathe()) {
|
|
59
165
|
const source = this.projection.getSource(savedChange);
|
|
@@ -64,11 +170,24 @@ var FirestoreProjector = class {
|
|
|
64
170
|
}
|
|
65
171
|
if (status === Status.SUCCESS) {
|
|
66
172
|
await this.queue.cleanup(checkpointId);
|
|
173
|
+
const durationMs = Date.now() - startedAt;
|
|
174
|
+
this.logger.info(`processed: Checkpoint<${checkpointKey}> caught up to Event<${eventId}> in ${durationMs}ms`, {
|
|
175
|
+
checkpointId: checkpointKey,
|
|
176
|
+
eventId,
|
|
177
|
+
durationMs
|
|
178
|
+
});
|
|
67
179
|
return;
|
|
68
180
|
}
|
|
69
181
|
errors.push(message);
|
|
70
182
|
}
|
|
71
|
-
|
|
183
|
+
const { attempts } = this.config.retry;
|
|
184
|
+
this.logger.error(`failed: Checkpoint<${checkpointKey}> exhausted ${attempts} retries for Event<${eventId}>`, {
|
|
185
|
+
checkpointId: checkpointKey,
|
|
186
|
+
eventId,
|
|
187
|
+
attempts,
|
|
188
|
+
errors
|
|
189
|
+
});
|
|
190
|
+
throw new Error(`Failed to handle event ${eventId}: ${errors.join(", ")}`);
|
|
72
191
|
}
|
|
73
192
|
async getCursor(savedChange) {
|
|
74
193
|
return await this.reader.getCursor(savedChange);
|
|
@@ -82,7 +201,10 @@ var FirestoreProjector = class {
|
|
|
82
201
|
}
|
|
83
202
|
if (!isTargetAfterHead) {
|
|
84
203
|
const processed = await this.checkIsProcessed(checkpointId, target);
|
|
85
|
-
if (processed === TaskState.PROCESSED)
|
|
204
|
+
if (processed === TaskState.PROCESSED) {
|
|
205
|
+
this.prunePendingCursors(checkpointId, [target.eventId]);
|
|
206
|
+
return [Status.SUCCESS, "Target event already processed"];
|
|
207
|
+
}
|
|
86
208
|
if (processed === TaskState.MISSING) {
|
|
87
209
|
const [status, message] = await this.enqueueOne(checkpointId, target);
|
|
88
210
|
if (status === Status.DEFERRED) return [Status.FAILURE, message];
|
|
@@ -111,14 +233,25 @@ var FirestoreProjector = class {
|
|
|
111
233
|
const settings = this.projection.getTaskSettings(e);
|
|
112
234
|
return Task.new(e, settings);
|
|
113
235
|
});
|
|
114
|
-
|
|
236
|
+
const result = await this.queue.enqueue(checkpointId, tasks);
|
|
237
|
+
const [status] = result;
|
|
238
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, tasks.map((t) => t.id));
|
|
239
|
+
const checkpointKey = checkpointId.serialize();
|
|
240
|
+
this.logger.debug(`enqueued: Checkpoint<${checkpointKey}> added ${tasks.length} tasks to queue`, {
|
|
241
|
+
checkpointId: checkpointKey,
|
|
242
|
+
count: tasks.length
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
115
245
|
}
|
|
116
246
|
async enqueueOne(checkpointId, target) {
|
|
117
247
|
const event = await this.reader.get(target);
|
|
118
248
|
if (!event) throw new Error(`Event not found for cursor ${target.ref}`);
|
|
119
249
|
const settings = this.projection.getTaskSettings(event);
|
|
120
250
|
const task = Task.new(event, settings);
|
|
121
|
-
|
|
251
|
+
const result = await this.queue.enqueue(checkpointId, [task]);
|
|
252
|
+
const [status] = result;
|
|
253
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, [task.id]);
|
|
254
|
+
return result;
|
|
122
255
|
}
|
|
123
256
|
async checkIsProcessed(checkpointId, cursor) {
|
|
124
257
|
return await this.queue.isProcessed(checkpointId, cursor);
|
|
@@ -139,20 +272,37 @@ var FirestoreProjector = class {
|
|
|
139
272
|
const filtered = (await Promise.all(tasks.map((t) => this.reader.get(t.cursor)))).filter((t) => !!t);
|
|
140
273
|
if (!filtered.length) return [Status.DEFERRED, "No events to process in claimed tasks"];
|
|
141
274
|
const context = {
|
|
142
|
-
onProcessed: this.queue.processed.bind(this.queue),
|
|
143
|
-
checkpointId
|
|
275
|
+
onProcessed: this.queue.processed.bind(this.queue, claimer),
|
|
276
|
+
checkpointId,
|
|
277
|
+
assertBeforeInsert: this.assertBeforeInsert.bind(this, checkpointId, claimer, filtered)
|
|
144
278
|
};
|
|
145
279
|
const hasTarget = tasks.some((t) => t.id.equals(targetEventId));
|
|
146
280
|
try {
|
|
147
281
|
if ((await this.projection.process(filtered, context)).some((id) => id?.equals(targetEventId))) return [Status.SUCCESS, "Target event processed successfully"];
|
|
148
282
|
return [Status.DEFERRED, "Target event not processed yet"];
|
|
149
283
|
} catch (e) {
|
|
284
|
+
const checkpointKey = checkpointId.serialize();
|
|
285
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
286
|
+
const truncated = errorMessage.length > 200 ? `${errorMessage.slice(0, 200)}...` : errorMessage;
|
|
287
|
+
this.logger.error(`error: Checkpoint<${checkpointKey}> processing failed: ${truncated}`, {
|
|
288
|
+
checkpointId: checkpointKey,
|
|
289
|
+
error: e
|
|
290
|
+
});
|
|
150
291
|
this.config.onProcessError(e);
|
|
151
292
|
if (this._unclaim) await this.queue.unclaim(checkpointId, tasks);
|
|
152
293
|
if (!hasTarget) return [Status.DEFERRED, "Target event not in claimed batch, deferring"];
|
|
153
294
|
return [Status.FAILURE, e];
|
|
154
295
|
}
|
|
155
296
|
}
|
|
297
|
+
async assertBeforeInsert(checkpointId, claimer, events) {
|
|
298
|
+
const claimedTasks = await this.queue.claimed(checkpointId, claimer);
|
|
299
|
+
const claimedTasksMap = new Map(claimedTasks.map((t) => [t.id.serialize(), t]));
|
|
300
|
+
for (const event of events) {
|
|
301
|
+
const task = claimedTasksMap.get(event.id.serialize());
|
|
302
|
+
if (!task) throw new Error(`Task not found for event ${event.id.serialize()} in claimer ${claimer.serialize()}`);
|
|
303
|
+
if (task.claimIds?.[0] !== claimer.serialize()) throw new Error(`Task ${task.id.serialize()} claimer mismatch: expected ${claimer.serialize()}, found ${task.claimIds?.[0]}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
156
306
|
};
|
|
157
307
|
var AlreadyEnqueuedError = class extends Error {
|
|
158
308
|
constructor() {
|
|
@@ -195,10 +345,13 @@ var FirestoreQueueStore = class {
|
|
|
195
345
|
async claim(checkpointId, claimer, tasks) {
|
|
196
346
|
const batch = this.collection.firestore.batch();
|
|
197
347
|
for (const task of tasks) {
|
|
348
|
+
if (task.claimIds.length > 0) throw new Error(`Task ${task.id.serialize()} is already claimed by ${task.claimIds.join(", ")}`);
|
|
198
349
|
const ref = this.queued(checkpointId, task.id);
|
|
199
350
|
batch.update(ref, {
|
|
200
351
|
claimer: claimer.serialize(),
|
|
201
352
|
claimedAt: FieldValue.serverTimestamp(),
|
|
353
|
+
[`claimsMetadata.${claimer.serialize()}`]: { claimedAt: FieldValue.serverTimestamp() },
|
|
354
|
+
claimIds: FieldValue.arrayUnion(claimer.serialize()),
|
|
202
355
|
attempts: FieldValue.increment(1),
|
|
203
356
|
remaining: FieldValue.increment(-1)
|
|
204
357
|
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
@@ -225,9 +378,9 @@ var FirestoreQueueStore = class {
|
|
|
225
378
|
});
|
|
226
379
|
const expiredTasks = [];
|
|
227
380
|
for (const task of tasks) {
|
|
228
|
-
const
|
|
381
|
+
const originalClaimIds = task.claimIds;
|
|
229
382
|
task.checkTimeout();
|
|
230
|
-
if (
|
|
383
|
+
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
231
384
|
}
|
|
232
385
|
if (expiredTasks.length > 0) {
|
|
233
386
|
const batch = this.collection.firestore.batch();
|
|
@@ -236,20 +389,19 @@ var FirestoreQueueStore = class {
|
|
|
236
389
|
batch.update(ref, {
|
|
237
390
|
claimer: FieldValue.delete(),
|
|
238
391
|
claimedAt: FieldValue.delete(),
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
392
|
+
claimIds: task.claimIds
|
|
393
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
242
394
|
}
|
|
243
395
|
await batch.commit();
|
|
244
396
|
}
|
|
245
397
|
return tasks;
|
|
246
398
|
}
|
|
247
399
|
async claimed(checkpointId, claimer) {
|
|
248
|
-
return (await this.queue(checkpointId).where("
|
|
400
|
+
return (await this.queue(checkpointId).where("claimIds", "array-contains", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
|
|
249
401
|
const data = this.converter.fromFirestoreSnapshot(doc);
|
|
250
402
|
const timestamp = doc.updateTime ? this.timestampToMicroseconds(doc.updateTime) : void 0;
|
|
251
403
|
return Task.deserializeWithLastUpdateTime(data, timestamp);
|
|
252
|
-
});
|
|
404
|
+
}).filter((task) => task.claimIds[0] === claimer.serialize());
|
|
253
405
|
}
|
|
254
406
|
async unclaim(checkpointId, tasks) {
|
|
255
407
|
const batch = this.collection.firestore.batch();
|
|
@@ -257,8 +409,9 @@ var FirestoreQueueStore = class {
|
|
|
257
409
|
const ref = this.queued(checkpointId, task.id);
|
|
258
410
|
batch.update(ref, {
|
|
259
411
|
claimer: FieldValue.delete(),
|
|
260
|
-
claimedAt: FieldValue.delete()
|
|
261
|
-
|
|
412
|
+
claimedAt: FieldValue.delete(),
|
|
413
|
+
claimIds: FieldValue.arrayRemove(task.currentClaimId)
|
|
414
|
+
}, { lastUpdateTime: this.microsecondsToTimestamp(task.lastUpdateTime) });
|
|
262
415
|
}
|
|
263
416
|
await batch.commit();
|
|
264
417
|
}
|
|
@@ -289,16 +442,22 @@ var FirestoreQueueStore = class {
|
|
|
289
442
|
queued(id, eventId) {
|
|
290
443
|
return this.queue(id).doc(eventId.serialize());
|
|
291
444
|
}
|
|
292
|
-
async processed(id, eventIds, context = {}) {
|
|
445
|
+
async processed(claimerId, id, eventIds, context = {}) {
|
|
293
446
|
const { transaction: trx, batchWriter } = context;
|
|
294
447
|
if (trx) {
|
|
295
448
|
for (const eventId of eventIds) {
|
|
296
449
|
const ref = this.queued(id, eventId);
|
|
297
|
-
trx.transaction.update(ref, {
|
|
450
|
+
trx.transaction.update(ref, {
|
|
451
|
+
processed: true,
|
|
452
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: FieldValue.serverTimestamp()
|
|
453
|
+
});
|
|
298
454
|
}
|
|
299
455
|
return;
|
|
300
456
|
}
|
|
301
|
-
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
457
|
+
await Promise.all(eventIds.map((eventId) => this.queued(id, eventId).update({
|
|
458
|
+
processed: true,
|
|
459
|
+
[`claimsMetadata.${claimerId.serialize()}.processedAt`]: FieldValue.serverTimestamp()
|
|
460
|
+
})));
|
|
302
461
|
}
|
|
303
462
|
async getTailCursor(id) {
|
|
304
463
|
const tailDoc = (await this.queue(id).where("remaining", ">", 0).orderBy("occurredAt", "asc").orderBy("revision", "asc").limit(1).get()).docs[0];
|
|
@@ -358,6 +517,8 @@ var FirestoreQueueStore = class {
|
|
|
358
517
|
processed: true,
|
|
359
518
|
claimer: void 0,
|
|
360
519
|
claimedAt: void 0,
|
|
520
|
+
claimsMetadata: {},
|
|
521
|
+
claimIds: [],
|
|
361
522
|
lock: new Lock({}),
|
|
362
523
|
remaining: 1,
|
|
363
524
|
claimTimeout: 0,
|
|
@@ -384,6 +545,11 @@ var Task = class Task extends Shape({
|
|
|
384
545
|
processed: Boolean,
|
|
385
546
|
claimer: Optional(String),
|
|
386
547
|
claimedAt: Optional(MicrosecondTimestamp),
|
|
548
|
+
claimsMetadata: Mapping([{
|
|
549
|
+
claimedAt: MicrosecondTimestamp,
|
|
550
|
+
processedAt: Optional(MicrosecondTimestamp)
|
|
551
|
+
}]),
|
|
552
|
+
claimIds: [String],
|
|
387
553
|
lock: Lock,
|
|
388
554
|
skipAfter: Number,
|
|
389
555
|
remaining: Number,
|
|
@@ -406,6 +572,8 @@ var Task = class Task extends Shape({
|
|
|
406
572
|
claimer: void 0,
|
|
407
573
|
processed: false,
|
|
408
574
|
claimedAt: void 0,
|
|
575
|
+
claimsMetadata: {},
|
|
576
|
+
claimIds: [],
|
|
409
577
|
lock: config.lock,
|
|
410
578
|
claimTimeout: config.claimTimeout,
|
|
411
579
|
skipAfter: config.skipAfter,
|
|
@@ -417,8 +585,11 @@ var Task = class Task extends Shape({
|
|
|
417
585
|
lastUpdateTime: void 0
|
|
418
586
|
});
|
|
419
587
|
}
|
|
588
|
+
get currentClaimId() {
|
|
589
|
+
return this.claimIds.at(-1);
|
|
590
|
+
}
|
|
420
591
|
get isProcessing() {
|
|
421
|
-
return
|
|
592
|
+
return this.currentClaimId !== void 0;
|
|
422
593
|
}
|
|
423
594
|
get isProcessed() {
|
|
424
595
|
return !!this.processed;
|
|
@@ -430,18 +601,18 @@ var Task = class Task extends Shape({
|
|
|
430
601
|
return this.attempts > this.isolateAfter;
|
|
431
602
|
}
|
|
432
603
|
checkTimeout() {
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
this.remaining -= 1;
|
|
439
|
-
}
|
|
604
|
+
const claimer = this.currentClaimId;
|
|
605
|
+
if (!claimer) return;
|
|
606
|
+
const claimInfo = this.claimsMetadata[claimer];
|
|
607
|
+
if (!claimInfo || !claimInfo.claimedAt) return;
|
|
608
|
+
if (MicrosecondTimestamp.now().micros - claimInfo.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) this.claimIds = this.claimIds.filter((id) => id !== claimer);
|
|
440
609
|
}
|
|
441
610
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
442
611
|
return Task.deserialize({
|
|
443
612
|
...data,
|
|
444
|
-
lastUpdateTime: timestamp
|
|
613
|
+
lastUpdateTime: timestamp,
|
|
614
|
+
claimIds: data.claimIds || [],
|
|
615
|
+
claimsMetadata: data.claimsMetadata || {}
|
|
445
616
|
});
|
|
446
617
|
}
|
|
447
618
|
static batch(tasks) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddd-ts/event-sourcing-firestore",
|
|
3
|
-
"version": "0.0.0-fix-
|
|
3
|
+
"version": "0.0.0-fix-get-handler.2",
|
|
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.0-fix-
|
|
14
|
-
"@ddd-ts/shape": "0.0.0-fix-
|
|
15
|
-
"@ddd-ts/store-firestore": "0.0.0-fix-
|
|
16
|
-
"@ddd-ts/traits": "0.0.0-fix-
|
|
17
|
-
"@ddd-ts/types": "0.0.0-fix-
|
|
13
|
+
"@ddd-ts/core": "0.0.0-fix-get-handler.2",
|
|
14
|
+
"@ddd-ts/shape": "0.0.0-fix-get-handler.2",
|
|
15
|
+
"@ddd-ts/store-firestore": "0.0.0-fix-get-handler.2",
|
|
16
|
+
"@ddd-ts/traits": "0.0.0-fix-get-handler.2",
|
|
17
|
+
"@ddd-ts/types": "0.0.0-fix-get-handler.2",
|
|
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.0-fix-
|
|
23
|
-
"@ddd-ts/tests": "0.0.0-fix-
|
|
24
|
-
"@ddd-ts/tools": "0.0.0-fix-
|
|
25
|
-
"@ddd-ts/types": "0.0.0-fix-
|
|
22
|
+
"@ddd-ts/shape": "0.0.0-fix-get-handler.2",
|
|
23
|
+
"@ddd-ts/tests": "0.0.0-fix-get-handler.2",
|
|
24
|
+
"@ddd-ts/tools": "0.0.0-fix-get-handler.2",
|
|
25
|
+
"@ddd-ts/types": "0.0.0-fix-get-handler.2",
|
|
26
26
|
"@types/jest": "^29.5.1"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|