@objectstack/core 17.0.0-rc.1 → 17.0.0-rc.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,253 @@
1
1
  # @objectstack/core
2
2
 
3
+ ## 17.0.0-rc.2
4
+
5
+ ### Minor Changes
6
+
7
+ - 98877c9: feat(core,platform-objects,spec): the ADR-0119 D2 migration-journal runner — a migration killed mid-run is resumable to completion or compensable to clean, with journal rows proving which (#4617)
8
+
9
+ **The gap D1 left open.** ADR-0119 D1 made `engine.transaction()` reachable
10
+ through the contract, which is the right answer for multi-write atomicity that
11
+ fits in one transaction. Migration-class work does not fit: a million-row
12
+ backfill cannot hold one write-lock for its duration, `driver-memory`'s
13
+ `beginTransaction` deep-clones the entire database (O(db) per begin),
14
+ `ObjectQL.transaction()` binds the **default driver only** so a multi-datasource
15
+ migration silently commits part of its work outside it, and a process **killed**
16
+ — as distinct from a thrown error — defeats in-process rollback entirely. So the
17
+ unit of atomicity is the _chunk_, and durability across chunks is a journal.
18
+
19
+ Four consumers had each converged on the same four moves — dry-run preflight,
20
+ undo journal, LIFO compensation, re-entrant forward recovery (ADR-0105 D13
21
+ promotion, ADR-0117 D8's ownership backfill, the org lifecycle transitions, and
22
+ D10 master-data distribution #4585). One copy is engineering; four is platform
23
+ debt, and the fourth author would have had to rediscover the invariant below
24
+ from scratch.
25
+
26
+ **New: `runMigrationJournal` (`@objectstack/core`).** Preflight runs every
27
+ step's read-only validator before any step writes, so a plan that would fail at
28
+ step 3 has not written step 1. Rows are chunked per the `bulk-write.ts`
29
+ discipline; each chunk's writes run inside `engine.transaction()`. On failure,
30
+ committed chunks are compensated newest-first, each in its own transaction. On
31
+ restart, a rediscovered run resumes forward from the first chunk lacking
32
+ `chunk_done`, or unwinds, per the plan's `onCrash` policy. Forward and
33
+ compensate callbacks receive an `attempt` counter; `attempt > 1` means the prior
34
+ outcome is UNKNOWN and the callback must recheck by natural key before
35
+ re-writing — the same at-least-once contract `bulk-write.ts` already documents,
36
+ reused rather than re-derived.
37
+
38
+ **The invariant that carries the design:** `chunk_done(i)` is written **inside**
39
+ the chunk's own transaction, so `done ⇔ committed` holds by construction;
40
+ `chunk_started(i)` is written autonomously **before** it. That asymmetry is what
41
+ gives `started ∧ ¬done` exactly one meaning — _the outcome is unknown_ — which
42
+ is the only state a crash can leave and the only state recovery reasons about.
43
+ Making both writes symmetric would look tidier and would destroy recovery.
44
+
45
+ **New: `sys_migration_journal` (`@objectstack/platform-objects`).** Rows keyed
46
+ `(run_id, seq)` under a unique index, so a resumed run that miscomputes its next
47
+ sequence fails loudly rather than double-recording an event. Registered
48
+ unconditionally alongside `sys_migration` because recovery must be discoverable
49
+ with **zero host wiring** — a journal some kernels compose and others do not is
50
+ a journal a boot scanner cannot rely on (ADR-0078). Distinct in grain from
51
+ `sys_migration`, which holds one durable verdict per named migration; this holds
52
+ many rows per _run_. Read-only over the API; writes go through the runner in
53
+ system context.
54
+
55
+ **The runner refuses rather than degrades**, in four places: the runtime cannot
56
+ roll back; any preflight fails; the plan declares `onCrash: 'compensate'` but a
57
+ step cannot compensate; or a resume's plan hash disagrees with the journal
58
+ (resuming a changed plan would apply chunk boundaries the journal never
59
+ described). A compensation failure halts and is journalled — never swallowed —
60
+ and the run ends `failed`, not `compensated`, because a database in a state no
61
+ clean story covers must not be reported as a tidy rollback.
62
+
63
+ **`engineCanRollBack` is now shared.** The two-level probe (engine method AND
64
+ default-driver `beginTransaction`) was the same condition written twice — here
65
+ and in `batchData`'s atomic gate. It now lives in `@objectstack/core` and
66
+ `@objectstack/metadata-protocol` imports it, as a type predicate so callers do
67
+ not each re-narrow the optional member by hand. Two copies of "can this runtime
68
+ actually roll back?" drift by one clause and leave one caller believing it has
69
+ atomicity it does not have.
70
+
71
+ Boot reconciliation and `os migrate resume` land separately; `findInterruptedRuns`
72
+ is the discovery primitive they will consume, and is exported here.
73
+
74
+ **Docs:** ADR-0118 (plugin-reachable transactions) is renumbered **ADR-0119**.
75
+ It merged one day after an unrelated ADR-0118 (非用户 actor 的平台契约) and the
76
+ earlier merge holds the number; citations of "ADR-0118 D1/D2/D3/D4" written
77
+ before 2026-08-03 mean the renumbered record.
78
+
79
+ - 071d0dc: feat(runtime,cli,core): boot reconciliation and `os migrate resume` for the migration journal — an interrupted run can no longer go unnoticed (ADR-0119 D2, #4617)
80
+
81
+ Completes ADR-0119 D2. The runner and `sys_migration_journal` landed in #4668; this is the discovery channel that makes an interrupted run findable by someone who does not already know it happened.
82
+
83
+ **`MigrationRecoveryPlugin` (`@objectstack/runtime`)** — at `kernel:ready`, scans the journal for runs that started and never concluded, and warns per run: how many chunks committed, which have an **unknown** outcome (`chunk_started` with no `chunk_done`), whether a compensation was left half-finished, and the exact command that will act. It also owns the `migration-plans` registry service.
84
+
85
+ **`os migrate resume` (`@objectstack/cli`)** — lists interrupted runs (read-only, the default), or acts on one with `--run <id>`, under confirmation. Exits non-zero when a run ends `failed`, so a scripted recovery cannot move on from a migration that needs a human.
86
+
87
+ **`MigrationPlanRegistry` (`@objectstack/core`)** — where a resume finds the plan it has to re-run.
88
+
89
+ ## Boot discovers, the CLI acts
90
+
91
+ This is the design decision, and it is deliberate rather than incidental.
92
+
93
+ Resuming is a large, irreversible, potentially hour-long write against production data. Doing that as an unrequested side effect of a process starting is the kind of behaviour an operator finds out about from a graph. It is also not always possible at boot: a resume needs the plan's live callbacks, and the package that owns them may not be loaded in whichever process happened to restart first.
94
+
95
+ So boot surfaces the run and names the command; the command acts, under explicit operator intent. ADR-0119 D2's per-plan `onCrash` policy still decides **what** acting means — resume forward from the first chunk lacking `chunk_done`, or unwind what committed — it just does not decide **when**, and "when" is the part a human should own.
96
+
97
+ Deferring is safe precisely because of the runner's re-entrancy: `started ∧ ¬done` is durable, so an interrupted run stays exactly as recoverable an hour later as it was at boot. Nothing decays while the operator decides.
98
+
99
+ ## Why a plan registry exists at all
100
+
101
+ A journal cannot hold a plan. `forward` and `compensate` are functions and `load()` reads the live database, so none of it crosses a process boundary — which is why the journal records the plan **hash**, not the plan. Recovery therefore needs the plan handed back by the code that owns it, and `migration-plans` is that seam: between "the journal knows a run stopped at chunk 7" and "something in this process knows what chunk 7 was supposed to do".
102
+
103
+ A run whose plan no loaded package registers is **reported**, never silently skipped — the operator is told which plan id is missing. "Nothing to resume" and "the code that owns this run is not here" are different facts, and only one of them is safe to ignore.
104
+
105
+ ## Degradation
106
+
107
+ No engine, or no `sys_migration_journal` registered (a lean kernel that never composed platform-objects) → the scan is skipped in **silence**: such a kernel has no interrupted runs to find, and a warning there would train operators to ignore this plugin's output, which is the one thing it cannot afford. A scan that **fails**, by contrast, is reported — "I could not check" and "there is nothing to find" are different answers.
108
+
109
+ 11 new tests pin the split (boot writes nothing to the journal), the three states an operator must tell apart (clean / interrupted / half-unwound), and both degradation paths.
110
+
111
+ ### Patch Changes
112
+
113
+ - 833b512: fix(core): 插件 init/start 的超时守卫定时器在 race 结束时被清除,进程不再空转 `startupTimeout` (#4813)
114
+
115
+ `ObjectKernel.initPluginWithTimeout()` / `startPluginWithTimeout()` 各自 `setTimeout` armed
116
+ 一根超时守卫,然后**把它扔了**:插件赢下 race 之后,那根定时器既没 `clearTimeout` 也没
117
+ `unref()`,带着 ref 一直挂到 `startupTimeout` 走完。于是每个进程在活干完之后还要空转整整
118
+ 一个 `startupTimeout` —— `ObjectQLPlugin` 是 120 秒。
119
+
120
+ 实测(`examples/app-crm`,同一条 `migrate recorded-by --json`,同一个构建链,唯一差别是本
121
+ 改动):
122
+
123
+ | | 墙钟 |
124
+ | :----- | :----- |
125
+ | 修复前 | 122.4s |
126
+ | 修复后 | 3.1s |
127
+
128
+ JSON 与 `✅ Graceful shutdown complete` 两次都在 ~3 秒出现 —— 后面那 119 秒纯粹是 8 根
129
+ 孤儿定时器(4 个 init + 4 个 start)钉着事件循环。`os serve` 里同样漏,只是那里进程本来
130
+ 就长命,看不出来。
131
+
132
+ **为什么是 `clearTimeout` 而不是 `unref()`。** 隔壁 `shutdown()` 的守卫用的是 `unref()`,
133
+ 但那个写法在这里是错的,而且不是风格问题:`unref()` 让定时器不再钉住事件循环,**同时也
134
+ 让它不再是一个守卫** —— 若 hook 永不 settle 且没有别的东西撑着事件循环,Node 会在定时器
135
+ 触发之前直接退出,超时被**静默吞掉**,谁也不会收到那个 error。守卫必须在 race 未决期间
136
+ 保持 ref'd,在 race 落定的那一刻被回收,这正是 `finally { clearTimeout(guard) }` 表达的
137
+ 语义。两个守卫合并为一个私有 helper `raceStartupTimeout()`,措辞与理由写在它的 doc
138
+ comment 里。
139
+
140
+ `startupTimeout` 的取值一个都没动 —— 慢启动的插件需要那个上限,问题从来不在时长,而在
141
+ 没人回收。
142
+
143
+ - Updated dependencies [430dcc2]
144
+ - Updated dependencies [e6ac4bd]
145
+ - Updated dependencies [80334c7]
146
+ - Updated dependencies [ce5242c]
147
+ - Updated dependencies [a7163ea]
148
+ - Updated dependencies [e6e9379]
149
+ - Updated dependencies [98877c9]
150
+ - Updated dependencies [98877c9]
151
+ - Updated dependencies [e6b1b69]
152
+ - Updated dependencies [ad047d2]
153
+ - Updated dependencies [2826d1e]
154
+ - Updated dependencies [5a84d41]
155
+ - Updated dependencies [20b1a9e]
156
+ - Updated dependencies [203a449]
157
+ - Updated dependencies [ac37fc6]
158
+ - Updated dependencies [4820f55]
159
+ - Updated dependencies [462d9c4]
160
+ - Updated dependencies [7d21581]
161
+ - Updated dependencies [f2445c9]
162
+ - Updated dependencies [23338c3]
163
+ - Updated dependencies [5b843fb]
164
+ - Updated dependencies [b4487aa]
165
+ - Updated dependencies [65ca83a]
166
+ - Updated dependencies [67bf2e2]
167
+ - Updated dependencies [c6d1cb4]
168
+ - Updated dependencies [36030ff]
169
+ - Updated dependencies [6117f7b]
170
+ - Updated dependencies [e533b0b]
171
+ - Updated dependencies [cdf4d9a]
172
+ - Updated dependencies [aee1806]
173
+ - Updated dependencies [c13350b]
174
+ - Updated dependencies [c13350b]
175
+ - Updated dependencies [9ca2d85]
176
+ - Updated dependencies [c13350b]
177
+ - Updated dependencies [891d345]
178
+ - Updated dependencies [a52e2ef]
179
+ - Updated dependencies [5293114]
180
+ - Updated dependencies [20bc357]
181
+ - Updated dependencies [5966c2a]
182
+ - Updated dependencies [2382580]
183
+ - Updated dependencies [d9fa683]
184
+ - Updated dependencies [3c7bcc0]
185
+ - Updated dependencies [4b6cac7]
186
+ - Updated dependencies [7631964]
187
+ - Updated dependencies [ac471a0]
188
+ - Updated dependencies [60ae58e]
189
+ - Updated dependencies [ce92674]
190
+ - Updated dependencies [9f601e8]
191
+ - Updated dependencies [51c5227]
192
+ - Updated dependencies [a4a85c8]
193
+ - Updated dependencies [07a4e26]
194
+ - Updated dependencies [ec975f1]
195
+ - Updated dependencies [eb4204b]
196
+ - Updated dependencies [4f13be2]
197
+ - Updated dependencies [61cc079]
198
+ - Updated dependencies [0e96e46]
199
+ - Updated dependencies [d52d4fe]
200
+ - Updated dependencies [742cebb]
201
+ - Updated dependencies [ce92674]
202
+ - Updated dependencies [cf2c9b7]
203
+ - Updated dependencies [0f9faa2]
204
+ - Updated dependencies [7cf42fe]
205
+ - Updated dependencies [5966c2a]
206
+ - Updated dependencies [f78dd83]
207
+ - Updated dependencies [a2cd18a]
208
+ - Updated dependencies [4638aaa]
209
+ - Updated dependencies [0222d3c]
210
+ - Updated dependencies [0a936ea]
211
+ - Updated dependencies [023c00b]
212
+ - Updated dependencies [155507e]
213
+ - Updated dependencies [7bba90b]
214
+ - Updated dependencies [7e05d8e]
215
+ - Updated dependencies [061406d]
216
+ - Updated dependencies [c1f344b]
217
+ - Updated dependencies [9c93465]
218
+ - Updated dependencies [ebb209c]
219
+ - Updated dependencies [63b33e6]
220
+ - Updated dependencies [2a44c1d]
221
+ - Updated dependencies [695cfbd]
222
+ - Updated dependencies [7445149]
223
+ - Updated dependencies [071d0dc]
224
+ - Updated dependencies [0848bea]
225
+ - Updated dependencies [d51bed2]
226
+ - Updated dependencies [b8b3c64]
227
+ - Updated dependencies [0c0fbd9]
228
+ - Updated dependencies [f3141d8]
229
+ - Updated dependencies [5a84d41]
230
+ - Updated dependencies [fd3013a]
231
+ - Updated dependencies [21676eb]
232
+ - Updated dependencies [e336549]
233
+ - Updated dependencies [d40f43a]
234
+ - Updated dependencies [e5e7ee0]
235
+ - Updated dependencies [a2ebea2]
236
+ - Updated dependencies [800bdb0]
237
+ - Updated dependencies [04f1182]
238
+ - Updated dependencies [5647006]
239
+ - Updated dependencies [38f7e4f]
240
+ - Updated dependencies [c57f3cf]
241
+ - Updated dependencies [97faca3]
242
+ - Updated dependencies [ad5fe25]
243
+ - Updated dependencies [ea90179]
244
+ - Updated dependencies [ce92674]
245
+ - Updated dependencies [5ef0b5b]
246
+ - Updated dependencies [48fbacb]
247
+ - Updated dependencies [355e951]
248
+ - Updated dependencies [dadb43f]
249
+ - @objectstack/spec@17.0.0-rc.2
250
+
3
251
  ## 17.0.0-rc.1
4
252
 
5
253
  ### Minor Changes