@things-factory/webhook 10.1.37 → 10.1.38

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.
@@ -1,7 +1,15 @@
1
+ import { WebhookDestination } from './webhook-destination/webhook-destination';
2
+ import { WebhookDestinationResolver } from './webhook-destination/webhook-destination-resolver';
3
+ import { WebhookOutboxEvent } from './webhook-outbox-event/webhook-outbox-event';
4
+ import { WebhookSequence } from './webhook-sequence/webhook-sequence';
1
5
  export * from './webhook-destination/webhook-destination';
2
6
  export * from './webhook-destination/webhook-destination-resolver';
3
7
  export * from './webhook-outbox-event/webhook-outbox-event';
4
8
  export * from './webhook-sequence/webhook-sequence';
9
+ export declare const entities: (typeof WebhookDestination | typeof WebhookOutboxEvent | typeof WebhookSequence)[];
10
+ export declare const schema: {
11
+ resolverClasses: (typeof WebhookDestinationResolver)[];
12
+ };
5
13
  export * from './outbox/delivery';
6
14
  export * from './outbox/settlement';
7
15
  export * from './outbox/row-effect';
@@ -18,3 +26,4 @@ export * from './observability/trace-context';
18
26
  export * from './outbox/store';
19
27
  export * from './outbox/dispatcher';
20
28
  export * from './outbox/webhook-store';
29
+ export * from './outbox/webhook-append-store';
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.schema = exports.entities = void 0;
3
4
  const tslib_1 = require("tslib");
4
5
  /*
5
6
  * Each source file is named rather than re-exporting folder barrels.
@@ -10,10 +11,22 @@ const tslib_1 = require("tslib");
10
11
  * `work-shift/days-of-week`: the parser was in the folder barrel, not here, and the consuming
11
12
  * lane wrote its own until the miss was found.
12
13
  */
14
+ const webhook_destination_1 = require("./webhook-destination/webhook-destination");
15
+ const webhook_destination_resolver_1 = require("./webhook-destination/webhook-destination-resolver");
16
+ const webhook_outbox_event_1 = require("./webhook-outbox-event/webhook-outbox-event");
17
+ const webhook_sequence_1 = require("./webhook-sequence/webhook-sequence");
13
18
  tslib_1.__exportStar(require("./webhook-destination/webhook-destination"), exports);
14
19
  tslib_1.__exportStar(require("./webhook-destination/webhook-destination-resolver"), exports);
15
20
  tslib_1.__exportStar(require("./webhook-outbox-event/webhook-outbox-event"), exports);
16
21
  tslib_1.__exportStar(require("./webhook-sequence/webhook-sequence"), exports);
22
+ /*
23
+ * ⚠ **The shell builds the database and the schema from these two names.** Without them the module's
24
+ * tables are never created in any application and the destination door is in no schema - the files stand
25
+ * and nothing reaches them. That was the state through 10.1.37 (found 2026-09-24 by the first application to
26
+ * append: its database had no webhook table at all). `test/append-store-db.test.ts` holds this line.
27
+ */
28
+ exports.entities = [webhook_destination_1.WebhookDestination, webhook_outbox_event_1.WebhookOutboxEvent, webhook_sequence_1.WebhookSequence];
29
+ exports.schema = { resolverClasses: [webhook_destination_resolver_1.WebhookDestinationResolver] };
17
30
  /*
18
31
  * The outbox machine, moved from operato-plant (ADR-0078 decision 2). Step ①: the pure judgement —
19
32
  * envelope, signature, reading the receiver's answer, settlement, what an answer does to our rows,
@@ -42,4 +55,5 @@ tslib_1.__exportStar(require("./observability/trace-context"), exports);
42
55
  tslib_1.__exportStar(require("./outbox/store"), exports);
43
56
  tslib_1.__exportStar(require("./outbox/dispatcher"), exports);
44
57
  tslib_1.__exportStar(require("./outbox/webhook-store"), exports);
58
+ tslib_1.__exportStar(require("./outbox/webhook-append-store"), exports);
45
59
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;GAQG;AACH,oFAAyD;AACzD,6FAAkE;AAClE,sFAA2D;AAC3D,8EAAmD;AAEnD;;;;GAIG;AACH,4DAAiC;AACjC,8DAAmC;AACnC,8DAAmC;AACnC,8DAAmC;AACnC,kEAAuC;AACvC,4DAAiC;AACjC,gEAAqC;AACrC,wDAA6B;AAC7B,uEAA4C;AAE5C;;;GAGG;AACH,0DAA+B;AAC/B,6DAAkC;AAElC;+EAC+E;AAC/E,uEAA4C;AAC5C,wEAA6C;AAE7C,6GAA6G;AAC7G,yDAA8B;AAC9B,8DAAmC;AACnC,iEAAsC","sourcesContent":["/*\n * Each source file is named rather than re-exporting folder barrels.\n *\n * A name added to a folder barrel but not listed here stops at the package boundary, and the\n * consumer's error is `TS2305: has no exported member` — which reads like the name does not\n * exist rather than like it is unreachable. That happened on 2026-09-07 with\n * `work-shift/days-of-week`: the parser was in the folder barrel, not here, and the consuming\n * lane wrote its own until the miss was found.\n */\nexport * from './webhook-destination/webhook-destination'\nexport * from './webhook-destination/webhook-destination-resolver'\nexport * from './webhook-outbox-event/webhook-outbox-event'\nexport * from './webhook-sequence/webhook-sequence'\n\n/*\n * The outbox machine, moved from operato-plant (ADR-0078 decision 2). Step ①: the pure judgement —\n * envelope, signature, reading the receiver's answer, settlement, what an answer does to our rows,\n * the sequence report, reconciliation, standing. None of these touch a store.\n */\nexport * from './outbox/delivery'\nexport * from './outbox/settlement'\nexport * from './outbox/row-effect'\nexport * from './outbox/seq-report'\nexport * from './outbox/reconciliation'\nexport * from './outbox/standing'\nexport * from './outbox/requeue-note'\nexport * from './outbox/page'\nexport * from './outbox/writing-transaction'\n\n/*\n * Step ②: the back row. Writing a fact down takes the caller's open transaction - in the type, not only\n * in a check - because the number and the row have to commit together or the number burns.\n */\nexport * from './outbox/append'\nexport * from './outbox/pump-gate'\n\n/* What a delivery needs to say \"when\" and \"which request\" — moved from operato-plant with the dispatcher's\n * needs in mind (ADR-0078 decision 1). The inbound side reads the same two. */\nexport * from './observability/clock-offset'\nexport * from './observability/trace-context'\n\n/* Step ②: the back row. One dispatcher over a store interface; `webhookStore` is this module's own store. */\nexport * from './outbox/store'\nexport * from './outbox/dispatcher'\nexport * from './outbox/webhook-store'\n\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;GAQG;AACH,mFAA8E;AAC9E,qGAA+F;AAC/F,sFAAgF;AAChF,0EAAqE;AAErE,oFAAyD;AACzD,6FAAkE;AAClE,sFAA2D;AAC3D,8EAAmD;AAEnD;;;;;GAKG;AACU,QAAA,QAAQ,GAAG,CAAC,wCAAkB,EAAE,yCAAkB,EAAE,kCAAe,CAAC,CAAA;AAEpE,QAAA,MAAM,GAAG,EAAE,eAAe,EAAE,CAAC,yDAA0B,CAAC,EAAE,CAAA;AAEvE;;;;GAIG;AACH,4DAAiC;AACjC,8DAAmC;AACnC,8DAAmC;AACnC,8DAAmC;AACnC,kEAAuC;AACvC,4DAAiC;AACjC,gEAAqC;AACrC,wDAA6B;AAC7B,uEAA4C;AAE5C;;;GAGG;AACH,0DAA+B;AAC/B,6DAAkC;AAElC;+EAC+E;AAC/E,uEAA4C;AAC5C,wEAA6C;AAE7C,6GAA6G;AAC7G,yDAA8B;AAC9B,8DAAmC;AACnC,iEAAsC;AACtC,wEAA6C","sourcesContent":["/*\n * Each source file is named rather than re-exporting folder barrels.\n *\n * A name added to a folder barrel but not listed here stops at the package boundary, and the\n * consumer's error is `TS2305: has no exported member` — which reads like the name does not\n * exist rather than like it is unreachable. That happened on 2026-09-07 with\n * `work-shift/days-of-week`: the parser was in the folder barrel, not here, and the consuming\n * lane wrote its own until the miss was found.\n */\nimport { WebhookDestination } from './webhook-destination/webhook-destination'\nimport { WebhookDestinationResolver } from './webhook-destination/webhook-destination-resolver'\nimport { WebhookOutboxEvent } from './webhook-outbox-event/webhook-outbox-event'\nimport { WebhookSequence } from './webhook-sequence/webhook-sequence'\n\nexport * from './webhook-destination/webhook-destination'\nexport * from './webhook-destination/webhook-destination-resolver'\nexport * from './webhook-outbox-event/webhook-outbox-event'\nexport * from './webhook-sequence/webhook-sequence'\n\n/*\n * ⚠ **The shell builds the database and the schema from these two names.** Without them the module's\n * tables are never created in any application and the destination door is in no schema - the files stand\n * and nothing reaches them. That was the state through 10.1.37 (found 2026-09-24 by the first application to\n * append: its database had no webhook table at all). `test/append-store-db.test.ts` holds this line.\n */\nexport const entities = [WebhookDestination, WebhookOutboxEvent, WebhookSequence]\n\nexport const schema = { resolverClasses: [WebhookDestinationResolver] }\n\n/*\n * The outbox machine, moved from operato-plant (ADR-0078 decision 2). Step ①: the pure judgement —\n * envelope, signature, reading the receiver's answer, settlement, what an answer does to our rows,\n * the sequence report, reconciliation, standing. None of these touch a store.\n */\nexport * from './outbox/delivery'\nexport * from './outbox/settlement'\nexport * from './outbox/row-effect'\nexport * from './outbox/seq-report'\nexport * from './outbox/reconciliation'\nexport * from './outbox/standing'\nexport * from './outbox/requeue-note'\nexport * from './outbox/page'\nexport * from './outbox/writing-transaction'\n\n/*\n * Step ②: the back row. Writing a fact down takes the caller's open transaction - in the type, not only\n * in a check - because the number and the row have to commit together or the number burns.\n */\nexport * from './outbox/append'\nexport * from './outbox/pump-gate'\n\n/* What a delivery needs to say \"when\" and \"which request\" — moved from operato-plant with the dispatcher's\n * needs in mind (ADR-0078 decision 1). The inbound side reads the same two. */\nexport * from './observability/clock-offset'\nexport * from './observability/trace-context'\n\n/* Step ②: the back row. One dispatcher over a store interface; `webhookStore` is this module's own store. */\nexport * from './outbox/store'\nexport * from './outbox/dispatcher'\nexport * from './outbox/webhook-store'\nexport * from './outbox/webhook-append-store'\n\n"]}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * **What one round of the pump does with each stream** — a pure judgement: no database, no network, and
3
+ * the environment arrives as an argument (ADR-0078 decision 5 as re-ruled on 2026-09-24).
4
+ *
5
+ * ═════════════════════════════════════════════════════════════════════════
6
+ * ## Silence is the failure this guards
7
+ *
8
+ * Facts can pile up for three reasons, and none of them fails anything: no destination is declared for
9
+ * that peer, a person turned the destination off, or nothing can sign. Each sends the administrator
10
+ * somewhere different - declare it, turn it back on, set a key - so each is its own sentence, and each
11
+ * comes with a key the pump says it once by. Said every round, the line buries everything else.
12
+ *
13
+ * ## The key's name belongs to the contract
14
+ *
15
+ * `webhookSecretFrom` walks the contract's ladder - per peer, then per domain - and a product passes no
16
+ * names of its own. Pinned to the peer, two products on one installation never share a key. A product that
17
+ * ran before this ladder passes `legacyBases`; a key found there is used and said once, because a quiet
18
+ * success means nobody ever moves to the new name.
19
+ *
20
+ * The missing-key sentence names the variables. That is for the process log only - the contract says so
21
+ * (`webhookSecretMissingNote`) - and this sentence never goes into a response.
22
+ * ═════════════════════════════════════════════════════════════════════════
23
+ */
24
+ /** One stream with facts waiting. The pump reads these off the pending rows. */
25
+ export interface PendingStream {
26
+ domainId: string;
27
+ /** The tenant's name - what the key's name is built from and what a person reads in the log. */
28
+ subdomain: string;
29
+ peer: string;
30
+ scope: string;
31
+ }
32
+ /** What the destination row says, read by the caller. */
33
+ export interface DestinationReading {
34
+ endpoint: string;
35
+ active: boolean;
36
+ params?: {
37
+ receivingDomain?: string;
38
+ } & Record<string, unknown>;
39
+ }
40
+ export interface RoundNote {
41
+ /** The same cause on the same stream carries the same key, so it is said once. */
42
+ key: string;
43
+ line: string;
44
+ }
45
+ export type RoundStep = {
46
+ kind: 'deliver';
47
+ stream: PendingStream;
48
+ endpoint: string;
49
+ secret: string;
50
+ headers: Record<string, string>;
51
+ /** Clock readings are kept per peer - two peers' clocks are not one clock. */
52
+ clockKey: string;
53
+ note?: RoundNote;
54
+ } | {
55
+ kind: 'wait';
56
+ stream: PendingStream;
57
+ note: RoundNote;
58
+ };
59
+ export interface RoundOptions {
60
+ /** Names this product's installations used before the ladder. Absent for a product that never had one. */
61
+ legacyBases?: readonly string[];
62
+ }
63
+ export declare function planRound(streams: readonly PendingStream[], destinationOf: (domainId: string, peer: string) => DestinationReading | undefined, env: Record<string, string | undefined>, options?: RoundOptions): RoundStep[];
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planRound = planRound;
4
+ const ops_contract_1 = require("@operato/ops-contract");
5
+ function planRound(streams, destinationOf, env, options = {}) {
6
+ return streams.map(stream => planStream(stream, destinationOf(stream.domainId, stream.peer), env, options));
7
+ }
8
+ function planStream(stream, destination, env, options) {
9
+ const { subdomain, peer } = stream;
10
+ const where = `"${subdomain}" → ${peer}`;
11
+ if (!destination?.endpoint) {
12
+ return {
13
+ kind: 'wait',
14
+ stream,
15
+ note: {
16
+ key: `no-destination:${subdomain}:${peer}`,
17
+ line: `[webhook] facts are piling up for ${where} but no destination is declared for that peer - set it on the webhook destinations screen. Nothing is lost.`
18
+ }
19
+ };
20
+ }
21
+ if (!destination.active) {
22
+ return {
23
+ kind: 'wait',
24
+ stream,
25
+ note: {
26
+ key: `turned-off:${subdomain}:${peer}`,
27
+ line: `[webhook] facts are piling up for ${where} - its destination is turned off. Nothing is lost; turn it back on when the receiver is ready.`
28
+ }
29
+ };
30
+ }
31
+ const lookup = { purpose: 'signing', domain: subdomain, peer, ...(options.legacyBases?.length ? { legacyBases: options.legacyBases } : {}) };
32
+ const found = (0, ops_contract_1.webhookSecretFrom)(env, lookup);
33
+ /*
34
+ * The answer is a union. This package compiles without `strictNullChecks`, and there a check on `secret`
35
+ * does not narrow it - so ask which branch it is by the field only that branch has.
36
+ */
37
+ if (!('opened' in found)) {
38
+ return {
39
+ kind: 'wait',
40
+ stream,
41
+ note: {
42
+ key: `no-secret:${subdomain}:${peer}`,
43
+ line: `[webhook] facts are piling up for ${where} but nothing can sign them - ${(0, ops_contract_1.webhookSecretMissingNote)(found.tried)} Nothing is lost; they go as soon as a key is there.`
44
+ }
45
+ };
46
+ }
47
+ const { secret, opened } = found;
48
+ const receivingDomain = destination.params?.receivingDomain;
49
+ return {
50
+ kind: 'deliver',
51
+ stream,
52
+ endpoint: destination.endpoint,
53
+ secret,
54
+ /* The receiver's own tenant - never assumed to be ours. */
55
+ headers: typeof receivingDomain === 'string' && receivingDomain ? { 'x-things-factory-domain': receivingDomain } : {},
56
+ clockKey: `${subdomain}:${peer}`,
57
+ ...(opened.compatibility
58
+ ? {
59
+ note: {
60
+ key: `legacy-secret:${subdomain}:${peer}`,
61
+ line: `[webhook] ${where} is signed with ${opened.name}, a name kept for installations that ran before - move the key to ${(0, ops_contract_1.webhookSecretCandidates)({ purpose: 'signing', domain: subdomain, peer })[0].name}.`
62
+ }
63
+ }
64
+ : {})
65
+ };
66
+ }
67
+ //# sourceMappingURL=pump-round.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pump-round.js","sourceRoot":"","sources":["../../../server/service/outbox/pump-round.ts"],"names":[],"mappings":"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","sourcesContent":["import { webhookSecretCandidates, webhookSecretFrom, webhookSecretMissingNote } from '@operato/ops-contract'\n\n/**\n * **What one round of the pump does with each stream** — a pure judgement: no database, no network, and\n * the environment arrives as an argument (ADR-0078 decision 5 as re-ruled on 2026-09-24).\n *\n * ═════════════════════════════════════════════════════════════════════════\n * ## Silence is the failure this guards\n *\n * Facts can pile up for three reasons, and none of them fails anything: no destination is declared for\n * that peer, a person turned the destination off, or nothing can sign. Each sends the administrator\n * somewhere different - declare it, turn it back on, set a key - so each is its own sentence, and each\n * comes with a key the pump says it once by. Said every round, the line buries everything else.\n *\n * ## The key's name belongs to the contract\n *\n * `webhookSecretFrom` walks the contract's ladder - per peer, then per domain - and a product passes no\n * names of its own. Pinned to the peer, two products on one installation never share a key. A product that\n * ran before this ladder passes `legacyBases`; a key found there is used and said once, because a quiet\n * success means nobody ever moves to the new name.\n *\n * The missing-key sentence names the variables. That is for the process log only - the contract says so\n * (`webhookSecretMissingNote`) - and this sentence never goes into a response.\n * ═════════════════════════════════════════════════════════════════════════\n */\n\n/** One stream with facts waiting. The pump reads these off the pending rows. */\nexport interface PendingStream {\n domainId: string\n /** The tenant's name - what the key's name is built from and what a person reads in the log. */\n subdomain: string\n peer: string\n scope: string\n}\n\n/** What the destination row says, read by the caller. */\nexport interface DestinationReading {\n endpoint: string\n active: boolean\n params?: { receivingDomain?: string } & Record<string, unknown>\n}\n\nexport interface RoundNote {\n /** The same cause on the same stream carries the same key, so it is said once. */\n key: string\n line: string\n}\n\nexport type RoundStep =\n | {\n kind: 'deliver'\n stream: PendingStream\n endpoint: string\n secret: string\n headers: Record<string, string>\n /** Clock readings are kept per peer - two peers' clocks are not one clock. */\n clockKey: string\n note?: RoundNote\n }\n | { kind: 'wait'; stream: PendingStream; note: RoundNote }\n\nexport interface RoundOptions {\n /** Names this product's installations used before the ladder. Absent for a product that never had one. */\n legacyBases?: readonly string[]\n}\n\nexport function planRound(\n streams: readonly PendingStream[],\n destinationOf: (domainId: string, peer: string) => DestinationReading | undefined,\n env: Record<string, string | undefined>,\n options: RoundOptions = {}\n): RoundStep[] {\n return streams.map(stream => planStream(stream, destinationOf(stream.domainId, stream.peer), env, options))\n}\n\nfunction planStream(stream: PendingStream, destination: DestinationReading | undefined, env: Record<string, string | undefined>, options: RoundOptions): RoundStep {\n const { subdomain, peer } = stream\n const where = `\"${subdomain}\" → ${peer}`\n\n if (!destination?.endpoint) {\n return {\n kind: 'wait',\n stream,\n note: {\n key: `no-destination:${subdomain}:${peer}`,\n line: `[webhook] facts are piling up for ${where} but no destination is declared for that peer - set it on the webhook destinations screen. Nothing is lost.`\n }\n }\n }\n\n if (!destination.active) {\n return {\n kind: 'wait',\n stream,\n note: {\n key: `turned-off:${subdomain}:${peer}`,\n line: `[webhook] facts are piling up for ${where} - its destination is turned off. Nothing is lost; turn it back on when the receiver is ready.`\n }\n }\n }\n\n const lookup = { purpose: 'signing' as const, domain: subdomain, peer, ...(options.legacyBases?.length ? { legacyBases: options.legacyBases } : {}) }\n const found = webhookSecretFrom(env, lookup)\n\n /*\n * The answer is a union. This package compiles without `strictNullChecks`, and there a check on `secret`\n * does not narrow it - so ask which branch it is by the field only that branch has.\n */\n if (!('opened' in found)) {\n return {\n kind: 'wait',\n stream,\n note: {\n key: `no-secret:${subdomain}:${peer}`,\n line: `[webhook] facts are piling up for ${where} but nothing can sign them - ${webhookSecretMissingNote(found.tried)} Nothing is lost; they go as soon as a key is there.`\n }\n }\n }\n\n const { secret, opened } = found\n const receivingDomain = destination.params?.receivingDomain\n\n return {\n kind: 'deliver',\n stream,\n endpoint: destination.endpoint,\n secret,\n /* The receiver's own tenant - never assumed to be ours. */\n headers: typeof receivingDomain === 'string' && receivingDomain ? { 'x-things-factory-domain': receivingDomain } : {},\n clockKey: `${subdomain}:${peer}`,\n ...(opened.compatibility\n ? {\n note: {\n key: `legacy-secret:${subdomain}:${peer}`,\n line: `[webhook] ${where} is signed with ${opened.name}, a name kept for installations that ran before - move the key to ${webhookSecretCandidates({ purpose: 'signing', domain: subdomain, peer })[0].name}.`\n }\n }\n : {})\n }\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import { EntityManager } from 'typeorm';
2
+ import { WebhookOutboxEvent } from '../webhook-outbox-event/webhook-outbox-event.js';
3
+ import type { OpenTransaction, OutboxAppendStore } from './append.js';
4
+ import type { WebhookStream } from './webhook-store.js';
5
+ export declare function webhookAppendStore(tx: OpenTransaction<EntityManager>, stream: WebhookStream): OutboxAppendStore<object, WebhookOutboxEvent>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.webhookAppendStore = webhookAppendStore;
4
+ const webhook_outbox_event_js_1 = require("../webhook-outbox-event/webhook-outbox-event.js");
5
+ const webhook_sequence_js_1 = require("../webhook-sequence/webhook-sequence.js");
6
+ /**
7
+ * **The front row over this module's own tables** — the writing half of one stream `(domain, peer, scope)`
8
+ * (ADR-0078 decision 2 ②, decision 6).
9
+ *
10
+ * ═════════════════════════════════════════════════════════════════════════
11
+ * ## It takes the open transaction, not a manager
12
+ *
13
+ * `allocate` raises the counter and `insert` writes the rows. If the two are not one transaction, the raise
14
+ * commits on its own and a failed insert leaves a number that names no row - the receiver stops at that hole
15
+ * for good. `OpenTransaction` can only come from `openTransaction`, so the default manager does not compile
16
+ * here. `test/append-store-db.test.ts` breaks an insert after the raise and reads the counter back.
17
+ *
18
+ * ## Raise, then read
19
+ *
20
+ * `increment` takes the row lock, so two writers on the same stream queue behind each other; reading after
21
+ * raising means the run handed out belongs to this transaction. Reading first and writing `last + n` would
22
+ * hand two writers the same numbers.
23
+ *
24
+ * The first append on a stream creates the counter row. Two first appends racing on a brand-new stream
25
+ * meet at the unique index and one of the two business transactions fails whole - loud, and nothing burned.
26
+ *
27
+ * ## What the product keeps per row
28
+ *
29
+ * `lane` and `provenance` are columns (decision 3); a product passes them in `extra`. Anything else in
30
+ * `extra` is refused: a key with no column would be accepted, dropped, and reported as written.
31
+ * ═════════════════════════════════════════════════════════════════════════
32
+ */
33
+ const EXTRA_COLUMNS = new Set(['lane', 'provenance']);
34
+ function webhookAppendStore(tx, stream) {
35
+ const { domainId, peer, scope } = stream;
36
+ const where = { domain: { id: domainId }, peer, scope };
37
+ return {
38
+ async allocate(count) {
39
+ const exists = await tx.findOne(webhook_sequence_js_1.WebhookSequence, { where });
40
+ if (!exists) {
41
+ await tx.save(webhook_sequence_js_1.WebhookSequence, tx.create(webhook_sequence_js_1.WebhookSequence, { domain: { id: domainId }, peer, scope, lastSeq: 0 }));
42
+ }
43
+ await tx.increment(webhook_sequence_js_1.WebhookSequence, where, 'lastSeq', count);
44
+ const after = await tx.findOne(webhook_sequence_js_1.WebhookSequence, { where });
45
+ if (!after) {
46
+ throw new Error(`webhookAppendStore: the counter for ${peer}/${scope} disappeared while allocating`);
47
+ }
48
+ /* Some drivers hand an int column back as a string. Count in numbers. */
49
+ const lastSeq = Number(after.lastSeq);
50
+ if (!Number.isFinite(lastSeq)) {
51
+ throw new Error(`webhookAppendStore: the counter for ${peer}/${scope} is not a number: ${String(after.lastSeq)}`);
52
+ }
53
+ return lastSeq - count + 1;
54
+ },
55
+ async insert(rows) {
56
+ const entities = rows.map(row => {
57
+ const unknown = Object.keys(row.extra ?? {}).filter(key => !EXTRA_COLUMNS.has(key));
58
+ if (unknown.length) {
59
+ throw new Error(`webhookAppendStore: no column for ${unknown.join(', ')} - it would be dropped and reported as written`);
60
+ }
61
+ return tx.create(webhook_outbox_event_js_1.WebhookOutboxEvent, {
62
+ domain: { id: domainId },
63
+ peer,
64
+ scope,
65
+ seq: row.seq,
66
+ eventId: row.eventId,
67
+ eventTime: row.eventTime,
68
+ recordTime: row.recordTime,
69
+ payload: row.record,
70
+ status: webhook_outbox_event_js_1.WebhookOutboxStatus.Pending,
71
+ ...(row.extra ?? {})
72
+ });
73
+ });
74
+ return await tx.save(webhook_outbox_event_js_1.WebhookOutboxEvent, entities);
75
+ }
76
+ };
77
+ }
78
+ //# sourceMappingURL=webhook-append-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-append-store.js","sourceRoot":"","sources":["../../../server/service/outbox/webhook-append-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { WebhookOutboxEvent, WebhookOutboxStatus } from '../webhook-outbox-event/webhook-outbox-event.js'\nimport { WebhookSequence } from '../webhook-sequence/webhook-sequence.js'\nimport type { NewOutboxRow, OpenTransaction, OutboxAppendStore } from './append.js'\nimport type { WebhookStream } from './webhook-store.js'\n\n/**\n * **The front row over this module's own tables** — the writing half of one stream `(domain, peer, scope)`\n * (ADR-0078 decision 2 ②, decision 6).\n *\n * ═════════════════════════════════════════════════════════════════════════\n * ## It takes the open transaction, not a manager\n *\n * `allocate` raises the counter and `insert` writes the rows. If the two are not one transaction, the raise\n * commits on its own and a failed insert leaves a number that names no row - the receiver stops at that hole\n * for good. `OpenTransaction` can only come from `openTransaction`, so the default manager does not compile\n * here. `test/append-store-db.test.ts` breaks an insert after the raise and reads the counter back.\n *\n * ## Raise, then read\n *\n * `increment` takes the row lock, so two writers on the same stream queue behind each other; reading after\n * raising means the run handed out belongs to this transaction. Reading first and writing `last + n` would\n * hand two writers the same numbers.\n *\n * The first append on a stream creates the counter row. Two first appends racing on a brand-new stream\n * meet at the unique index and one of the two business transactions fails whole - loud, and nothing burned.\n *\n * ## What the product keeps per row\n *\n * `lane` and `provenance` are columns (decision 3); a product passes them in `extra`. Anything else in\n * `extra` is refused: a key with no column would be accepted, dropped, and reported as written.\n * ═════════════════════════════════════════════════════════════════════════\n */\n\nconst EXTRA_COLUMNS = new Set(['lane', 'provenance'])\n\nexport function webhookAppendStore(tx: OpenTransaction<EntityManager>, stream: WebhookStream): OutboxAppendStore<object, WebhookOutboxEvent> {\n const { domainId, peer, scope } = stream\n const where = { domain: { id: domainId }, peer, scope }\n\n return {\n async allocate(count: number): Promise<number> {\n const exists = await tx.findOne(WebhookSequence, { where })\n\n if (!exists) {\n await tx.save(WebhookSequence, tx.create(WebhookSequence, { domain: { id: domainId } as any, peer, scope, lastSeq: 0 }))\n }\n\n await tx.increment(WebhookSequence, where, 'lastSeq', count)\n\n const after = await tx.findOne(WebhookSequence, { where })\n if (!after) {\n throw new Error(`webhookAppendStore: the counter for ${peer}/${scope} disappeared while allocating`)\n }\n\n /* Some drivers hand an int column back as a string. Count in numbers. */\n const lastSeq = Number(after.lastSeq)\n if (!Number.isFinite(lastSeq)) {\n throw new Error(`webhookAppendStore: the counter for ${peer}/${scope} is not a number: ${String(after.lastSeq)}`)\n }\n\n return lastSeq - count + 1\n },\n\n async insert(rows: readonly NewOutboxRow<object>[]): Promise<WebhookOutboxEvent[]> {\n const entities = rows.map(row => {\n const unknown = Object.keys(row.extra ?? {}).filter(key => !EXTRA_COLUMNS.has(key))\n if (unknown.length) {\n throw new Error(`webhookAppendStore: no column for ${unknown.join(', ')} - it would be dropped and reported as written`)\n }\n\n return tx.create(WebhookOutboxEvent, {\n domain: { id: domainId } as any,\n peer,\n scope,\n seq: row.seq,\n eventId: row.eventId,\n eventTime: row.eventTime,\n recordTime: row.recordTime,\n payload: row.record,\n status: WebhookOutboxStatus.Pending,\n ...(row.extra ?? {})\n })\n })\n\n return await tx.save(WebhookOutboxEvent, entities)\n }\n }\n}\n"]}