@zerotal/arch 1.12.0 → 1.13.1
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/docs/changelog.md +98 -0
- package/docs/flow/index.md +1 -1
- package/docs/queue.md +31 -0
- package/docs/support-policy.md +3 -2
- package/docs/upgrade.md +58 -0
- package/package.json +3 -3
package/docs/changelog.md
CHANGED
|
@@ -27,6 +27,104 @@ the section for every version you cross and apply its migration notes, not only
|
|
|
27
27
|
majors. [Releases and versioning](/docs/support-policy#releases-and-versioning) explains
|
|
28
28
|
when that carve-out ends.
|
|
29
29
|
|
|
30
|
+
## 1.13.1 — 2026-08-31
|
|
31
|
+
|
|
32
|
+
One addition, found by sizing a job rather than doing it.
|
|
33
|
+
|
|
34
|
+
The 2.0 ledger carries an entry to prefix every `@internal` export with `_` — 270 symbols,
|
|
35
|
+
filed as mechanical. Three of them are `Job` classes, and a job class name is not a source
|
|
36
|
+
symbol: `JobRegistry` keys on it and that string is written into the persisted queue payload,
|
|
37
|
+
so a job enqueued yesterday is resolved by today's process. Renaming one invalidates every job
|
|
38
|
+
already in the queue, at deploy time rather than at change time, and no test sees it because a
|
|
39
|
+
test enqueues and runs in the same process.
|
|
40
|
+
|
|
41
|
+
The rename is re-scoped. The hazard it exposed is fixed here.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`Job.jobName`** — a declared name for the queue payload, so renaming a job class is free:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export class SendWelcomeEmail extends Job {
|
|
49
|
+
static override jobName = "SendWelcomeEmail"; // survives a class rename
|
|
50
|
+
async handle(): Promise<void> {}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
It defaults to the class name, so a job that declares nothing behaves exactly as before.
|
|
55
|
+
This is the same tool [`Migration.id`](/docs/upgrade#1-10-to-1-11) is for a migration's
|
|
56
|
+
filename, shipped in 1.11.0 for the same reason: an identity the framework derived from a
|
|
57
|
+
name someone was free to change, with no way to say otherwise.
|
|
58
|
+
|
|
59
|
+
It also decouples the queue from a build that mangles names. `zt compile` does not minify
|
|
60
|
+
today, so that is not a live hazard — but nothing in the registry said it depended on that,
|
|
61
|
+
and an assumption worth relying on is worth writing down.
|
|
62
|
+
|
|
63
|
+
## 1.13.0 — 2026-08-31
|
|
64
|
+
|
|
65
|
+
Three retirements, taken together on purpose. Each is a small migration, and three minors
|
|
66
|
+
each asking an app to move costs more than one that asks properly — so this is one crossing
|
|
67
|
+
and one `zt upgrade` run.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
bun zt upgrade --to 1.13.0
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Removed — BREAKING
|
|
74
|
+
|
|
75
|
+
- **Flow's `Component.client(…)`.** Use the `` this.$`…` `` tagged template.
|
|
76
|
+
|
|
77
|
+
This is a security fix wearing an ergonomics change's clothes, which is why it did not wait
|
|
78
|
+
for 2.0. `client()` took a **string** and queued it to be evaluated in the browser, so the
|
|
79
|
+
caller owned the escaping — and its own docblock had to warn _never interpolate unescaped
|
|
80
|
+
user input_. A method whose documentation has to tell you not to hold it that way is a
|
|
81
|
+
footgun with a label on. `$` is a tagged template, so every `${…}` is encoded as a JS
|
|
82
|
+
literal before it reaches the page.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// before — escaping was yours to remember
|
|
86
|
+
this.client(`toast(${JSON.stringify(this.search)})`);
|
|
87
|
+
|
|
88
|
+
// after — encoded for you
|
|
89
|
+
this.$`toast(${this.search})`;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The codemod rewrites a call whose argument is a single literal. **One whose argument is a
|
|
93
|
+
variable or a concatenation is reported rather than rewritten**: those are precisely the
|
|
94
|
+
ones the warning was about, and wrapping a finished string as `` $`${expr}` `` would encode
|
|
95
|
+
it as a string literal and stop running it as code. A codemod that quietly did that would
|
|
96
|
+
leave an app compiling, running, and no longer doing anything where it used to run a script.
|
|
97
|
+
|
|
98
|
+
Removing it also frees `client` as a property name on a component — the same benefit
|
|
99
|
+
removing `title` gave in 1.7.3.
|
|
100
|
+
|
|
101
|
+
### Changed — BREAKING
|
|
102
|
+
|
|
103
|
+
- **`LockDriver.extend()` is required.** Only affects a custom lock driver; all three
|
|
104
|
+
built-in drivers already implement it.
|
|
105
|
+
|
|
106
|
+
It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and that
|
|
107
|
+
fallback was correct only by coincidence. `acquire` happens to be an owner-guarded refresh
|
|
108
|
+
on every built-in driver, and nothing in the interface ever said it had to be — so a
|
|
109
|
+
third-party driver whose `acquire` takes a _free_ lock, which is the ordinary reading of
|
|
110
|
+
the word, would have had `refresh()` silently take a lock another holder owned. That is the
|
|
111
|
+
one thing a lock exists to prevent. Requiring the method turns an assumption the contract
|
|
112
|
+
never stated into something a driver has to answer.
|
|
113
|
+
|
|
114
|
+
- **`routes:types` and `serve --dev` are retired**, in favour of `route:types` and `dev`.
|
|
115
|
+
Both are rewritten by the codemod.
|
|
116
|
+
|
|
117
|
+
`serve --dev` **fails with a message** rather than being ignored, and the flag is still
|
|
118
|
+
declared for that reason alone. Flag parsing runs non-strict, so simply deleting it would
|
|
119
|
+
have left `serve --dev` starting a plain server — no watcher, no rebuild, no explanation. A
|
|
120
|
+
retired flag that silently changes what a command does is worse than one that is still
|
|
121
|
+
there.
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- **The `client-tagged-template` codemod**, which is what makes the first item above a
|
|
126
|
+
migration rather than a search.
|
|
127
|
+
|
|
30
128
|
## 1.12.0 — 2026-08-31
|
|
31
129
|
|
|
32
130
|
One change, deliberately alone: the minor exists to carry it.
|
package/docs/flow/index.md
CHANGED
|
@@ -198,7 +198,7 @@ The names in use:
|
|
|
198
198
|
| Rendering | `render` `layout` `placeholder` `slot` `hasSlot` `child` `isInteractive` |
|
|
199
199
|
| Actions & state | `bind` `validate` `resetValidation` `errors` `addError` `refresh` `$refresh` `$set` `cancelled` `signal` |
|
|
200
200
|
| Navigation | `redirect` `redirectRoute` `redirectIntended` `currentUrl` `navigateCurrent` |
|
|
201
|
-
| Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream`
|
|
201
|
+
| Events & realtime | `dispatch` `dispatchSelf` `dispatchTo` `stream` `$` |
|
|
202
202
|
| Misc | `flash` `download` `clearDurable` |
|
|
203
203
|
|
|
204
204
|
Anything beginning with `_` is also framework-internal, as are the statics `durable`
|
package/docs/queue.md
CHANGED
|
@@ -141,6 +141,37 @@ export class PruneDeletedContentJob extends Job {
|
|
|
141
141
|
JobRegistry.register(PruneDeletedContentJob as never);
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
+
### Renaming a job class
|
|
145
|
+
|
|
146
|
+
A queued job is a **persisted reference to a class**. The payload in the database or in Redis
|
|
147
|
+
carries a string, and the worker resolves the class by it — so a job enqueued yesterday is
|
|
148
|
+
looked up by today's process. That makes the class name a compatibility surface, and renaming
|
|
149
|
+
the class a silent data migration: jobs already in the queue under the old name stop resolving
|
|
150
|
+
at the moment you deploy.
|
|
151
|
+
|
|
152
|
+
It is invisible to a test suite, because a test enqueues and runs in the same process.
|
|
153
|
+
|
|
154
|
+
Declare the stored name and the class is free to move:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { Job } from "@zerotal/queue";
|
|
158
|
+
|
|
159
|
+
export class SendWelcomeEmail extends Job {
|
|
160
|
+
// The name in the queue payload. Keep it when the class is renamed.
|
|
161
|
+
static override jobName = "SendWelcomeEmail";
|
|
162
|
+
|
|
163
|
+
async handle(): Promise<void> {}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
It defaults to the class name, so a job that says nothing behaves exactly as before. Declare it
|
|
168
|
+
when you rename a job class, or ahead of time on jobs you expect to rename — it is the same
|
|
169
|
+
tool `Migration.id` is for a migration's filename, and the same reason.
|
|
170
|
+
|
|
171
|
+
**Drain before you rename, if you have not declared one.** A rename with jobs in flight and no
|
|
172
|
+
`jobName` leaves rows the worker reports as an unknown job class; `zt queue:failed` lists them
|
|
173
|
+
and `zt queue:retry` can replay them once the name matches again.
|
|
174
|
+
|
|
144
175
|
## Auto-registration
|
|
145
176
|
|
|
146
177
|
You don't import or wire up your jobs anywhere. Any job class placed under
|
package/docs/support-policy.md
CHANGED
|
@@ -75,11 +75,12 @@ dependency order, from CI. Never mix versions across packages.
|
|
|
75
75
|
tilde if you would rather cross a minor deliberately.
|
|
76
76
|
- **A break is never silent.** Every one is called out in the release notes as
|
|
77
77
|
**BREAKING**, with the reason and the migration steps, and the version gets its
|
|
78
|
-
own section in the Upgrade Guide.
|
|
78
|
+
own section in the Upgrade Guide. Seven have shipped so far — the
|
|
79
79
|
`ComponentWith` / `BaseModelWith` removal in 1.3.0, Flow's `socket:` listener
|
|
80
80
|
prefix in 1.7.2, the removal of Flow's `this.title(…)` in 1.7.3, SQLite
|
|
81
81
|
foreign-key enforcement in 1.11.0, `countTokens` returning `number | null` in
|
|
82
|
-
1.11.2,
|
|
82
|
+
1.11.2, the refusal to write a boolean into a text column in 1.12.0, and the removal of
|
|
83
|
+
Flow's `Component.client(…)` alongside two retired aliases in 1.13.0.
|
|
83
84
|
- **One of those five is in the wrong place, and it stays on the record.** 1.11.2
|
|
84
85
|
is a patch, and by the rule above a patch cannot carry a break. It did: the
|
|
85
86
|
`countTokens` signature changed in the same release that promoted `@zerotal/ai`
|
package/docs/upgrade.md
CHANGED
|
@@ -289,6 +289,64 @@ doing something quiet.
|
|
|
289
289
|
If it really is a new migration, give it a name that does not collide once the
|
|
290
290
|
leading digits are removed.
|
|
291
291
|
|
|
292
|
+
## 1.12 to 1.13
|
|
293
|
+
|
|
294
|
+
Three retirements in one crossing, deliberately together: each is a small migration, and
|
|
295
|
+
three minors each asking an app to move costs more than one that asks properly. `zt upgrade`
|
|
296
|
+
does the mechanical half.
|
|
297
|
+
|
|
298
|
+
```bash fragment
|
|
299
|
+
bun zt upgrade --to 1.13.0
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### `Component.client(…)` is removed — use the `$` tagged template
|
|
303
|
+
|
|
304
|
+
The reason this did not wait: `client()` took a **string** and queued it to be evaluated in
|
|
305
|
+
the browser, so the caller owned the escaping. Its own docblock had to say _never interpolate
|
|
306
|
+
unescaped user input_, which is a warning about a footgun rather than a design. `$` is a
|
|
307
|
+
tagged template, so every `${…}` is encoded as a JS literal before it reaches the page.
|
|
308
|
+
|
|
309
|
+
```ts fragment
|
|
310
|
+
// in a component class body — before
|
|
311
|
+
this.client(`$refs.titleInput.focus()`);
|
|
312
|
+
this.client(`toast(${JSON.stringify(this.search)})`); // escaping was yours to remember
|
|
313
|
+
|
|
314
|
+
// after
|
|
315
|
+
this.$`$refs.titleInput.focus()`;
|
|
316
|
+
this.$`toast(${this.search})`; // encoded for you
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The codemod rewrites a call whose argument is a single literal. **A call whose argument is a
|
|
320
|
+
variable or a concatenation is reported rather than rewritten**, because those are exactly the
|
|
321
|
+
ones the security note was about — and wrapping the finished string as `` $`${expr}` `` would
|
|
322
|
+
encode it as a string literal and stop running it as code. Read those and interpolate through
|
|
323
|
+
`$` instead.
|
|
324
|
+
|
|
325
|
+
Removing it also frees `client` as a property name on your components, the way removing
|
|
326
|
+
`title` did in 1.7.3.
|
|
327
|
+
|
|
328
|
+
### `LockDriver.extend()` is required
|
|
329
|
+
|
|
330
|
+
Only affects a **custom lock driver**; the three built-in ones already implement it.
|
|
331
|
+
|
|
332
|
+
It shipped optional in 1.5.0 with `acquire(key, owner, ttl)` as the fallback, and the fallback
|
|
333
|
+
was correct only by coincidence: `acquire` happens to be an owner-guarded refresh on every
|
|
334
|
+
built-in driver, and nothing in the interface said it had to be. A driver whose `acquire` takes
|
|
335
|
+
a _free_ lock — the ordinary reading of the word — would have had `refresh()` silently take a
|
|
336
|
+
lock another holder owned, which is the one thing a lock exists to prevent.
|
|
337
|
+
|
|
338
|
+
Implement `extend(key, owner, ttlSeconds)`: push the deadline out, return `false` when the key
|
|
339
|
+
is free or held by someone else.
|
|
340
|
+
|
|
341
|
+
### `routes:types` and `serve --dev` are retired
|
|
342
|
+
|
|
343
|
+
`route:types` and `dev` are the real names. The codemod rewrites both in scripts and CI config.
|
|
344
|
+
|
|
345
|
+
`serve --dev` **fails loudly** rather than being ignored. The flag is still declared for exactly
|
|
346
|
+
that reason: flag parsing is non-strict, so deleting it would have left `serve --dev` starting a
|
|
347
|
+
plain server with no watcher and no message — a retired flag that silently changes what a
|
|
348
|
+
command does.
|
|
349
|
+
|
|
292
350
|
## 1.11 to 1.12
|
|
293
351
|
|
|
294
352
|
One breaking change, and it is the intended kind: a minor, announced, with the reason.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/arch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@zerotal/core": "1.
|
|
38
|
+
"@zerotal/core": "1.13.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/orm": "1.
|
|
42
|
+
"@zerotal/orm": "1.13.1"
|
|
43
43
|
},
|
|
44
44
|
"description": "The Zerotal agent surface — an MCP server that hands coding agents the framework's machine-readable truth: exact API signatures, live routes and schema, version-matched docs, and `zt doctor`.",
|
|
45
45
|
"keywords": [
|