@zerotal/arch 1.13.0 → 1.13.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/docs/changelog.md CHANGED
@@ -27,6 +27,83 @@ 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.2 — 2026-08-31
31
+
32
+ From a production field report at 1.12.0 — an Inertia + React app on SQLite, 117 routes,
33
+ 1256 tests, live behind Caddy. Six items, of which one was still open. The other five had
34
+ been closed between 1.10.0 and 1.13.0 and are listed at the end, because a report that
35
+ carries items forward is worth answering precisely rather than generally.
36
+
37
+ ### Fixed
38
+
39
+ - **`Inertia.stream()` honours `X-Inertia`.** It answered every request with `text/html`,
40
+ including the XHR a running Inertia client sends. So the obvious way to adopt server
41
+ rendering — point a route at `stream` — broke client-side navigation _to_ that route.
42
+
43
+ The shape of the failure is the reason it survived: the first load looks perfect, which
44
+ is what a person checks. The second click does nothing. It fails only for somebody
45
+ already in the app, silently, and only once the route otherwise works.
46
+
47
+ `render` and `stream` now share the branch that writes the page object rather than one
48
+ of them having it, which is precisely how they came apart. An app that wrote a header
49
+ check in front of the call can delete it; it does no harm either way.
50
+
51
+ ### Verified, not changed
52
+
53
+ Five of the six items were already closed. Each was re-checked against this release rather
54
+ than taken on trust, because the report carried them forward from 1.9.0:
55
+
56
+ - **STARTTLS on port 587** — fixed in 1.11.0, with `SmtpStartTls.test.ts` covering it. The
57
+ cause was that a write issued before the handshake completes is dropped.
58
+ - **`intended_url` across `Auth.attempt()`** — fixed in 1.11.0. `attempt` delegates to
59
+ `login`, `regenerate()` deliberately carries the data bag, and only privilege markers are
60
+ swept. `intendedFlow.test.ts` runs the three steps in order against a real session,
61
+ including the ID rotation.
62
+ - **`bun install --ignore-scripts`** — documented in the
63
+ [deployment guide](/docs/deployment).
64
+ - **A default test timeout** — `zt test` has passed `--timeout=30000` for some time, and
65
+ [the testing guide](/docs/testing#bun-test-vs-bun-zt-test) already names the two
66
+ alternatives that do not work: `bunfig.toml`'s `[test] timeout`, and
67
+ `setDefaultTimeout()` in a preload.
68
+ - **`postForm` refusing a `File`** — it has thrown, naming `multipart()`, since 1.0.2.
69
+
70
+ The report's other upload concern — that `http.file()` consumes the multipart stream, so a
71
+ later `body()` reads empty — **does not reproduce**: `_parseFormData()` caches, and reading
72
+ the file first is safe. A test now pins that, since nothing had covered that order.
73
+
74
+ ## 1.13.1 — 2026-08-31
75
+
76
+ One addition, found by sizing a job rather than doing it.
77
+
78
+ The 2.0 ledger carries an entry to prefix every `@internal` export with `_` — 270 symbols,
79
+ filed as mechanical. Three of them are `Job` classes, and a job class name is not a source
80
+ symbol: `JobRegistry` keys on it and that string is written into the persisted queue payload,
81
+ so a job enqueued yesterday is resolved by today's process. Renaming one invalidates every job
82
+ already in the queue, at deploy time rather than at change time, and no test sees it because a
83
+ test enqueues and runs in the same process.
84
+
85
+ The rename is re-scoped. The hazard it exposed is fixed here.
86
+
87
+ ### Added
88
+
89
+ - **`Job.jobName`** — a declared name for the queue payload, so renaming a job class is free:
90
+
91
+ ```ts
92
+ export class SendWelcomeEmail extends Job {
93
+ static override jobName = "SendWelcomeEmail"; // survives a class rename
94
+ async handle(): Promise<void> {}
95
+ }
96
+ ```
97
+
98
+ It defaults to the class name, so a job that declares nothing behaves exactly as before.
99
+ This is the same tool [`Migration.id`](/docs/upgrade#1-10-to-1-11) is for a migration's
100
+ filename, shipped in 1.11.0 for the same reason: an identity the framework derived from a
101
+ name someone was free to change, with no way to say otherwise.
102
+
103
+ It also decouples the queue from a build that mangles names. `zt compile` does not minify
104
+ today, so that is not a live hazard — but nothing in the registry said it depended on that,
105
+ and an assumption worth relying on is worth writing down.
106
+
30
107
  ## 1.13.0 — 2026-08-31
31
108
 
32
109
  Three retirements, taken together on purpose. Each is a small migration, and three minors
@@ -89,12 +89,23 @@ export class PostController {
89
89
  | Response body | Fully buffered string | Streaming `ReadableStream` |
90
90
  | TTFB | Immediate | After the shell is ready |
91
91
  | Page `<Head>` | Client only | Collected into the served `<head>` |
92
- | XHR navigation | JSON (the normal path) | N/Aonly the first-page document |
92
+ | XHR navigation | JSON (the normal path) | JSON — the same page object |
93
93
 
94
- For XHR navigations (`X-Inertia: true`), keep using `inertia()` — streaming only
95
- benefits the initial HTML document load.
94
+ **Both implement the whole protocol.** An `X-Inertia: true` request gets the page
95
+ object as JSON from either one; the streaming half applies to the first arrival,
96
+ which is the only load that renders a document.
97
+
98
+ That means a route can be moved to `inertiaStream()` for its cold load without
99
+ anything else changing. Until 1.13.2 it could not: `inertiaStream()` answered every
100
+ request with `text/html`, including the XHR, so pointing a route at it broke
101
+ client-side navigation _to_ that route. The first load looked perfect — which is what
102
+ a person checks — and the second click did nothing, for somebody already in the app.
103
+ Apps that hit this wrote a header check in front of the call; that workaround still
104
+ works and is no longer needed.
96
105
 
97
106
  > **Tip** — Stream the heaviest landing pages and leave everything else on `inertia()`.
107
+ > Streaming costs TTFB on a page that is mostly shell, so it is a choice per route
108
+ > rather than a global default.
98
109
 
99
110
  ## Page metadata: `<Head>` on the server
100
111
 
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerotal/arch",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
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.13.0"
38
+ "@zerotal/core": "1.13.2"
39
39
  },
40
40
  "devDependencies": {
41
41
  "typescript": "^5.8.0",
42
- "@zerotal/orm": "1.13.0"
42
+ "@zerotal/orm": "1.13.2"
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": [