@ddd-ts/event-sourcing-firestore 0.0.0-fix-build.1 → 0.0.0-fix-get-handler.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/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
@@ -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,GACV,MAAM,kCAAkC,CAAC"}
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 { Firestore, WriteBatch } from "firebase-admin/firestore";
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";
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
- 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
- });
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":"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"}
1
+ {"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,MAAM,EACN,YAAY,EACZ,OAAO,EACP,KAAK,QAAQ,EACb,KAAK,KAAK,EACV,KAAK,YAAY,EACjB,IAAI,EAEJ,qBAAqB,EACrB,KAAK,UAAU,EAChB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,OAAO,EAAE,oBAAoB,EAAmB,MAAM,eAAe,CAAC;AAC/E,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,SAAS,EAET,UAAU,EACX,MAAM,0BAA0B,CAAC;AAmBlC,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACjE;AASD,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,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,6CAA6C;IAC7C,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACvC,6CAA6C;IAC7C,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,wBAUd;IAGH,OAAO,KAAK,MAAM,GAEjB;IAEM,OAAO;IAwBd,OAAO,CAAC,qBAAqB,CAGf;IACd,OAAO,CAAC,eAAe,CAAkC;IAMzD,OAAO,CAAC,cAAc,CAGR;IAEd,OAAO,CAAC,mBAAmB;IASrB,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,QAAQ,CAAC;YA8GlC,SAAS;YAsDT,SAAS;YAKT,OAAO;YAmEP,YAAY;YAKZ,gBAAgB;YAahB,OAAO;YAwCP,UAAU;YAmBV,gBAAgB;YAKhB,cAAc;YAKd,UAAU;YAcV,aAAa;YA+Db,kBAAkB;CAyBjC;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;IA6BF,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,SAsBhD;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;IAWb,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,EAAE;CA0DjC"}
@@ -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 target = await this.getCursor(savedChange);
57
- if (!target) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
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
- throw new Error(`Failed to handle event ${savedChange.id.serialize()}: ${errors.join(", ")}`);
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) return [Status.SUCCESS, "Target event already 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
- return await this.queue.enqueue(checkpointId, tasks);
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
- return await this.queue.enqueue(checkpointId, [task]);
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 originalClaimer = task.claimer;
382
+ const originalClaimIds = task.claimIds;
230
383
  task.checkTimeout();
231
- if (originalClaimer && !task.claimer) expiredTasks.push(task);
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
- attempts: task.attempts,
241
- remaining: task.remaining
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("claimer", "==", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
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, { processed: true });
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({ processed: true })));
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 !!this.claimer;
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
- if (!this.claimedAt) return;
435
- if (_ddd_ts_shape.MicrosecondTimestamp.now().micros - this.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) {
436
- this.claimedAt = void 0;
437
- this.claimer = void 0;
438
- this.attempts += 1;
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 target = await this.getCursor(savedChange);
56
- if (!target) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
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
- throw new Error(`Failed to handle event ${savedChange.id.serialize()}: ${errors.join(", ")}`);
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) return [Status.SUCCESS, "Target event already 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
- return await this.queue.enqueue(checkpointId, tasks);
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
- return await this.queue.enqueue(checkpointId, [task]);
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 originalClaimer = task.claimer;
381
+ const originalClaimIds = task.claimIds;
229
382
  task.checkTimeout();
230
- if (originalClaimer && !task.claimer) expiredTasks.push(task);
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
- attempts: task.attempts,
240
- remaining: task.remaining
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("claimer", "==", claimer.serialize()).orderBy("occurredAt", "asc").orderBy("revision", "asc").get()).docs.map((doc) => {
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, { processed: true });
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({ processed: true })));
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 !!this.claimer;
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
- if (!this.claimedAt) return;
434
- if (MicrosecondTimestamp.now().micros - this.claimedAt.micros > BigInt(this.claimTimeout) * 1000n) {
435
- this.claimedAt = void 0;
436
- this.claimer = void 0;
437
- this.attempts += 1;
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-build.1",
3
+ "version": "0.0.0-fix-get-handler.1",
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-build.1",
14
- "@ddd-ts/shape": "0.0.0-fix-build.1",
15
- "@ddd-ts/store-firestore": "0.0.0-fix-build.1",
16
- "@ddd-ts/traits": "0.0.0-fix-build.1",
17
- "@ddd-ts/types": "0.0.0-fix-build.1",
13
+ "@ddd-ts/core": "0.0.0-fix-get-handler.1",
14
+ "@ddd-ts/shape": "0.0.0-fix-get-handler.1",
15
+ "@ddd-ts/store-firestore": "0.0.0-fix-get-handler.1",
16
+ "@ddd-ts/traits": "0.0.0-fix-get-handler.1",
17
+ "@ddd-ts/types": "0.0.0-fix-get-handler.1",
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-build.1",
23
- "@ddd-ts/tests": "0.0.0-fix-build.1",
24
- "@ddd-ts/tools": "0.0.0-fix-build.1",
25
- "@ddd-ts/types": "0.0.0-fix-build.1",
22
+ "@ddd-ts/shape": "0.0.0-fix-get-handler.1",
23
+ "@ddd-ts/tests": "0.0.0-fix-get-handler.1",
24
+ "@ddd-ts/tools": "0.0.0-fix-get-handler.1",
25
+ "@ddd-ts/types": "0.0.0-fix-get-handler.1",
26
26
  "@types/jest": "^29.5.1"
27
27
  },
28
28
  "exports": {