@ttt-productions/notification-core 0.15.11 → 0.15.12

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.
@@ -53,8 +53,10 @@ export declare function archiveNotificationWithGeneration(db: ServerFirestore, p
53
53
  * move. Invoked in the write phase ONLY on the successful 'archived' path —
54
54
  * never on replay/conflict/generation-mismatch/missing — so the audit is
55
55
  * written iff the archive actually happens. Honor reads-before-writes: do not
56
- * read inside the hook.
56
+ * read inside the hook. The hook is awaited inside the transaction body, so a
57
+ * throw or a rejected promise fails the archive instead of committing it
58
+ * without its write.
57
59
  */
58
- auditWrite?: (txn: ServerTransaction) => void;
60
+ auditWrite?: (txn: ServerTransaction) => void | Promise<void>;
59
61
  }): Promise<ArchiveOutcome>;
60
62
  //# sourceMappingURL=observed-generation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"observed-generation.d.ts","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEzE;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,eAAe,EACnB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE;IAAE,0BAA0B,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEtG;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE;IACN,SAAS,EAAE,YAAY,CAAC;IACxB,mHAAmH;IACnH,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B,EAAE,MAAM,CAAC;IACnC,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GACA,OAAO,CAAC,cAAc,CAAC,CAoDzB"}
1
+ {"version":3,"file":"observed-generation.d.ts","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEzE;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,EAAE,EAAE,eAAe,EACnB,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE;IAAE,0BAA0B,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3D,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,qBAAqB,GAAG,SAAS,CAAC;AAEtG;;;;;;;GAOG;AACH,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE;IACN,SAAS,EAAE,YAAY,CAAC;IACxB,mHAAmH;IACnH,UAAU,EAAE,YAAY,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,0BAA0B,EAAE,MAAM,CAAC;IACnC,qFAAqF;IACrF,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/D,GACA,OAAO,CAAC,cAAc,CAAC,CAoDzB"}
@@ -77,7 +77,7 @@ export async function archiveNotificationWithGeneration(db, params) {
77
77
  // Compose any caller-supplied write (e.g. an audit event) into THIS
78
78
  // transaction so it commits atomically with the archive. Only reached on the
79
79
  // successful path, after all reads, so reads-before-writes is preserved.
80
- auditWrite?.(tx);
80
+ await auditWrite?.(tx);
81
81
  return 'archived';
82
82
  });
83
83
  }
@@ -1 +1 @@
1
- {"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAmB,EACnB,SAAuB,EACvB,MAA4D;IAE5D,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QAC5D,IAAK,IAAI,CAAC,kBAAyC,KAAK,MAAM,CAAC,0BAA0B,EAAE,CAAC;YAC1F,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;YAChC,gBAAgB,EAAE,MAAM,CAAC,0BAA0B;SACpD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAID;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAAmB,EACnB,MAwBC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,GAAG,MAAM,CAAC;IAEX,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,+CAA+C;QAC/C,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YACvE,OAAQ,QAAQ,CAAC,WAAkC,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QACxE,IAAK,UAAU,CAAC,kBAAyC,KAAK,0BAA0B,EAAE,CAAC;YACzF,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAA4B;YAC1C,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,SAAS;YACT,WAAW;YACX,QAAQ,EAAE,SAAS,CAAC,EAAE;YACtB,0BAA0B;YAC1B,QAAQ;YACR,aAAa;YACb,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC;SAC1C,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;QAE9D,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC/B,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrB,oEAAoE;QACpE,6EAA6E;QAC7E,yEAAyE;QACzE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACjB,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Observed-generation seen/archive protocol (notification redesign — P1).\n *\n * The SEEN/ARCHIVE precondition compares the card's current opaque\n * `activityGeneration` against the generation the client actually observed when\n * it rendered the row — so activity arriving after render (or a delete+recreate\n * that rotated the token) is never silently swallowed. Archive additionally\n * carries a retry-stable `requestId`; the deterministic history doc id makes a\n * replay return the stored result and touch nothing.\n *\n * GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)\n * via its own canonical hash (in the consuming app's core package) and passes the\n * refs in; this module owns only the transactional state machine, not the id formulas.\n */\n\nimport type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\n\nexport type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';\n\n/**\n * Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`\n * still matches the observed one; otherwise no-op (the row stays unseen so newer\n * activity is never swallowed).\n */\nexport async function markNotificationSeenWithGeneration(\n db: ServerFirestore,\n activeRef: ServerDocRef,\n params: { observedActivityGeneration: string; now?: number },\n): Promise<MarkSeenOutcome> {\n return db.runTransaction(async (tx) => {\n const snap = await tx.get(activeRef);\n if (!snap.exists) return 'missing';\n const data = (snap.data() ?? {}) as Record<string, unknown>;\n if ((data.activityGeneration as string | undefined) !== params.observedActivityGeneration) {\n return 'generation-mismatch';\n }\n tx.update(activeRef, {\n seenAt: params.now ?? Date.now(),\n seenAtGeneration: params.observedActivityGeneration,\n });\n return 'seen';\n });\n}\n\nexport type ArchiveOutcome = 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';\n\n/**\n * Archive one active card into history, idempotently and replay-safely.\n *\n * - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).\n * - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).\n * - first-seen: archive only if the active card's `activityGeneration` still\n * matches the observed generation; else `generation-mismatch` (leave it active).\n */\nexport async function archiveNotificationWithGeneration(\n db: ServerFirestore,\n params: {\n activeRef: ServerDocRef;\n /** Deterministic history doc id = hash('notification-archive', category, audienceScope, requestId) (app-built). */\n historyRef: ServerDocRef;\n requestId: string;\n observedActivityGeneration: string;\n /** Deterministic payload hash (app-built) — the replay-vs-conflict discriminator. */\n payloadHash: string;\n category: string;\n audienceScope: string;\n /** Native-TTL expiry for the history doc; converted to a Timestamp via the factory. */\n expireAtMs: number;\n timestampFromMillis: (ms: number) => unknown;\n /** Admin history quick-access field (admin category only). */\n handledBy?: string;\n /**\n * Optional hook to compose additional writes (e.g. an audit event) INTO the\n * same archive transaction, so they commit atomically with the active→history\n * move. Invoked in the write phase ONLY on the successful 'archived' path —\n * never on replay/conflict/generation-mismatch/missing — so the audit is\n * written iff the archive actually happens. Honor reads-before-writes: do not\n * read inside the hook.\n */\n auditWrite?: (txn: ServerTransaction) => void;\n },\n): Promise<ArchiveOutcome> {\n const {\n activeRef,\n historyRef,\n requestId,\n observedActivityGeneration,\n payloadHash,\n category,\n audienceScope,\n expireAtMs,\n timestampFromMillis,\n handledBy,\n auditWrite,\n } = params;\n\n return db.runTransaction(async (tx) => {\n // Reads first (Firestore reads-before-writes).\n const historySnap = await tx.get(historyRef);\n if (historySnap.exists) {\n const existing = (historySnap.data() ?? {}) as Record<string, unknown>;\n return (existing.payloadHash as string | undefined) === payloadHash ? 'replayed' : 'conflict';\n }\n const activeSnap = await tx.get(activeRef);\n if (!activeSnap.exists) return 'missing';\n const activeData = (activeSnap.data() ?? {}) as Record<string, unknown>;\n if ((activeData.activityGeneration as string | undefined) !== observedActivityGeneration) {\n return 'generation-mismatch';\n }\n\n const now = Date.now();\n const historyDoc: Record<string, unknown> = {\n archiveOccurrenceId: historyRef.id,\n requestId,\n payloadHash,\n activeId: activeRef.id,\n observedActivityGeneration,\n category,\n audienceScope,\n archivedSnapshot: activeData,\n archivedAt: now,\n expireAt: timestampFromMillis(expireAtMs),\n };\n if (handledBy !== undefined) historyDoc.handledBy = handledBy;\n\n tx.set(historyRef, historyDoc);\n tx.delete(activeRef);\n // Compose any caller-supplied write (e.g. an audit event) into THIS\n // transaction so it commits atomically with the archive. Only reached on the\n // successful path, after all reads, so reads-before-writes is preserved.\n auditWrite?.(tx);\n return 'archived';\n });\n}\n"]}
1
+ {"version":3,"file":"observed-generation.js","sourceRoot":"","sources":["../../src/server/observed-generation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAMH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kCAAkC,CACtD,EAAmB,EACnB,SAAuB,EACvB,MAA4D;IAE5D,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACnC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QAC5D,IAAK,IAAI,CAAC,kBAAyC,KAAK,MAAM,CAAC,0BAA0B,EAAE,CAAC;YAC1F,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QACD,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE;YACnB,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE;YAChC,gBAAgB,EAAE,MAAM,CAAC,0BAA0B;SACpD,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC,CAAC;AACL,CAAC;AAID;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iCAAiC,CACrD,EAAmB,EACnB,MA0BC;IAED,MAAM,EACJ,SAAS,EACT,UAAU,EACV,SAAS,EACT,0BAA0B,EAC1B,WAAW,EACX,QAAQ,EACR,aAAa,EACb,UAAU,EACV,mBAAmB,EACnB,SAAS,EACT,UAAU,GACX,GAAG,MAAM,CAAC;IAEX,OAAO,EAAE,CAAC,cAAc,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;QACpC,+CAA+C;QAC/C,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC7C,IAAI,WAAW,CAAC,MAAM,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;YACvE,OAAQ,QAAQ,CAAC,WAAkC,KAAK,WAAW,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;QAChG,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,CAAC,MAAM;YAAE,OAAO,SAAS,CAAC;QACzC,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,CAA4B,CAAC;QACxE,IAAK,UAAU,CAAC,kBAAyC,KAAK,0BAA0B,EAAE,CAAC;YACzF,OAAO,qBAAqB,CAAC;QAC/B,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,UAAU,GAA4B;YAC1C,mBAAmB,EAAE,UAAU,CAAC,EAAE;YAClC,SAAS;YACT,WAAW;YACX,QAAQ,EAAE,SAAS,CAAC,EAAE;YACtB,0BAA0B;YAC1B,QAAQ;YACR,aAAa;YACb,gBAAgB,EAAE,UAAU;YAC5B,UAAU,EAAE,GAAG;YACf,QAAQ,EAAE,mBAAmB,CAAC,UAAU,CAAC;SAC1C,CAAC;QACF,IAAI,SAAS,KAAK,SAAS;YAAE,UAAU,CAAC,SAAS,GAAG,SAAS,CAAC;QAE9D,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC/B,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACrB,oEAAoE;QACpE,6EAA6E;QAC7E,yEAAyE;QACzE,MAAM,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;QACvB,OAAO,UAAU,CAAC;IACpB,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Observed-generation seen/archive protocol (notification redesign — P1).\n *\n * The SEEN/ARCHIVE precondition compares the card's current opaque\n * `activityGeneration` against the generation the client actually observed when\n * it rendered the row — so activity arriving after render (or a delete+recreate\n * that rotated the token) is never silently swallowed. Archive additionally\n * carries a retry-stable `requestId`; the deterministic history doc id makes a\n * replay return the stored result and touch nothing.\n *\n * GENERIC: the app constructs the deterministic ids (`historyRef`, `payloadHash`)\n * via its own canonical hash (in the consuming app's core package) and passes the\n * refs in; this module owns only the transactional state machine, not the id formulas.\n */\n\nimport type { ServerFirestore, ServerDocRef, ServerTransaction } from './types.js';\n\nexport type MarkSeenOutcome = 'seen' | 'generation-mismatch' | 'missing';\n\n/**\n * Set `seenAt`/`seenAtGeneration` ONLY if the card's current `activityGeneration`\n * still matches the observed one; otherwise no-op (the row stays unseen so newer\n * activity is never swallowed).\n */\nexport async function markNotificationSeenWithGeneration(\n db: ServerFirestore,\n activeRef: ServerDocRef,\n params: { observedActivityGeneration: string; now?: number },\n): Promise<MarkSeenOutcome> {\n return db.runTransaction(async (tx) => {\n const snap = await tx.get(activeRef);\n if (!snap.exists) return 'missing';\n const data = (snap.data() ?? {}) as Record<string, unknown>;\n if ((data.activityGeneration as string | undefined) !== params.observedActivityGeneration) {\n return 'generation-mismatch';\n }\n tx.update(activeRef, {\n seenAt: params.now ?? Date.now(),\n seenAtGeneration: params.observedActivityGeneration,\n });\n return 'seen';\n });\n}\n\nexport type ArchiveOutcome = 'archived' | 'replayed' | 'conflict' | 'generation-mismatch' | 'missing';\n\n/**\n * Archive one active card into history, idempotently and replay-safely.\n *\n * - history doc already exists + same `payloadHash` ⇒ `replayed` (touch NOTHING).\n * - history doc already exists + DIFFERENT `payloadHash` ⇒ `conflict` (caller alerts).\n * - first-seen: archive only if the active card's `activityGeneration` still\n * matches the observed generation; else `generation-mismatch` (leave it active).\n */\nexport async function archiveNotificationWithGeneration(\n db: ServerFirestore,\n params: {\n activeRef: ServerDocRef;\n /** Deterministic history doc id = hash('notification-archive', category, audienceScope, requestId) (app-built). */\n historyRef: ServerDocRef;\n requestId: string;\n observedActivityGeneration: string;\n /** Deterministic payload hash (app-built) — the replay-vs-conflict discriminator. */\n payloadHash: string;\n category: string;\n audienceScope: string;\n /** Native-TTL expiry for the history doc; converted to a Timestamp via the factory. */\n expireAtMs: number;\n timestampFromMillis: (ms: number) => unknown;\n /** Admin history quick-access field (admin category only). */\n handledBy?: string;\n /**\n * Optional hook to compose additional writes (e.g. an audit event) INTO the\n * same archive transaction, so they commit atomically with the active→history\n * move. Invoked in the write phase ONLY on the successful 'archived' path —\n * never on replay/conflict/generation-mismatch/missing — so the audit is\n * written iff the archive actually happens. Honor reads-before-writes: do not\n * read inside the hook. The hook is awaited inside the transaction body, so a\n * throw or a rejected promise fails the archive instead of committing it\n * without its write.\n */\n auditWrite?: (txn: ServerTransaction) => void | Promise<void>;\n },\n): Promise<ArchiveOutcome> {\n const {\n activeRef,\n historyRef,\n requestId,\n observedActivityGeneration,\n payloadHash,\n category,\n audienceScope,\n expireAtMs,\n timestampFromMillis,\n handledBy,\n auditWrite,\n } = params;\n\n return db.runTransaction(async (tx) => {\n // Reads first (Firestore reads-before-writes).\n const historySnap = await tx.get(historyRef);\n if (historySnap.exists) {\n const existing = (historySnap.data() ?? {}) as Record<string, unknown>;\n return (existing.payloadHash as string | undefined) === payloadHash ? 'replayed' : 'conflict';\n }\n const activeSnap = await tx.get(activeRef);\n if (!activeSnap.exists) return 'missing';\n const activeData = (activeSnap.data() ?? {}) as Record<string, unknown>;\n if ((activeData.activityGeneration as string | undefined) !== observedActivityGeneration) {\n return 'generation-mismatch';\n }\n\n const now = Date.now();\n const historyDoc: Record<string, unknown> = {\n archiveOccurrenceId: historyRef.id,\n requestId,\n payloadHash,\n activeId: activeRef.id,\n observedActivityGeneration,\n category,\n audienceScope,\n archivedSnapshot: activeData,\n archivedAt: now,\n expireAt: timestampFromMillis(expireAtMs),\n };\n if (handledBy !== undefined) historyDoc.handledBy = handledBy;\n\n tx.set(historyRef, historyDoc);\n tx.delete(activeRef);\n // Compose any caller-supplied write (e.g. an audit event) into THIS\n // transaction so it commits atomically with the archive. Only reached on the\n // successful path, after all reads, so reads-before-writes is preserved.\n await auditWrite?.(tx);\n return 'archived';\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/notification-core",
3
- "version": "0.15.11",
3
+ "version": "0.15.12",
4
4
  "description": "Shared notification system for TTT Productions apps — active/history two-tier architecture with dedup, batch processing, and themed UI components",
5
5
  "repository": {
6
6
  "type": "git",