@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.
Files changed (268) hide show
  1. package/README.md +4 -3
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +81 -142
  6. package/dist/cadre-invite-protocol.js +168 -458
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +1089 -475
  12. package/dist/cadre-node.js +2170 -1037
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +88 -32
  18. package/dist/control-cohort.js +91 -33
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +856 -318
  21. package/dist/control-database.js +1422 -701
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-formation-recorder.d.ts +46 -43
  24. package/dist/control-formation-recorder.js +77 -78
  25. package/dist/control-formation-recorder.js.map +1 -1
  26. package/dist/control-protocol-guard.d.ts +11 -4
  27. package/dist/control-protocol-guard.js +20 -5
  28. package/dist/control-protocol-guard.js.map +1 -1
  29. package/dist/control-schema-version.d.ts +97 -0
  30. package/dist/control-schema-version.js +225 -0
  31. package/dist/control-schema-version.js.map +1 -0
  32. package/dist/control-schema.d.ts +45 -5
  33. package/dist/control-schema.js +262 -78
  34. package/dist/control-schema.js.map +1 -1
  35. package/dist/control-write-retry.d.ts +45 -10
  36. package/dist/control-write-retry.js +92 -22
  37. package/dist/control-write-retry.js.map +1 -1
  38. package/dist/delegate-admission.d.ts +13 -0
  39. package/dist/delegate-admission.js +27 -0
  40. package/dist/delegate-admission.js.map +1 -1
  41. package/dist/ed25519-key.d.ts +10 -7
  42. package/dist/ed25519-key.js +23 -20
  43. package/dist/ed25519-key.js.map +1 -1
  44. package/dist/edge-node/config.d.ts +122 -0
  45. package/dist/edge-node/config.js +199 -0
  46. package/dist/edge-node/config.js.map +1 -0
  47. package/dist/edge-node/dissolve.d.ts +95 -0
  48. package/dist/edge-node/dissolve.js +184 -0
  49. package/dist/edge-node/dissolve.js.map +1 -0
  50. package/dist/edge-node/edge-join.d.ts +44 -0
  51. package/dist/edge-node/edge-join.js +227 -0
  52. package/dist/edge-node/edge-join.js.map +1 -0
  53. package/dist/edge-node/edge-node.d.ts +295 -0
  54. package/dist/edge-node/edge-node.js +704 -0
  55. package/dist/edge-node/edge-node.js.map +1 -0
  56. package/dist/edge-node/index.d.ts +18 -0
  57. package/dist/edge-node/index.js +18 -0
  58. package/dist/edge-node/index.js.map +1 -0
  59. package/dist/edge-node/join-credential.d.ts +11 -0
  60. package/dist/edge-node/join-credential.js +11 -0
  61. package/dist/edge-node/join-credential.js.map +1 -0
  62. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  63. package/dist/edge-node/lazy-raw-storage.js +134 -0
  64. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  65. package/dist/edge-node/node-local.d.ts +60 -0
  66. package/dist/edge-node/node-local.js +58 -0
  67. package/dist/edge-node/node-local.js.map +1 -0
  68. package/dist/edge-node/options.d.ts +83 -0
  69. package/dist/edge-node/options.js +110 -0
  70. package/dist/edge-node/options.js.map +1 -0
  71. package/dist/edge-node/owner-grant.d.ts +41 -0
  72. package/dist/edge-node/owner-grant.js +38 -0
  73. package/dist/edge-node/owner-grant.js.map +1 -0
  74. package/dist/edge-node/platform.d.ts +140 -0
  75. package/dist/edge-node/platform.js +12 -0
  76. package/dist/edge-node/platform.js.map +1 -0
  77. package/dist/edge-node/strand-move.d.ts +115 -0
  78. package/dist/edge-node/strand-move.js +223 -0
  79. package/dist/edge-node/strand-move.js.map +1 -0
  80. package/dist/edge-node/strands.d.ts +28 -0
  81. package/dist/edge-node/strands.js +60 -0
  82. package/dist/edge-node/strands.js.map +1 -0
  83. package/dist/enrolled-machine-store.d.ts +3 -3
  84. package/dist/enrolled-machine-store.js +3 -3
  85. package/dist/formation-token.d.ts +48 -0
  86. package/dist/formation-token.js +26 -0
  87. package/dist/formation-token.js.map +1 -0
  88. package/dist/index.d.ts +27 -17
  89. package/dist/index.js +39 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/joined-strand-store.d.ts +29 -12
  92. package/dist/joined-strand-store.js +66 -27
  93. package/dist/joined-strand-store.js.map +1 -1
  94. package/dist/key-store.d.ts +9 -4
  95. package/dist/key-store.js +9 -4
  96. package/dist/key-store.js.map +1 -1
  97. package/dist/link-budget.d.ts +9 -7
  98. package/dist/link-budget.js +9 -7
  99. package/dist/link-budget.js.map +1 -1
  100. package/dist/machine-removal.d.ts +19 -39
  101. package/dist/machine-removal.js +24 -98
  102. package/dist/machine-removal.js.map +1 -1
  103. package/dist/member-redemption.d.ts +256 -0
  104. package/dist/member-redemption.js +490 -0
  105. package/dist/member-redemption.js.map +1 -0
  106. package/dist/membership-view.d.ts +91 -0
  107. package/dist/membership-view.js +110 -0
  108. package/dist/membership-view.js.map +1 -0
  109. package/dist/owner-anchor-sync.d.ts +4 -4
  110. package/dist/owner-anchor-sync.js +16 -15
  111. package/dist/owner-anchor-sync.js.map +1 -1
  112. package/dist/owner-grant-protocol.d.ts +251 -0
  113. package/dist/owner-grant-protocol.js +344 -0
  114. package/dist/owner-grant-protocol.js.map +1 -0
  115. package/dist/owner-key-export.d.ts +70 -0
  116. package/dist/owner-key-export.js +279 -0
  117. package/dist/owner-key-export.js.map +1 -0
  118. package/dist/owner-key-flows.d.ts +55 -0
  119. package/dist/owner-key-flows.js +60 -0
  120. package/dist/owner-key-flows.js.map +1 -0
  121. package/dist/owner-key-store.d.ts +31 -0
  122. package/dist/owner-key-store.js +81 -0
  123. package/dist/owner-key-store.js.map +1 -0
  124. package/dist/owner-keys.d.ts +78 -0
  125. package/dist/owner-keys.js +133 -0
  126. package/dist/owner-keys.js.map +1 -0
  127. package/dist/owner-signer.d.ts +54 -0
  128. package/dist/owner-signer.js +51 -0
  129. package/dist/owner-signer.js.map +1 -0
  130. package/dist/peer-addr-book.d.ts +10 -1
  131. package/dist/peer-addr-book.js +4 -3
  132. package/dist/peer-addr-book.js.map +1 -1
  133. package/dist/peer-authorization.d.ts +83 -18
  134. package/dist/peer-authorization.js +123 -53
  135. package/dist/peer-authorization.js.map +1 -1
  136. package/dist/peer-join-backfill.d.ts +89 -20
  137. package/dist/peer-join-backfill.js +265 -108
  138. package/dist/peer-join-backfill.js.map +1 -1
  139. package/dist/pending-join-runner.d.ts +48 -18
  140. package/dist/pending-join-runner.js +74 -56
  141. package/dist/pending-join-runner.js.map +1 -1
  142. package/dist/provider/rent-node.js +1 -1
  143. package/dist/provider/rent-node.js.map +1 -1
  144. package/dist/seed-bootstrap.d.ts +162 -133
  145. package/dist/seed-bootstrap.js +300 -283
  146. package/dist/seed-bootstrap.js.map +1 -1
  147. package/dist/seed-trust-policy.js +4 -4
  148. package/dist/strand-addr-protocol.d.ts +4 -1
  149. package/dist/strand-addr-protocol.js +8 -2
  150. package/dist/strand-addr-protocol.js.map +1 -1
  151. package/dist/strand-cohort.d.ts +3 -0
  152. package/dist/strand-cohort.js.map +1 -1
  153. package/dist/strand-database.d.ts +24 -2
  154. package/dist/strand-database.js +29 -6
  155. package/dist/strand-database.js.map +1 -1
  156. package/dist/strand-first-sync-gate.d.ts +176 -88
  157. package/dist/strand-first-sync-gate.js +288 -121
  158. package/dist/strand-first-sync-gate.js.map +1 -1
  159. package/dist/strand-formation-deadlines.d.ts +9 -9
  160. package/dist/strand-formation-deadlines.js +9 -9
  161. package/dist/strand-formation-manager.d.ts +23 -17
  162. package/dist/strand-formation-manager.js +46 -44
  163. package/dist/strand-formation-manager.js.map +1 -1
  164. package/dist/strand-formation-protocol.d.ts +5 -8
  165. package/dist/strand-formation-protocol.js +101 -16
  166. package/dist/strand-formation-protocol.js.map +1 -1
  167. package/dist/strand-instance-manager.d.ts +27 -18
  168. package/dist/strand-instance-manager.js +119 -50
  169. package/dist/strand-instance-manager.js.map +1 -1
  170. package/dist/strand-membership-reconciler.d.ts +16 -13
  171. package/dist/strand-membership-reconciler.js +30 -22
  172. package/dist/strand-membership-reconciler.js.map +1 -1
  173. package/dist/strand-membership-writer.d.ts +45 -22
  174. package/dist/strand-membership-writer.js +226 -131
  175. package/dist/strand-membership-writer.js.map +1 -1
  176. package/dist/strand-move.d.ts +6 -5
  177. package/dist/strand-move.js +36 -32
  178. package/dist/strand-move.js.map +1 -1
  179. package/dist/strand-peer-redial.js +2 -3
  180. package/dist/strand-peer-redial.js.map +1 -1
  181. package/dist/strand-revocation-enforcer.d.ts +134 -60
  182. package/dist/strand-revocation-enforcer.js +144 -71
  183. package/dist/strand-revocation-enforcer.js.map +1 -1
  184. package/dist/strand-solicitation.d.ts +50 -39
  185. package/dist/strand-solicitation.js +10 -3
  186. package/dist/strand-solicitation.js.map +1 -1
  187. package/dist/strand-watcher.d.ts +80 -13
  188. package/dist/strand-watcher.js +152 -48
  189. package/dist/strand-watcher.js.map +1 -1
  190. package/dist/table-change-watch.d.ts +63 -0
  191. package/dist/table-change-watch.js +209 -0
  192. package/dist/table-change-watch.js.map +1 -0
  193. package/dist/timeout-scheduler.d.ts +4 -3
  194. package/dist/timeout-scheduler.js +4 -3
  195. package/dist/timeout-scheduler.js.map +1 -1
  196. package/dist/types.d.ts +374 -150
  197. package/dist/types.js +4 -4
  198. package/dist/types.js.map +1 -1
  199. package/package.json +17 -8
  200. package/src/bootstrap-peer-store-file.ts +44 -44
  201. package/src/bootstrap-peer-store.ts +214 -214
  202. package/src/cadre-invite-protocol.ts +740 -1052
  203. package/src/cadre-join-request.ts +24 -3
  204. package/src/cadre-node.ts +2402 -1079
  205. package/src/control-authorization.ts +47 -14
  206. package/src/control-cohort.ts +204 -128
  207. package/src/control-database.ts +1749 -820
  208. package/src/control-formation-recorder.ts +359 -353
  209. package/src/control-protocol-guard.ts +23 -6
  210. package/src/control-schema-version.ts +301 -0
  211. package/src/control-schema.ts +273 -78
  212. package/src/control-write-retry.ts +641 -567
  213. package/src/delegate-admission.ts +351 -314
  214. package/src/ed25519-key.ts +174 -171
  215. package/src/edge-node/config.ts +277 -0
  216. package/src/edge-node/dissolve.ts +251 -0
  217. package/src/edge-node/edge-join.ts +319 -0
  218. package/src/edge-node/edge-node.ts +1045 -0
  219. package/src/edge-node/index.ts +59 -0
  220. package/src/edge-node/join-credential.ts +16 -0
  221. package/src/edge-node/lazy-raw-storage.ts +178 -0
  222. package/src/edge-node/node-local.ts +84 -0
  223. package/src/edge-node/options.ts +179 -0
  224. package/src/edge-node/owner-grant.ts +63 -0
  225. package/src/edge-node/platform.ts +151 -0
  226. package/src/edge-node/strand-move.ts +270 -0
  227. package/src/edge-node/strands.ts +72 -0
  228. package/src/enrolled-machine-store.ts +3 -3
  229. package/src/formation-token.ts +63 -0
  230. package/src/index.ts +899 -789
  231. package/src/joined-strand-store.ts +73 -28
  232. package/src/key-store.ts +10 -4
  233. package/src/link-budget.ts +9 -7
  234. package/src/machine-removal.ts +26 -111
  235. package/src/member-redemption.ts +649 -0
  236. package/src/membership-view.ts +144 -0
  237. package/src/owner-anchor-sync.ts +19 -18
  238. package/src/owner-grant-protocol.ts +547 -0
  239. package/src/owner-key-export.ts +348 -0
  240. package/src/owner-key-flows.ts +109 -0
  241. package/src/owner-key-store.ts +85 -0
  242. package/src/owner-keys.ts +178 -0
  243. package/src/owner-signer.ts +94 -0
  244. package/src/peer-addr-book.ts +4 -3
  245. package/src/peer-authorization.ts +558 -488
  246. package/src/peer-join-backfill.ts +971 -761
  247. package/src/pending-join-runner.ts +76 -53
  248. package/src/provider/rent-node.ts +1 -1
  249. package/src/seed-bootstrap.ts +343 -349
  250. package/src/seed-trust-policy.ts +4 -4
  251. package/src/strand-addr-protocol.ts +547 -538
  252. package/src/strand-cohort.ts +4 -0
  253. package/src/strand-database.ts +352 -319
  254. package/src/strand-first-sync-gate.ts +334 -133
  255. package/src/strand-formation-deadlines.ts +227 -227
  256. package/src/strand-formation-manager.ts +731 -725
  257. package/src/strand-formation-protocol.ts +122 -24
  258. package/src/strand-instance-manager.ts +2092 -2026
  259. package/src/strand-membership-reconciler.ts +33 -25
  260. package/src/strand-membership-writer.ts +2093 -1974
  261. package/src/strand-move.ts +50 -38
  262. package/src/strand-peer-redial.ts +2 -3
  263. package/src/strand-revocation-enforcer.ts +740 -654
  264. package/src/strand-solicitation.ts +61 -34
  265. package/src/strand-watcher.ts +175 -56
  266. package/src/table-change-watch.ts +285 -0
  267. package/src/timeout-scheduler.ts +29 -28
  268. 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
+ }
@@ -1,28 +1,29 @@
1
- /**
2
- * The timer seam shared by the strand bring-up loops that re-arm themselves one step at a
3
- * time — the first-sync write gate's probe loop (`strand-first-sync-gate.ts`) and the
4
- * membership reconciler's retry ladder (`strand-membership-reconciler.ts`). `setTimeout`
5
- * rather than a repeating interval because the next step is armed only once the previous
6
- * one settles, so a slow network read never stacks steps behind it, and because the delay
7
- * can differ per step.
8
- *
9
- * Tests inject a hand-cranked clock; production omits it and gets real timers, `unref`'d so
10
- * a loop that is only waiting never holds a Node process open (the call is optional —
11
- * browsers and React Native return a plain handle with no `unref`).
12
- */
13
-
14
- /** Timer seam for a self-rescheduling loop; omit for real (unref'd) timeouts. */
15
- export interface TimeoutScheduler {
16
- setTimeout(fn: () => void, ms: number): unknown;
17
- clearTimeout(handle: unknown): void;
18
- }
19
-
20
- /** Real timers, `unref`'d where the runtime supports it. */
21
- export const defaultTimeoutScheduler: TimeoutScheduler = {
22
- setTimeout: (fn, ms) => {
23
- const handle = setTimeout(fn, ms);
24
- (handle as { unref?: () => void }).unref?.();
25
- return handle;
26
- },
27
- clearTimeout: (handle) => clearTimeout(handle as Parameters<typeof clearTimeout>[0])
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
+ };