@things-factory/webhook 10.1.4
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-client/bootstrap.d.ts +1 -0
- package/dist-client/bootstrap.js +2 -0
- package/dist-client/bootstrap.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/pages/webhook-destinations-page.d.ts +20 -0
- package/dist-client/pages/webhook-destinations-page.js +228 -0
- package/dist-client/pages/webhook-destinations-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +8 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +5 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/index.d.ts +4 -0
- package/dist-server/service/index.js +17 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/webhook-destination/webhook-destination-resolver.d.ts +22 -0
- package/dist-server/service/webhook-destination/webhook-destination-resolver.js +130 -0
- package/dist-server/service/webhook-destination/webhook-destination-resolver.js.map +1 -0
- package/dist-server/service/webhook-destination/webhook-destination.d.ts +57 -0
- package/dist-server/service/webhook-destination/webhook-destination.js +118 -0
- package/dist-server/service/webhook-destination/webhook-destination.js.map +1 -0
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.d.ts +57 -0
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.js +166 -0
- package/dist-server/service/webhook-outbox-event/webhook-outbox-event.js.map +1 -0
- package/dist-server/service/webhook-sequence/webhook-sequence.d.ts +31 -0
- package/dist-server/service/webhook-sequence/webhook-sequence.js +82 -0
- package/dist-server/service/webhook-sequence/webhook-sequence.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +39 -0
- package/things-factory.config.js +13 -0
- package/translations/en.json +11 -0
- package/translations/ja.json +11 -0
- package/translations/ko.json +11 -0
- package/translations/ms.json +11 -0
- package/translations/zh.json +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-outbox-event.js","sourceRoot":"","sources":["../../../server/service/webhook-outbox-event/webhook-outbox-event.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, Int, ObjectType, registerEnumType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\n/*\n * ═══════════════════════════════════════════════════════════════════════════\n * One fact waiting to reach a peer.\n *\n * ── This row is the contract's envelope plus delivery state ────────────────\n * The shape is not invented here. `@operato/ops-contract` already declares what crosses\n * this seam, and the row is that declaration with the delivery columns added:\n *\n * WebhookEnvelope seq · scope · eventId · eventTime · recordTime · record\n * this row seq · scope · eventId · eventTime · recordTime · payload\n * + status · attempts · lastError · sentAt\n *\n * `payload` is the only product-specific column — a production report, a putaway task, a\n * setpoint. Everything else is the same whether the sender is an MES, a WMS, a YMS or an EMS,\n * which is why this table does not carry a product's name (ADR-0041).\n *\n * ── Why `scope` and not `channel` ─────────────────────────────────────────\n * operato-plant called this `channel` and the contract calls it `scope`. Same idea: the unit\n * the numbers are counted in. The contract's word wins, because the receiving side reads\n * `scope` off the envelope to decide which cursor to compare against — a sender using a\n * different word has to translate, and a translation is a place to be wrong.\n *\n * ── Failure that keeps the fact, and failure that gives up ─────────────────\n * A 429 or a 503 is not a failed delivery — it is \"not now\". Those must not consume the\n * retry budget, and they must not be recorded as attempts, or a receiver asking us to wait\n * looks identical to a receiver refusing us. Measured on 2026-09-08: the sender's\n * backpressure path touched neither `attempts` nor `lastError`, so a loop that re-offered the\n * same batch every five seconds left no trace on either side. The row cannot fix that alone —\n * the pump has to say something out loud — but it is why `attempts` means \"tries that were\n * really refused\".\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nexport enum WebhookOutboxStatus {\n /** Waiting to go. The pump picks these up in `seq` order. */\n Pending = 'PENDING',\n /** The peer took it. */\n Sent = 'SENT',\n /** The peer refused it and resending will not change that — a person has to look. */\n Failed = 'FAILED'\n}\n\nregisterEnumType(WebhookOutboxStatus, {\n name: 'WebhookOutboxStatus',\n description: 'Whether a queued fact is waiting, delivered, or refused for good.'\n})\n\n@ObjectType({ description: 'A fact queued for one peer, with its number and what happened when we tried to deliver it.' })\n@Entity()\n/*\n * Numbering is per (domain, peer, scope) and must be unique there — the receiving side\n * detects gaps by comparing consecutive numbers, so two rows sharing a number would make a\n * gap invisible.\n */\n@Index('ix_webhook_outbox_event_0', (event: WebhookOutboxEvent) => [event.domain, event.peer, event.scope, event.seq], {\n unique: true\n})\n/* The pump's own query: what is still waiting for this peer, oldest first. */\n@Index('ix_webhook_outbox_event_1', (event: WebhookOutboxEvent) => [event.domain, event.peer, event.status, event.seq])\nexport class WebhookOutboxEvent {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { description: 'Unique identifier for this queued fact.' })\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { description: 'The tenant this fact belongs to.' })\n domain: Domain\n\n @RelationId((event: WebhookOutboxEvent) => event.domain)\n domainId: string\n\n /** Who this is going to. Same unit as the destination row. */\n @Column()\n @Field({ description: 'The peer this fact is queued for.' })\n peer: string\n\n /**\n * The unit the numbers are counted in — the contract's `WebhookEnvelope.scope`.\n *\n * Without it the receiving side cannot tell which run of numbers a given envelope belongs\n * to, and two streams sharing one cursor answer \"a number is missing\" forever.\n */\n @Column()\n @Field({ description: 'The unit the sequence is counted in (the contract\\'s scope). Two streams must not share one cursor.' })\n scope: string\n\n /** Increases by one within (domain, peer, scope). A hole here is a lost fact. */\n @Column({ type: 'int' })\n @Field(type => Int, { description: 'Position in this scope. Increases by one; a hole means a fact went missing.' })\n seq: number\n\n /**\n * Idempotency key. The receiving side uses it to recognise a fact it has already taken, so\n * a resend after a lost response does not seat the same fact twice.\n *\n * It also lets a refusal name **which** envelope was refused: the receiver echoes this back\n * in `WebhookResponse.rejected`, and without it the sender can only be told \"one of these\n * was refused\" (measured 2026-09-07 — a whole batch was marked delivered because the\n * refusal could not be matched to a row).\n */\n @Column()\n @Field({ description: 'Idempotency key. Echoed back in a refusal so the sender knows which row was refused.' })\n eventId: string\n\n /** When the fact happened. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When the fact happened, as the source knows it.' })\n eventTime: Date\n\n /**\n * When we wrote it down. **Kept apart from `eventTime`** — a fact that arrives late is not\n * a fact that happened late, and folding them together makes a backfill look like a\n * present-tense event.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When this side recorded the fact. Not the same as when it happened.' })\n recordTime: Date\n\n /** The fact itself, in whatever shape this product speaks. The only product-specific column. */\n @Column({ type: 'simple-json' })\n @Field(type => ScalarObject, { description: 'The fact being pushed. The only column whose shape belongs to the product.' })\n payload: any\n\n @Column({ type: 'varchar', default: WebhookOutboxStatus.Pending })\n @Field(type => WebhookOutboxStatus, { description: 'Waiting, delivered, or refused for good.' })\n status: WebhookOutboxStatus\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'When the peer took it.' })\n sentAt: Date\n\n /**\n * How many times a delivery was **really refused** — not how many times we tried.\n *\n * A peer answering 429 or 503 is asking us to wait; that does not count here, or a\n * receiver under load becomes indistinguishable from a receiver that rejects us.\n */\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'Deliveries that were refused. Being asked to wait (429/503) does not count.' })\n attempts: number\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'What the peer said the last time it refused.' })\n lastError: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt: Date\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
export declare class WebhookSequence {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
domain: Domain;
|
|
5
|
+
domainId: string;
|
|
6
|
+
peer: string;
|
|
7
|
+
scope: string;
|
|
8
|
+
/**
|
|
9
|
+
* The highest number handed out. Never decreases — that is the whole reason this row
|
|
10
|
+
* exists apart from the queue.
|
|
11
|
+
*/
|
|
12
|
+
lastSeq: number;
|
|
13
|
+
/** The highest number the peer says it has placed. Null until a reply has said so. */
|
|
14
|
+
peerLastSeq?: number;
|
|
15
|
+
/**
|
|
16
|
+
* How many numbers below the peer's first envelope it never received — its own count, taken
|
|
17
|
+
* once, when it reported `first` for this stream.
|
|
18
|
+
*
|
|
19
|
+
* **A point fact, not a flowing one.** It answers "does the peer's history of this stream
|
|
20
|
+
* start at the beginning", asked at the moment the stream began for that peer. Recomputing it
|
|
21
|
+
* later against a moving cursor answers a different question and reads as the same one.
|
|
22
|
+
*
|
|
23
|
+
* **Not a loss, and not an instruction to resend.** The contract refuses to judge that
|
|
24
|
+
* (`checkSequence` → `unseenBefore`): a source that has been running for months and a twin
|
|
25
|
+
* connected today produce this number legitimately, and only the source knows which case it
|
|
26
|
+
* is. Filling the beginning of a history is a person's decision — the twin catalogue carries
|
|
27
|
+
* it as 「지난 기록 채우기 승인」.
|
|
28
|
+
*/
|
|
29
|
+
peerFirstUnseenBefore?: number;
|
|
30
|
+
updatedAt: Date;
|
|
31
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookSequence = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const type_graphql_1 = require("type-graphql");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
/*
|
|
9
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
+
* The last number handed out on one stream.
|
|
11
|
+
*
|
|
12
|
+
* A separate row rather than `MAX(seq)` over the queue, because the queue is drained: once a
|
|
13
|
+
* fact is delivered it may be pruned, and then `MAX(seq)` walks backwards and the next fact
|
|
14
|
+
* reuses a number the peer has already seen. The peer would read that as a resend and drop a
|
|
15
|
+
* fact that never arrived.
|
|
16
|
+
*
|
|
17
|
+
* ── One row per stream, and the stream is (domain, peer, scope) ────────────
|
|
18
|
+
* operato-plant keyed this `(domain, channel)` — no peer. That holds while a tenant talks to
|
|
19
|
+
* exactly one peer and breaks silently the moment it talks to two: both peers draw from one
|
|
20
|
+
* counter, so each sees a sequence with holes in it and asks for a backfill that will never
|
|
21
|
+
* arrive, because the missing numbers were handed to the other peer (ADR-0041).
|
|
22
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
*/
|
|
24
|
+
let WebhookSequence = class WebhookSequence {
|
|
25
|
+
};
|
|
26
|
+
exports.WebhookSequence = WebhookSequence;
|
|
27
|
+
tslib_1.__decorate([
|
|
28
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
29
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
30
|
+
tslib_1.__metadata("design:type", String)
|
|
31
|
+
], WebhookSequence.prototype, "id", void 0);
|
|
32
|
+
tslib_1.__decorate([
|
|
33
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
34
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain),
|
|
35
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
36
|
+
], WebhookSequence.prototype, "domain", void 0);
|
|
37
|
+
tslib_1.__decorate([
|
|
38
|
+
(0, typeorm_1.RelationId)((seq) => seq.domain),
|
|
39
|
+
tslib_1.__metadata("design:type", String)
|
|
40
|
+
], WebhookSequence.prototype, "domainId", void 0);
|
|
41
|
+
tslib_1.__decorate([
|
|
42
|
+
(0, typeorm_1.Column)(),
|
|
43
|
+
(0, type_graphql_1.Field)({ description: 'The peer this stream goes to.' }),
|
|
44
|
+
tslib_1.__metadata("design:type", String)
|
|
45
|
+
], WebhookSequence.prototype, "peer", void 0);
|
|
46
|
+
tslib_1.__decorate([
|
|
47
|
+
(0, typeorm_1.Column)(),
|
|
48
|
+
(0, type_graphql_1.Field)({ description: 'The unit the numbers are counted in.' }),
|
|
49
|
+
tslib_1.__metadata("design:type", String)
|
|
50
|
+
], WebhookSequence.prototype, "scope", void 0);
|
|
51
|
+
tslib_1.__decorate([
|
|
52
|
+
(0, typeorm_1.Column)({ type: 'int', default: 0 }),
|
|
53
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'Highest number handed out on this stream. Never goes backwards.' }),
|
|
54
|
+
tslib_1.__metadata("design:type", Number)
|
|
55
|
+
], WebhookSequence.prototype, "lastSeq", void 0);
|
|
56
|
+
tslib_1.__decorate([
|
|
57
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
58
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
59
|
+
nullable: true,
|
|
60
|
+
description: 'Highest number the peer reports having placed, from its own reply. Null when no reply has said. Not derived from our delivery marks — a lost reply leaves the peer ahead of them.'
|
|
61
|
+
}),
|
|
62
|
+
tslib_1.__metadata("design:type", Number)
|
|
63
|
+
], WebhookSequence.prototype, "peerLastSeq", void 0);
|
|
64
|
+
tslib_1.__decorate([
|
|
65
|
+
(0, typeorm_1.Column)({ type: 'int', nullable: true }),
|
|
66
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, {
|
|
67
|
+
nullable: true,
|
|
68
|
+
description: 'Numbers below the peer\'s first envelope that it never received, as it reported once at the start of this stream. Says nothing about whether they were lost — only the sender knows that.'
|
|
69
|
+
}),
|
|
70
|
+
tslib_1.__metadata("design:type", Number)
|
|
71
|
+
], WebhookSequence.prototype, "peerFirstUnseenBefore", void 0);
|
|
72
|
+
tslib_1.__decorate([
|
|
73
|
+
(0, typeorm_1.UpdateDateColumn)(),
|
|
74
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
75
|
+
tslib_1.__metadata("design:type", Date)
|
|
76
|
+
], WebhookSequence.prototype, "updatedAt", void 0);
|
|
77
|
+
exports.WebhookSequence = WebhookSequence = tslib_1.__decorate([
|
|
78
|
+
(0, type_graphql_1.ObjectType)({ description: 'The last sequence number handed out on one (peer, scope) stream.' }),
|
|
79
|
+
(0, typeorm_1.Entity)(),
|
|
80
|
+
(0, typeorm_1.Index)('ix_webhook_sequence_0', (seq) => [seq.domain, seq.peer, seq.scope], { unique: true })
|
|
81
|
+
], WebhookSequence);
|
|
82
|
+
//# sourceMappingURL=webhook-sequence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-sequence.js","sourceRoot":"","sources":["../../../server/service/webhook-sequence/webhook-sequence.ts"],"names":[],"mappings":";;;;AAAA,qCAAgH;AAChH,+CAAyD;AAEzD,iDAA8C;AAE9C;;;;;;;;;;;;;;;GAeG;AAII,IAAM,eAAe,GAArB,MAAM,eAAe;CAiF3B,CAAA;AAjFY,0CAAe;AAGjB;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,CAAC;;2CACC;AAInB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;sCACd,cAAM;+CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,GAAoB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC;;iDACjC;AAIhB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;6CAC5C;AAIZ;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,sCAAsC,EAAE,CAAC;;8CAClD;AAQb;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACnC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC;;gDACxF;AA4Bf;IANC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,mLAAmL;KACtL,CAAC;;oDACkB;AAsBpB;IANC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE;QAClB,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,2LAA2L;KAC9L,CAAC;;8DAC4B;AAI9B;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACf,IAAI;kDAAA;0BAhFJ,eAAe;IAH3B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,kEAAkE,EAAE,CAAC;IAC/F,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,uBAAuB,EAAE,CAAC,GAAoB,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;GACjG,eAAe,CAiF3B","sourcesContent":["import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn, RelationId, UpdateDateColumn } from 'typeorm'\nimport { Field, ID, Int, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\n\n/*\n * ═══════════════════════════════════════════════════════════════════════════\n * The last number handed out on one stream.\n *\n * A separate row rather than `MAX(seq)` over the queue, because the queue is drained: once a\n * fact is delivered it may be pruned, and then `MAX(seq)` walks backwards and the next fact\n * reuses a number the peer has already seen. The peer would read that as a resend and drop a\n * fact that never arrived.\n *\n * ── One row per stream, and the stream is (domain, peer, scope) ────────────\n * operato-plant keyed this `(domain, channel)` — no peer. That holds while a tenant talks to\n * exactly one peer and breaks silently the moment it talks to two: both peers draw from one\n * counter, so each sees a sequence with holes in it and asks for a backfill that will never\n * arrive, because the missing numbers were handed to the other peer (ADR-0041).\n * ═══════════════════════════════════════════════════════════════════════════\n */\n@ObjectType({ description: 'The last sequence number handed out on one (peer, scope) stream.' })\n@Entity()\n@Index('ix_webhook_sequence_0', (seq: WebhookSequence) => [seq.domain, seq.peer, seq.scope], { unique: true })\nexport class WebhookSequence {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain: Domain\n\n @RelationId((seq: WebhookSequence) => seq.domain)\n domainId: string\n\n @Column()\n @Field({ description: 'The peer this stream goes to.' })\n peer: string\n\n @Column()\n @Field({ description: 'The unit the numbers are counted in.' })\n scope: string\n\n /**\n * The highest number handed out. Never decreases — that is the whole reason this row\n * exists apart from the queue.\n */\n @Column({ type: 'int', default: 0 })\n @Field(type => Int, { description: 'Highest number handed out on this stream. Never goes backwards.' })\n lastSeq: number\n\n /*\n * ── What the peer told us, kept apart from what we handed out ─────────────\n *\n * These two are the peer's answers, copied from its reply (`WebhookResponse.lastSeq` and\n * `firstUnseenBefore`). They are here because the alternative is to infer them, and inferring\n * them went wrong on 2026-09-08: operato-plant took \"what the twin has seen\" from the highest\n * row it had marked SENT and reset its counter to that, 200 short.\n *\n * The two diverge in one direction and always the same one. SENT records that a reply came\n * back; the peer's cursor records what the peer placed. When a reply is lost — the process\n * goes down between the peer writing and us reading its answer — the peer is ahead and our\n * own bookkeeping cannot see it. So the peer's number is a fact only the peer can state, and\n * it needs a home rather than a derivation.\n *\n * **Nullable, and never defaulted to zero.** \"The peer has not told us\" is not \"the peer has\n * seen nothing\": one means we cannot answer, the other is an answer. A `?? 0` here would turn\n * the first into the second and hand a caller a number it would act on.\n */\n\n /** The highest number the peer says it has placed. Null until a reply has said so. */\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, {\n nullable: true,\n description:\n 'Highest number the peer reports having placed, from its own reply. Null when no reply has said. Not derived from our delivery marks — a lost reply leaves the peer ahead of them.'\n })\n peerLastSeq?: number\n\n /**\n * How many numbers below the peer's first envelope it never received — its own count, taken\n * once, when it reported `first` for this stream.\n *\n * **A point fact, not a flowing one.** It answers \"does the peer's history of this stream\n * start at the beginning\", asked at the moment the stream began for that peer. Recomputing it\n * later against a moving cursor answers a different question and reads as the same one.\n *\n * **Not a loss, and not an instruction to resend.** The contract refuses to judge that\n * (`checkSequence` → `unseenBefore`): a source that has been running for months and a twin\n * connected today produce this number legitimately, and only the source knows which case it\n * is. Filling the beginning of a history is a person's decision — the twin catalogue carries\n * it as 「지난 기록 채우기 승인」.\n */\n @Column({ type: 'int', nullable: true })\n @Field(type => Int, {\n nullable: true,\n description:\n 'Numbers below the peer\\'s first envelope that it never received, as it reported once at the start of this stream. Says nothing about whether they were lost — only the sender knows that.'\n })\n peerFirstUnseenBefore?: number\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt: Date\n}\n"]}
|