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

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