@serfab/cadre-core 1.16.0 → 1.18.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/README.md +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -18
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +391 -152
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One way for cadre-core to be told that a control or strand table changed, so an idle loop
|
|
3
|
+
* can wait for a change instead of re-reading on a timer. Built on Quereus's
|
|
4
|
+
* `Database.watch`, which hears both a commit made on this `Database` and, through the
|
|
5
|
+
* Optimystic plugin's `notifyExternalChange` bridge, every change this node's storage takes
|
|
6
|
+
* in from elsewhere: a consensus commit it took part in as a cohort member, a block-transfer
|
|
7
|
+
* push such as join backfill, and a read-repair landing. Events are table-wide; there are no
|
|
8
|
+
* row keys.
|
|
9
|
+
*
|
|
10
|
+
* The consumer's `onChange` never runs inside a commit. The watch handler only sets flags
|
|
11
|
+
* and arms a timer, and runs are single-flight: changes inside the coalescing window share
|
|
12
|
+
* one run, a change during a run produces exactly one follow-up run, and a consumer's own
|
|
13
|
+
* `trigger` (start, retry backoff) and the backstop go through the same rule.
|
|
14
|
+
* `docs/architecture.md` → "Table change subscriptions" says what the seam does not hear,
|
|
15
|
+
* which is why consumers keep a backstop.
|
|
16
|
+
*/
|
|
17
|
+
import debug from 'debug';
|
|
18
|
+
import type { ChangeScope, Database, Subscription, TableWatch, WatchEvent } from '@quereus/quereus';
|
|
19
|
+
import { defaultTimeoutScheduler } from './timeout-scheduler.js';
|
|
20
|
+
import type { TimeoutScheduler } from './timeout-scheduler.js';
|
|
21
|
+
|
|
22
|
+
const log = debug('sereus:cadre:table-watch');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Default coalescing window. One local commit fires the watch twice (Quereus's own
|
|
26
|
+
* post-commit dispatch, then the plugin's storage event); this folds both into one run.
|
|
27
|
+
*/
|
|
28
|
+
export const TABLE_CHANGE_COALESCE_MS = 50;
|
|
29
|
+
|
|
30
|
+
export interface TableChangeWatchOptions {
|
|
31
|
+
/** Short label for log lines (e.g. 'strand-watcher'). */
|
|
32
|
+
label: string;
|
|
33
|
+
/** Coalescing window after the first change; default {@link TABLE_CHANGE_COALESCE_MS}. */
|
|
34
|
+
coalesceMs?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Run anyway this long after the last run settled, change or not. Omit for none.
|
|
37
|
+
* The path for a change that arrives without a storage event (see the module doc).
|
|
38
|
+
*/
|
|
39
|
+
backstopMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Called inline, synchronously, from the watch handler — inside the committing call,
|
|
42
|
+
* possibly with the control write lock held — once per changed table per event, with the
|
|
43
|
+
* name as the consumer passed it. Only for setting a flag (cache invalidation). Must not
|
|
44
|
+
* read, write or await.
|
|
45
|
+
*/
|
|
46
|
+
onInvalidate?: (qualifiedTable: string) => void;
|
|
47
|
+
/** Timer seam; default real, unref'd timers. */
|
|
48
|
+
scheduler?: TimeoutScheduler;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TableChangeWatch {
|
|
52
|
+
/** Schedule a run after `delayMs` (default 0) through the same single-flight rule; an earlier pending run wins. */
|
|
53
|
+
trigger(delayMs?: number): void;
|
|
54
|
+
/** Stop: unsubscribe, clear timers; no run starts afterwards. Idempotent. Does not wait for a run in flight. */
|
|
55
|
+
dispose(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe `onChange` to changes in `tables` (qualified `'Schema.Table'` names, e.g.
|
|
60
|
+
* `'CadreControl.Strand'`). Throws synchronously on a name that is not qualified, or a table
|
|
61
|
+
* that does not exist.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: the plugin arms its storage bridge for a table on that table's first full
|
|
64
|
+
* initialization on this `Database`, so a table not yet touched hears local commits only,
|
|
65
|
+
* not replicated ones. Consumers' start runs touch their tables; the seam does not, because
|
|
66
|
+
* touching a never-written collection consults the network.
|
|
67
|
+
*
|
|
68
|
+
* NOTE: Quereus silently disposes a subscription whose table is dropped or altered. Control
|
|
69
|
+
* and strand schemas are fixed for a database's life (an app-schema attach adds `App` tables
|
|
70
|
+
* and alters no `Strand` one), so nothing re-subscribes; revisit if that stops holding.
|
|
71
|
+
*/
|
|
72
|
+
export function watchTables(
|
|
73
|
+
db: Database,
|
|
74
|
+
tables: readonly string[],
|
|
75
|
+
onChange: () => Promise<void>,
|
|
76
|
+
options: TableChangeWatchOptions,
|
|
77
|
+
): TableChangeWatch {
|
|
78
|
+
return new TableChangeWatcher({ db, tables }, onChange, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* {@link watchTables}' schedule — single-flight runs, `trigger`, the backstop — with no table
|
|
83
|
+
* subscription, for a consumer with no database to watch (tests, fakes).
|
|
84
|
+
*/
|
|
85
|
+
export function scheduleRuns(onChange: () => Promise<void>, options: TableChangeWatchOptions): TableChangeWatch {
|
|
86
|
+
return new TableChangeWatcher(undefined, onChange, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The subscriptions one database owner has handed out, so its `close()` can dispose those
|
|
91
|
+
* still live — synchronously, ahead of its own awaits, so nothing a closing commit does
|
|
92
|
+
* schedules a run. (`Database.close()` disposes them too, but only after its own awaits.)
|
|
93
|
+
*/
|
|
94
|
+
export class TableWatchRegistry {
|
|
95
|
+
private readonly live = new Set<TableChangeWatch>();
|
|
96
|
+
|
|
97
|
+
watch(
|
|
98
|
+
db: Database,
|
|
99
|
+
tables: readonly string[],
|
|
100
|
+
onChange: () => Promise<void>,
|
|
101
|
+
options: TableChangeWatchOptions,
|
|
102
|
+
): TableChangeWatch {
|
|
103
|
+
const inner = watchTables(db, tables, onChange, options);
|
|
104
|
+
const tracked: TableChangeWatch = {
|
|
105
|
+
trigger: delayMs => inner.trigger(delayMs),
|
|
106
|
+
dispose: () => {
|
|
107
|
+
this.live.delete(tracked);
|
|
108
|
+
inner.dispose();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
this.live.add(tracked);
|
|
112
|
+
return tracked;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
disposeAll(): void {
|
|
116
|
+
for (const watch of [...this.live]) {
|
|
117
|
+
watch.dispose();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** One armed timer; an object of its own so the scheduler's handles need not be unique. */
|
|
123
|
+
interface Wake {
|
|
124
|
+
handle: unknown;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The tables a {@link TableChangeWatcher} subscribes to; absent for {@link scheduleRuns}. */
|
|
128
|
+
interface WatchedTables {
|
|
129
|
+
db: Database;
|
|
130
|
+
tables: readonly string[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class TableChangeWatcher implements TableChangeWatch {
|
|
134
|
+
private readonly subscription: Subscription | undefined;
|
|
135
|
+
/** The consumer's name for each watched table, by Quereus's lowercased `schema.table` key. */
|
|
136
|
+
private readonly names = new Map<string, string>();
|
|
137
|
+
private readonly scheduler: TimeoutScheduler;
|
|
138
|
+
private readonly coalesceMs: number;
|
|
139
|
+
/**
|
|
140
|
+
* Every armed timer: the coalescing window, `trigger` delays and the backstop. Whichever
|
|
141
|
+
* fires first starts the run, and starting a run clears the rest.
|
|
142
|
+
*/
|
|
143
|
+
private readonly wakes = new Set<Wake>();
|
|
144
|
+
/** The coalescing window, while armed: a change joins it instead of extending it. */
|
|
145
|
+
private coalesceWake: Wake | undefined;
|
|
146
|
+
private running = false;
|
|
147
|
+
/** A timer fired while a run was in flight: one more run, a window after it settles. */
|
|
148
|
+
private rerun = false;
|
|
149
|
+
private disposed = false;
|
|
150
|
+
private warnedThenable = false;
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
watched: WatchedTables | undefined,
|
|
154
|
+
private readonly onChange: () => Promise<void>,
|
|
155
|
+
private readonly options: TableChangeWatchOptions,
|
|
156
|
+
) {
|
|
157
|
+
this.scheduler = options.scheduler ?? defaultTimeoutScheduler;
|
|
158
|
+
this.coalesceMs = options.coalesceMs ?? TABLE_CHANGE_COALESCE_MS;
|
|
159
|
+
this.subscription = watched && this.subscribe(watched);
|
|
160
|
+
this.armBackstop();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
trigger(delayMs = 0): void {
|
|
164
|
+
if (!this.disposed) {
|
|
165
|
+
this.arm(delayMs);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dispose(): void {
|
|
170
|
+
if (this.disposed) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.disposed = true;
|
|
174
|
+
this.subscription?.unsubscribe();
|
|
175
|
+
this.clearWakes();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private subscribe({ db, tables }: WatchedTables): Subscription {
|
|
179
|
+
const watches = tables.map(fullTableWatch);
|
|
180
|
+
watches.forEach((watch, i) => this.names.set(tableKey(watch), tables[i]));
|
|
181
|
+
const scope: ChangeScope = { watches, nonDeterministicSources: [], unboundParameters: [] };
|
|
182
|
+
// Returns undefined, so Quereus never awaits consumer work inside a commit.
|
|
183
|
+
return db.watch(scope, event => this.handleChange(event));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Runs inside the committing call, possibly under the control write lock: flags and a timer only. */
|
|
187
|
+
private handleChange(event: WatchEvent): void {
|
|
188
|
+
if (this.disposed) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.coalesceWake ??= this.arm(this.coalesceMs);
|
|
192
|
+
for (const { watch } of event.matched) {
|
|
193
|
+
this.invalidate(this.names.get(tableKey(watch)) ?? tableKey(watch));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private invalidate(table: string): void {
|
|
198
|
+
let result: unknown;
|
|
199
|
+
try {
|
|
200
|
+
result = this.options.onInvalidate?.(table);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log('%s: onInvalidate threw for %s: %O', this.options.label, table, err);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!this.warnedThenable && isThenable(result)) {
|
|
206
|
+
this.warnedThenable = true;
|
|
207
|
+
log('%s: misuse — onInvalidate returned a promise for %s; it is never awaited, and must only set a flag', this.options.label, table);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private arm(delayMs: number): Wake {
|
|
212
|
+
const wake: Wake = { handle: undefined };
|
|
213
|
+
wake.handle = this.scheduler.setTimeout(() => this.fire(wake), delayMs);
|
|
214
|
+
this.wakes.add(wake);
|
|
215
|
+
return wake;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private armBackstop(): void {
|
|
219
|
+
if (this.options.backstopMs !== undefined) {
|
|
220
|
+
this.arm(this.options.backstopMs);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private clearWakes(): void {
|
|
225
|
+
for (const wake of this.wakes) {
|
|
226
|
+
this.scheduler.clearTimeout(wake.handle);
|
|
227
|
+
}
|
|
228
|
+
this.wakes.clear();
|
|
229
|
+
this.coalesceWake = undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private fire(wake: Wake): void {
|
|
233
|
+
if (!this.wakes.delete(wake)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (wake === this.coalesceWake) {
|
|
237
|
+
this.coalesceWake = undefined;
|
|
238
|
+
}
|
|
239
|
+
if (this.running) {
|
|
240
|
+
this.rerun = true;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.clearWakes();
|
|
244
|
+
this.rerun = false;
|
|
245
|
+
this.running = true;
|
|
246
|
+
void this.run();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async run(): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await this.onChange();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
log('%s: change run failed: %O', this.options.label, err);
|
|
254
|
+
}
|
|
255
|
+
this.running = false;
|
|
256
|
+
this.settled();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private settled(): void {
|
|
260
|
+
if (this.disposed) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.rerun) {
|
|
264
|
+
this.coalesceWake ??= this.arm(this.coalesceMs);
|
|
265
|
+
}
|
|
266
|
+
this.armBackstop();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function fullTableWatch(qualifiedTable: string): TableWatch {
|
|
271
|
+
const [schema, table, ...rest] = qualifiedTable.split('.');
|
|
272
|
+
if (!schema || !table || rest.length > 0) {
|
|
273
|
+
throw new Error(`watchTables: '${qualifiedTable}' is not a qualified 'Schema.Table' name`);
|
|
274
|
+
}
|
|
275
|
+
return { table: { schema, table }, columns: 'all', scope: { kind: 'full' } };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Quereus's own matching key for a watched table. */
|
|
279
|
+
function tableKey(watch: TableWatch): string {
|
|
280
|
+
return `${watch.table.schema}.${watch.table.table}`.toLowerCase();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isThenable(value: unknown): boolean {
|
|
284
|
+
return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function';
|
|
285
|
+
}
|
package/src/timeout-scheduler.ts
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The timer seam shared by the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The timer seam shared by the loops that re-arm themselves one step at a time — the
|
|
3
|
+
* membership reconciler's retry ladder (`strand-membership-reconciler.ts`) — and by the table
|
|
4
|
+
* change subscription's coalescing, trigger and backstop timers (`table-change-watch.ts`), on
|
|
5
|
+
* which the first-sync write gate's probes run (`strand-first-sync-gate.ts`). `setTimeout`
|
|
6
|
+
* rather than a repeating interval because the next step is armed only once the previous
|
|
7
|
+
* one settles, so a slow network read never stacks steps behind it, and because the delay
|
|
8
|
+
* can differ per step.
|
|
9
|
+
*
|
|
10
|
+
* Tests inject a hand-cranked clock; production omits it and gets real timers, `unref`'d so
|
|
11
|
+
* a loop that is only waiting never holds a Node process open (the call is optional —
|
|
12
|
+
* browsers and React Native return a plain handle with no `unref`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Timer seam for a self-rescheduling loop; omit for real (unref'd) timeouts. */
|
|
16
|
+
export interface TimeoutScheduler {
|
|
17
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
18
|
+
clearTimeout(handle: unknown): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Real timers, `unref`'d where the runtime supports it. */
|
|
22
|
+
export const defaultTimeoutScheduler: TimeoutScheduler = {
|
|
23
|
+
setTimeout: (fn, ms) => {
|
|
24
|
+
const handle = setTimeout(fn, ms);
|
|
25
|
+
(handle as { unref?: () => void }).unref?.();
|
|
26
|
+
return handle;
|
|
27
|
+
},
|
|
28
|
+
clearTimeout: (handle) => clearTimeout(handle as Parameters<typeof clearTimeout>[0])
|
|
29
|
+
};
|