@substrat-run/kernel 0.120.0 → 0.122.0
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 +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +5 -2
- package/dist/list-index.js.map +1 -1
- package/dist/module-migrations.d.ts +26 -0
- package/dist/module-migrations.d.ts.map +1 -0
- package/dist/module-migrations.js +21 -0
- package/dist/module-migrations.js.map +1 -0
- package/dist/platform-call.d.ts +20 -0
- package/dist/platform-call.d.ts.map +1 -1
- package/dist/platform-call.js +24 -0
- package/dist/platform-call.js.map +1 -1
- package/dist/platform-sweep.d.ts +216 -4
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +648 -45
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +148 -5
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js.map +1 -1
- package/dist/sql-limits.d.ts +70 -0
- package/dist/sql-limits.d.ts.map +1 -0
- package/dist/sql-limits.js +197 -0
- package/dist/sql-limits.js.map +1 -0
- package/dist/system-switch-record.d.ts +158 -0
- package/dist/system-switch-record.d.ts.map +1 -0
- package/dist/system-switch-record.js +227 -0
- package/dist/system-switch-record.js.map +1 -0
- package/dist/system-switch.d.ts +2 -2
- package/dist/system-switch.d.ts.map +1 -1
- package/dist/ulid.d.ts +6 -0
- package/dist/ulid.d.ts.map +1 -1
- package/dist/ulid.js +21 -7
- package/dist/ulid.js.map +1 -1
- package/dist/version-migrations.d.ts +108 -0
- package/dist/version-migrations.d.ts.map +1 -0
- package/dist/version-migrations.js +187 -0
- package/dist/version-migrations.js.map +1 -0
- package/dist/vertical-events.d.ts +96 -2
- package/dist/vertical-events.d.ts.map +1 -1
- package/dist/vertical-events.js +239 -4
- package/dist/vertical-events.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A version's SQL migrations, stored apart from its manifest (#1764), shared by both adapters.
|
|
3
|
+
*
|
|
4
|
+
* Since #1677 a pushed deploy manifest carries its whole cumulative migration set
|
|
5
|
+
* (`migrations: { moduleId, version, sql }[]`). The manifest is one `manifest_json` column on
|
|
6
|
+
* `vertical_versions`, so every read of a version moved that SQL too: `readVersion` on every
|
|
7
|
+
* admit, promote and bind, and every `storedDeployManifest.parse` behind `/registry`, `/model`,
|
|
8
|
+
* `/schedules` and the serving upload. Only the promote review reads the SQL.
|
|
9
|
+
*
|
|
10
|
+
* So `publishVersion` splits the field off: the SQL goes to `vertical_version_migrations`, one
|
|
11
|
+
* row per migration, and the manifest is stored without it. The wire is unchanged, since the
|
|
12
|
+
* CLI still sends `migrations` inside the manifest. Two columns on `vertical_versions` carry
|
|
13
|
+
* what the rows alone cannot:
|
|
14
|
+
*
|
|
15
|
+
* - `migration_count`: how many rows the version stored. **NULL means the version carries no
|
|
16
|
+
* SQL** (pushed before #1677, over the push's caps, or malformed), which a reader must show
|
|
17
|
+
* as "not available", never as "no migrations". `0` is a version whose modules ship none. A
|
|
18
|
+
* read whose row count disagrees with it answers null rather than a short list.
|
|
19
|
+
* - `migrations_split`: 1 once the version's SQL is out of its manifest. NULL is a version
|
|
20
|
+
* stored before this, whose manifest may still carry it. The backfill below moves those a
|
|
21
|
+
* bounded batch at a time, and until it reaches a version the read falls back to that
|
|
22
|
+
* version's manifest. The fallback is only for those rows. A version published by this code
|
|
23
|
+
* is split on the way in.
|
|
24
|
+
*/
|
|
25
|
+
import { deployManifest, storedDeployManifest, } from '@substrat-run/contracts';
|
|
26
|
+
/**
|
|
27
|
+
* The table, and the index the backfill walks. Interpolated into both adapters' directory DDL,
|
|
28
|
+
* so `lint:spine-ddl` sees one spelling on each side. Anything that references this table or
|
|
29
|
+
* this index goes in this fragment, which is what the adapters run after their column additions.
|
|
30
|
+
*
|
|
31
|
+
* The index is partial: it holds only versions still waiting for the backfill, so "is there
|
|
32
|
+
* anything left" is one probe that costs nothing once the backfill is done. It names a column
|
|
33
|
+
* a directory from before #1764 lacks, so the adapters run these statements after their
|
|
34
|
+
* column additions, not in their DDL loop.
|
|
35
|
+
*/
|
|
36
|
+
export const VERSION_MIGRATIONS_DDL = `
|
|
37
|
+
CREATE TABLE IF NOT EXISTS vertical_version_migrations (
|
|
38
|
+
version_id TEXT NOT NULL,
|
|
39
|
+
-- The migration's place in the order the host runs them, from 0.
|
|
40
|
+
ordinal INTEGER NOT NULL,
|
|
41
|
+
module_id TEXT NOT NULL,
|
|
42
|
+
-- The module's SqlMigration.version, e.g. 0001-init.
|
|
43
|
+
version TEXT NOT NULL,
|
|
44
|
+
sql TEXT NOT NULL,
|
|
45
|
+
PRIMARY KEY (version_id, ordinal)
|
|
46
|
+
);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS vertical_versions_unsplit
|
|
48
|
+
ON vertical_versions (id) WHERE migrations_split IS NULL;
|
|
49
|
+
`;
|
|
50
|
+
/**
|
|
51
|
+
* Take `migrations` out of a manifest.
|
|
52
|
+
*
|
|
53
|
+
* `push` holds the field to the push boundary's caps (`deployManifest`): at most 2000 entries
|
|
54
|
+
* and 512 KiB of SQL, which also bounds every row under a Durable Object's 2 MB row limit.
|
|
55
|
+
* `/deploy` already refuses an over-cap set, before the upload, because it parses the whole
|
|
56
|
+
* manifest with `deployManifest`. This split caps whatever reaches `publishVersion` by any
|
|
57
|
+
* route, including `POST /verticals/:slug/versions`, which stores `manifestJson` unparsed
|
|
58
|
+
* (#1765). There, a field over the caps or not shaped like migrations is dropped and the
|
|
59
|
+
* version reads as "SQL not available", so the promote dialog still asks. `stored` is for a
|
|
60
|
+
* manifest already in the directory, whose history the caps must never make unreadable
|
|
61
|
+
* (`storedDeployManifest`).
|
|
62
|
+
*
|
|
63
|
+
* A manifest that is not a JSON object is returned as it came, with no migrations. The
|
|
64
|
+
* manifest is only rewritten when it has a `migrations` key to remove.
|
|
65
|
+
*/
|
|
66
|
+
export function splitManifestMigrations(manifestJson, mode) {
|
|
67
|
+
if (manifestJson === null)
|
|
68
|
+
return { manifestJson, migrations: null };
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
parsed = JSON.parse(manifestJson);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return { manifestJson, migrations: null };
|
|
75
|
+
}
|
|
76
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) || !('migrations' in parsed)) {
|
|
77
|
+
return { manifestJson, migrations: null };
|
|
78
|
+
}
|
|
79
|
+
const { migrations: carried, ...rest } = parsed;
|
|
80
|
+
const schema = (mode === 'push' ? deployManifest : storedDeployManifest).shape.migrations;
|
|
81
|
+
const result = schema.safeParse(carried);
|
|
82
|
+
return {
|
|
83
|
+
manifestJson: JSON.stringify(rest),
|
|
84
|
+
migrations: result.success && result.data !== undefined ? result.data : null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** Replace one version's migration rows. The caller sets its `migration_count` and `migrations_split`. */
|
|
88
|
+
export function writeVersionMigrations(db, versionId, migrations) {
|
|
89
|
+
db.run('DELETE FROM vertical_version_migrations WHERE version_id = ?', versionId);
|
|
90
|
+
(migrations ?? []).forEach((m, ordinal) => {
|
|
91
|
+
db.run(`INSERT INTO vertical_version_migrations (version_id, ordinal, module_id, version, sql)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?)`, versionId, ordinal, m.moduleId, m.version, m.sql);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The two reads that find unsplit versions. Named so the kernel's test can EXPLAIN these
|
|
97
|
+
* exact strings: each must be answered from `vertical_versions_unsplit`, never by a scan of
|
|
98
|
+
* every version's row (and its manifest's overflow pages).
|
|
99
|
+
*/
|
|
100
|
+
export const UNSPLIT_IDS_SQL = 'SELECT id FROM vertical_versions WHERE migrations_split IS NULL ORDER BY id LIMIT ?';
|
|
101
|
+
export const UNSPLIT_PROBE_SQL = 'SELECT 1 AS present FROM vertical_versions WHERE migrations_split IS NULL LIMIT 1';
|
|
102
|
+
/** How many versions one backfill batch moves at most. */
|
|
103
|
+
const BATCH_VERSIONS = 25;
|
|
104
|
+
/**
|
|
105
|
+
* How much manifest one batch reads at most, in UTF-8 bytes (the first version is always
|
|
106
|
+
* moved). A stored manifest can be up to about 1.5 MiB, and a batch holds the directory DO.
|
|
107
|
+
* Bytes, because what a batch costs is the bytes it reads: a manifest of four-byte characters
|
|
108
|
+
* is four times its character count.
|
|
109
|
+
*/
|
|
110
|
+
export const BATCH_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
111
|
+
/**
|
|
112
|
+
* Move the SQL out of the next versions stored before #1764: at most `limit` of them, and
|
|
113
|
+
* at most `BATCH_MANIFEST_BYTES` of manifest. Returns how many it moved, and whether any are
|
|
114
|
+
* left.
|
|
115
|
+
*
|
|
116
|
+
* Bounded so that no one run has to read the whole version history: the Durable-Object
|
|
117
|
+
* adapter runs one batch per alarm and re-arms while any are left, so its constructor never
|
|
118
|
+
* does per-version work. Resumable because each version is marked as it is moved, in the
|
|
119
|
+
* caller's transaction with its rows. A version is moved by the same `splitManifestMigrations`
|
|
120
|
+
* a push goes through, in `stored` mode, so history is read without the push caps.
|
|
121
|
+
*
|
|
122
|
+
* Idempotent: a version that is already split is never selected, and moving one replaces its
|
|
123
|
+
* rows. A directory restored from a dump taken before the backfill comes back unsplit and is
|
|
124
|
+
* simply moved again.
|
|
125
|
+
*/
|
|
126
|
+
export function splitVersionMigrationsBatch(db, limit = BATCH_VERSIONS) {
|
|
127
|
+
// The ids come off the partial index alone; each manifest is read only when it is moved.
|
|
128
|
+
const ids = db.all(UNSPLIT_IDS_SQL, limit);
|
|
129
|
+
let moved = 0;
|
|
130
|
+
let bytes = 0;
|
|
131
|
+
for (const { id } of ids) {
|
|
132
|
+
// The candidate is measured before it is taken, so a batch never passes the bound by one
|
|
133
|
+
// more manifest. The first is always taken, or a manifest over the bound would never move.
|
|
134
|
+
// `octet_length` (SQLite 3.43+) counts bytes; `length` would count code points.
|
|
135
|
+
const { size } = db.all('SELECT COALESCE(octet_length(manifest_json), 0) AS size FROM vertical_versions WHERE id = ?', id)[0];
|
|
136
|
+
if (moved > 0 && bytes + size > BATCH_MANIFEST_BYTES)
|
|
137
|
+
break;
|
|
138
|
+
bytes += size;
|
|
139
|
+
const { manifest_json: stored } = db.all('SELECT manifest_json FROM vertical_versions WHERE id = ?', id)[0];
|
|
140
|
+
const split = splitManifestMigrations(stored, 'stored');
|
|
141
|
+
writeVersionMigrations(db, id, split.migrations);
|
|
142
|
+
const count = split.migrations?.length ?? null;
|
|
143
|
+
// A manifest with nothing taken out is left where it is, not rewritten.
|
|
144
|
+
if (split.manifestJson === stored) {
|
|
145
|
+
db.run('UPDATE vertical_versions SET migration_count = ?, migrations_split = 1 WHERE id = ?', count, id);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
db.run('UPDATE vertical_versions SET manifest_json = ?, migration_count = ?, migrations_split = 1 WHERE id = ?', split.manifestJson, count, id);
|
|
149
|
+
}
|
|
150
|
+
moved++;
|
|
151
|
+
}
|
|
152
|
+
return { moved, more: versionsAwaitSplit(db) };
|
|
153
|
+
}
|
|
154
|
+
/** Whether any version still waits for the backfill. One probe of the partial index. */
|
|
155
|
+
export function versionsAwaitSplit(db) {
|
|
156
|
+
return db.all(UNSPLIT_PROBE_SQL).length > 0;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* One version's migrations, in the order the host runs them. `undefined` when there is no
|
|
160
|
+
* such version. `migrations` is null when the version carries none that can be shown, which
|
|
161
|
+
* a reader must say as "not available", never as "no migrations".
|
|
162
|
+
*/
|
|
163
|
+
export function versionMigrationsOf(db, versionId) {
|
|
164
|
+
// The manifest is read only for a version the backfill has not reached: its SQL, if any,
|
|
165
|
+
// is still in there. A split version's manifest carries none, so it is left unread.
|
|
166
|
+
const row = db.all(`SELECT vertical_slug, migration_count, migrations_split,
|
|
167
|
+
CASE WHEN migrations_split IS NULL THEN manifest_json END AS manifest_json
|
|
168
|
+
FROM vertical_versions WHERE id = ?`, versionId)[0];
|
|
169
|
+
if (!row)
|
|
170
|
+
return undefined;
|
|
171
|
+
if (row.migrations_split === null) {
|
|
172
|
+
return { verticalSlug: row.vertical_slug, migrations: splitManifestMigrations(row.manifest_json, 'stored').migrations };
|
|
173
|
+
}
|
|
174
|
+
if (row.migration_count === null)
|
|
175
|
+
return { verticalSlug: row.vertical_slug, migrations: null };
|
|
176
|
+
const rows = db.all(`SELECT module_id, version, sql FROM vertical_version_migrations
|
|
177
|
+
WHERE version_id = ? ORDER BY ordinal`, versionId);
|
|
178
|
+
// Never a short list: rows that disagree with the count are not the version's migrations.
|
|
179
|
+
if (rows.length !== row.migration_count)
|
|
180
|
+
return { verticalSlug: row.vertical_slug, migrations: null };
|
|
181
|
+
return {
|
|
182
|
+
verticalSlug: row.vertical_slug,
|
|
183
|
+
// Parsed on the way in (`splitManifestMigrations`), so the brand is the stored value's.
|
|
184
|
+
migrations: rows.map((r) => ({ moduleId: r.module_id, version: r.version, sql: r.sql })),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=version-migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version-migrations.js","sourceRoot":"","sources":["../src/version-migrations.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
* production builds them, and one definition is what keeps the two identical.
|
|
24
24
|
* `lint:spine-ddl` holds each adapter to including it.
|
|
25
25
|
*/
|
|
26
|
-
import { type EventExport, type ModuleManifest, type ExportedBatch, type WantedEvent, type WithheldEvent } from '@substrat-run/contracts';
|
|
26
|
+
import { type ImportCursorMove, type ImportCursorMoved, type ScopeId, type TenantId, type VerticalResolution, type EventExport, type ModuleManifest, type ExportedBatch, type WantedEvent, type WithheldEvent } from '@substrat-run/contracts';
|
|
27
27
|
import { type OutboxEnvelopeRow } from './outbox-event.js';
|
|
28
28
|
import type { ImportHandler } from './scope-host.js';
|
|
29
|
-
|
|
29
|
+
import type { SwitchSql } from './system-switch.js';
|
|
30
|
+
export declare const VERTICAL_EVENTS_DDL = "\n -- #1705: every event this scope has received from another vertical. The envelope only, and\n -- never the payload. The producer's outbox keeps the one copy, where erasure already reaches.\n CREATE TABLE IF NOT EXISTS _substrat_imports (\n -- The PRODUCER's event id. A ULID the producer's host minted, so it is unique across\n -- every source this scope imports from, and it is the dedupe key: a batch delivered\n -- twice finds its events here and its handlers' journal rows in _substrat_deliveries.\n event_id TEXT PRIMARY KEY,\n source_scope_id TEXT NOT NULL,\n source_vertical TEXT NOT NULL,\n type TEXT NOT NULL,\n schema_version INTEGER NOT NULL,\n occurred_at TEXT NOT NULL,\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n -- How many vertical boundaries this event's cause chain has crossed, this one included.\n -- An export read in THIS scope counts from here when one of its own events was caused by\n -- an import, which is how a loop between two verticals is stopped.\n hops INTEGER NOT NULL,\n -- NULL: released, and handed to this scope's handlers. Otherwise the producer's reason\n -- for NOT releasing it ('pii', 'version', 'cascade', 'undecodable'). Such a row was\n -- named but never carried.\n withheld TEXT,\n imported_at TEXT NOT NULL\n );\n -- #1705: how far this scope has read each producer. Written in the same store as the\n -- handlers' writes, so a restore of this scope rewinds the watermark with the data and the\n -- producer re-delivers what the restore undid. A watermark the platform held instead would\n -- skip it.\n CREATE TABLE IF NOT EXISTS _substrat_import_cursors (\n source_scope_id TEXT PRIMARY KEY,\n source_vertical TEXT NOT NULL,\n -- The id of the last producer row the reads walked (released or withheld). NULL is not\n -- stored: a producer with no row here has never been read, which the absence says.\n cursor TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n -- #1705: the export read, on the PRODUCER side. It asks for a few types after a watermark,\n -- in id order. Without this index SQLite walks the primary key from the watermark forward and\n -- steps over every row of every other type. A consumer that is caught up would then pay, on\n -- every pass, for all the unexported activity since the last exported event. With it, the read\n -- seeks each type's range and costs what it returns.\n CREATE INDEX IF NOT EXISTS _substrat_outbox_type_id ON _substrat_outbox (type, id);\n -- #1705 PR 3: what a REPLAY moved out of the live journal. A replay re-delivers events this\n -- scope already took, and the live tables are also the dedupe (a row in _substrat_deliveries\n -- means \"do not run this handler again\"). So the rows for the replayed range leave them. They\n -- are moved here rather than dropped, because they are the record that a handler ran on an\n -- event, when, and what it reported: evidence the spine keeps. One row per moved row, the\n -- whole row as JSON under the replay that moved it. Nothing reads this to deliver.\n -- ERASURE: today no erasure path touches imported copies, because only piiClass 'none' crosses\n -- and the delivery-error rewrite (#1632) reaches only this scope's OWN outbox events. If a\n -- consumer-side erasure of imported copies is ever added, it must reach `row` here too (a\n -- moved delivery row's `error` is a handler's own sentence), or the history keeps what the\n -- live tables gave up. Follow-up: #1757.\n CREATE TABLE IF NOT EXISTS _substrat_import_replays (\n -- The act (a ULID). The admin log's row for the replay carries the same id.\n replay_id TEXT NOT NULL,\n replayed_at TEXT NOT NULL,\n source_scope_id TEXT NOT NULL,\n -- 'journal': a _substrat_imports row. 'delivery': a _substrat_deliveries row.\n kind TEXT NOT NULL,\n event_id TEXT NOT NULL,\n -- The handler's module on a 'delivery' row, and '' on a 'journal' row.\n consumer_module TEXT NOT NULL,\n row TEXT NOT NULL,\n PRIMARY KEY (replay_id, kind, event_id, consumer_module)\n );\n";
|
|
30
31
|
/**
|
|
31
32
|
* The producer-side read: rows of the given types after the watermark, oldest first.
|
|
32
33
|
*
|
|
@@ -96,6 +97,27 @@ export declare function exportsOf(manifests: readonly {
|
|
|
96
97
|
exports?: readonly EventExport[];
|
|
97
98
|
};
|
|
98
99
|
}[]): Map<string, EventExport>;
|
|
100
|
+
/**
|
|
101
|
+
* The outbox's insertion mark (#1705 PR 2): the kick's "before", taken ahead of an invoke.
|
|
102
|
+
*
|
|
103
|
+
* `rowid`, not the event id. A ULID minted in the same millisecond as the newest row can sort
|
|
104
|
+
* BELOW it, so "ids above the newest id" can miss an event this invoke wrote. SQLite assigns a
|
|
105
|
+
* new row `max(rowid) + 1` while the newest row is still there. No code path deletes outbox rows
|
|
106
|
+
* inside an invoke: the only removals are a restore and a wipe, and they replace the whole table
|
|
107
|
+
* outside any invocation. So every row the invoke and its consumers add sits above the mark. If
|
|
108
|
+
* that ever stopped being true, the effect is bounded: an exported event would miss its kick and
|
|
109
|
+
* wait for the sweep, and no event would be lost or sent twice.
|
|
110
|
+
*/
|
|
111
|
+
export declare const OUTBOX_MARK_SQL = "SELECT COALESCE(MAX(rowid), 0) AS mark FROM _substrat_outbox";
|
|
112
|
+
/**
|
|
113
|
+
* How many rows of the exported `types` were added after `mark` (#1705 PR 2). This is the
|
|
114
|
+
* count `ScopeStubOptions.onExportedEvents` reports. A seek on the rowid, so it walks only
|
|
115
|
+
* what the invoke added. Callers skip it entirely when the deployment exports nothing.
|
|
116
|
+
*/
|
|
117
|
+
export declare function exportedSinceQuery(types: readonly string[], mark: number): {
|
|
118
|
+
sql: string;
|
|
119
|
+
params: unknown[];
|
|
120
|
+
};
|
|
99
121
|
/** The consumer's watermark per producer, oldest source first. */
|
|
100
122
|
export declare const IMPORT_CURSORS_SQL = "SELECT source_scope_id, source_vertical, cursor, updated_at FROM _substrat_import_cursors ORDER BY source_scope_id";
|
|
101
123
|
/** One producer's watermark, for the compare-and-set a batch is applied under. */
|
|
@@ -115,6 +137,74 @@ export declare const IMPORT_CURSOR_ADVANCE_SQL = "\n INSERT INTO _substrat_impo
|
|
|
115
137
|
* `imported_at` forward or rewrite what was withheld and why.
|
|
116
138
|
*/
|
|
117
139
|
export declare const IMPORT_RECORD_SQL = "\n INSERT OR IGNORE INTO _substrat_imports\n (event_id, source_scope_id, source_vertical, type, schema_version, occurred_at,\n entity_type, entity_id, hops, withheld, imported_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n";
|
|
140
|
+
/**
|
|
141
|
+
* The producer end of the edge a lever moves, resolved by the sweep's own rule (#1705 PR 3).
|
|
142
|
+
*
|
|
143
|
+
* Both ends must be their tenant's one primary instance, exactly as `sweepEdge` requires. A
|
|
144
|
+
* watermark on any other pair is one no pass reads, so a move there would report success and
|
|
145
|
+
* change nothing. `resolve` is the directory's `resolveVerticalInstance`, and it takes the
|
|
146
|
+
* CONSUMER's tenant, so a producer in another tenant is not found rather than moved.
|
|
147
|
+
*/
|
|
148
|
+
export declare function importCursorSourceOf(resolve: (tenantId: TenantId, vertical: string) => Promise<VerticalResolution>, consumer: {
|
|
149
|
+
tenantId: TenantId;
|
|
150
|
+
scopeId: ScopeId;
|
|
151
|
+
vertical: string | null;
|
|
152
|
+
}, from: string): Promise<{
|
|
153
|
+
vertical: string;
|
|
154
|
+
scopeId: ScopeId;
|
|
155
|
+
}>;
|
|
156
|
+
/**
|
|
157
|
+
* Move one edge's watermark (#1705 PR 3). The adapter supplies the store (as the switches'
|
|
158
|
+
* `SwitchSql`), the transaction, the queue, the clock and the act's id. Run on the scope's own
|
|
159
|
+
* queue, so a pass delivering on the same edge can never interleave with the move. Everything the move decides is decided here, once, for
|
|
160
|
+
* both adapters.
|
|
161
|
+
*
|
|
162
|
+
* Why a bare rewind is not a replay. Three guards stand between an event and a second run of
|
|
163
|
+
* its handler, and a replay must pass each one on purpose:
|
|
164
|
+
*
|
|
165
|
+
* 1. `IMPORT_CURSOR_ADVANCE_SQL` moves the watermark forward only. The lever sets it here.
|
|
166
|
+
* 2. `_substrat_deliveries` holds a row per (event, module) that ran, and the delivery path
|
|
167
|
+
* skips any pair it finds. Rewound but not cleared, every replayed event would come back
|
|
168
|
+
* as a `duplicate` and nothing would run.
|
|
169
|
+
* 3. `_substrat_imports` is `INSERT OR IGNORE`. Left in place, an event first withheld for its
|
|
170
|
+
* version would stay recorded as withheld after the consumer's upgrade released it.
|
|
171
|
+
*
|
|
172
|
+
* So a replay moves the range's rows in (2) and (3) into `_substrat_import_replays` under
|
|
173
|
+
* `replayId`, then clears them, then sets the watermark. The evidence stays, and the live tables
|
|
174
|
+
* take the events again. Two things are true between the move and the redelivery, and are
|
|
175
|
+
* stated rather than hidden: a cause walk through a moved event's id ends at `missing`, and an
|
|
176
|
+
* export read in this scope counts fewer hops for a chain through it. Both heal when the next
|
|
177
|
+
* pass redelivers. The undercount only loosens the loop bound (the hop cap bounds loops, and
|
|
178
|
+
* authorizes nothing), so a loop may run a round longer; no event crosses that exports, grants
|
|
179
|
+
* and the PII rule would not already release.
|
|
180
|
+
*
|
|
181
|
+
* Refused, with the reason a person reads:
|
|
182
|
+
* - a replay whose `after` is AHEAD of the watermark (that is a skip), or a skip whose `through`
|
|
183
|
+
* is BEHIND it (that is a replay). The acknowledgement names one direction, and it must be
|
|
184
|
+
* the direction that moves;
|
|
185
|
+
* - a skip past now, which would pass over events not yet written while the edge read as
|
|
186
|
+
* caught up;
|
|
187
|
+
* - a replay on an edge that has delivered nothing yet: there is nothing to run again.
|
|
188
|
+
*/
|
|
189
|
+
export declare function moveImportCursor(sql: SwitchSql, input: {
|
|
190
|
+
move: ImportCursorMove;
|
|
191
|
+
source: {
|
|
192
|
+
vertical: string;
|
|
193
|
+
scopeId: string;
|
|
194
|
+
};
|
|
195
|
+
replayId: string;
|
|
196
|
+
/** Epoch ms: the rows' time, and "now" for a skip, as the watermark's ceiling. */
|
|
197
|
+
now: number;
|
|
198
|
+
/**
|
|
199
|
+
* What the code running this scope imports (`CrossVerticalRegistry.consumes()`). A move names
|
|
200
|
+
* an edge only if the consumer declares it. Otherwise a skip would plant a watermark for an
|
|
201
|
+
* edge that does not exist yet, and the backfill of a later version that adds the import would
|
|
202
|
+
* silently start from it.
|
|
203
|
+
*/
|
|
204
|
+
imports: readonly {
|
|
205
|
+
from: string;
|
|
206
|
+
}[];
|
|
207
|
+
}): ImportCursorMoved;
|
|
118
208
|
/** The note a withheld event's dead letter carries at the consumer. Names the reason, never the content. */
|
|
119
209
|
export declare function withheldNote(reason: WithheldEvent['reason'], vertical: string): string;
|
|
120
210
|
/** One registered import handler: which module, from which vertical, for which (type, version). */
|
|
@@ -145,9 +235,13 @@ export declare class CrossVerticalRegistry {
|
|
|
145
235
|
private readonly exportsByType;
|
|
146
236
|
private readonly importVersion;
|
|
147
237
|
private readonly registered;
|
|
238
|
+
/** `exportTypes()`, kept current by `register`: an invoke reads it, and must not rebuild a map to. */
|
|
239
|
+
private exportTypeList;
|
|
148
240
|
register(manifest: Pick<ModuleManifest, 'id' | 'events'>, handlers: Record<string, Record<string, ImportHandler>> | undefined): void;
|
|
149
241
|
/** type → export, over every registered module: what this deployment releases. */
|
|
150
242
|
exports(): Map<string, EventExport>;
|
|
243
|
+
/** The exported type names (#1705 PR 2), for the per-invoke kick count. Empty: exports nothing. */
|
|
244
|
+
exportTypes(): readonly string[];
|
|
151
245
|
/** What this deployment imports, one row per (source, type), sorted. */
|
|
152
246
|
consumes(): {
|
|
153
247
|
from: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vertical-events.d.ts","sourceRoot":"","sources":["../src/vertical-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"vertical-events.d.ts","sourceRoot":"","sources":["../src/vertical-events.ts"],"names":[],"mappings":"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"}
|