akanjs 2.4.1-rc.7 → 2.4.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,331 @@
|
|
|
1
1
|
# akanjs
|
|
2
2
|
|
|
3
|
+
## 2.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 473be34: fix(devkit): restart the backend for server code saved while the builder was away
|
|
8
|
+
|
|
9
|
+
Nothing watches the source tree between a builder leaving and its replacement being ready: the idle
|
|
10
|
+
suspend stops its own watcher before the wake's boot build, a recycle or crash takes the builder's
|
|
11
|
+
watcher with it, and a fresh builder primes its mtime index from whatever it finds on disk — so a save
|
|
12
|
+
that lands in that window is _baseline_ to it and produces no event anywhere. The client half of such a
|
|
13
|
+
save is rescued by the boot build reading the new file; the backend half was not. A `.service.ts` saved
|
|
14
|
+
in that window left the server running the code it replaced, with nothing on screen to say so.
|
|
15
|
+
|
|
16
|
+
The dev host now stamps `(mtime, size)` for every file in the backend import graph when the builder goes
|
|
17
|
+
away — at suspend, at a recycle request, at a crash exit — and compares them when a builder is ready
|
|
18
|
+
again, restarting the backend for anything that moved. Comparing stamps rather than waiting for events
|
|
19
|
+
also covers the watcher dropping one, which Bun's recursive `fs.watch` does.
|
|
20
|
+
|
|
21
|
+
A config change while suspended is exempt, because it replaces the backend along with the builder on its
|
|
22
|
+
own. Where the backend graph scan has never succeeded — path-role fallback rules — there is nothing to
|
|
23
|
+
stamp, and the host says so rather than staying quiet about it.
|
|
24
|
+
|
|
25
|
+
- f5bfa27: perf(devkit): stop parsing every source file to find barrel imports
|
|
26
|
+
|
|
27
|
+
`rewriteBarrelImports` ran a full TypeScript parse of every file it was given, to find import
|
|
28
|
+
statements it then discarded for all but the barrel ones. It runs on every source file of every dev
|
|
29
|
+
rebuild, which made it the single most expensive thing in one.
|
|
30
|
+
|
|
31
|
+
- **63% of files import no barrel at all.** A static import cannot name a specifier without that
|
|
32
|
+
specifier appearing literally in the source, so a substring test skips them before the parser is
|
|
33
|
+
involved — 4ms for 1189 files. The bundler plugin already did this privately; it now lives in
|
|
34
|
+
`rewriteBarrelImports` so the CSS and client-entry walks get it too.
|
|
35
|
+
- **`setParentNodes: true` was paid for no reader.** Nothing reads `node.parent`; every position comes
|
|
36
|
+
from `getStart(sourceFile)`, which takes the file explicitly.
|
|
37
|
+
|
|
38
|
+
Measured across 1189 files: 299ms and 161MB of RSS become 88ms and 5MB. End to end on `apps/akan`,
|
|
39
|
+
`CssCompiler.discoverCssAndSources` drops from 262ms to 190ms and the client-entry discovery walk from
|
|
40
|
+
242ms to 176ms, retaining 107MB instead of 128MB. Verified output-identical on 1535 files: same import
|
|
41
|
+
statements from the parser, and no file the pre-filter skips would have been rewritten.
|
|
42
|
+
|
|
43
|
+
`CssCompiler` also memoises import resolution for the life of one rebuild, where a miss cost up to 13
|
|
44
|
+
sequential `exists()` calls repeated per importer (a further 216ms → 190ms).
|
|
45
|
+
|
|
46
|
+
- 473be34: fix(devkit): bound the `ps` fallback that reads another process's memory
|
|
47
|
+
|
|
48
|
+
Where there is no `/proc` (macOS), the dev host reads a process's RSS by shelling out to `ps`, with no
|
|
49
|
+
timeout. An absent `ps` was already handled — it answers `null`, which callers read as "no new
|
|
50
|
+
information" — but a stuck one was not, and its only caller awaits it at the end of a 20s settle before
|
|
51
|
+
committing a builder recycle. A hang there meant the recycle silently never happened.
|
|
52
|
+
|
|
53
|
+
Now spawned directly with a 2s kill timer, which is the same treatment the dev-stability harness already
|
|
54
|
+
needed after `ps` hung under load.
|
|
55
|
+
|
|
56
|
+
- 068158b: feat(devkit): bound dev-server memory and stop losing watch events
|
|
57
|
+
|
|
58
|
+
Two dev-server problems that compounded each other.
|
|
59
|
+
|
|
60
|
+
- The builder grew without bound because `Bun.build` retains native bundler arenas that
|
|
61
|
+
`Bun.gc(true)` never reclaims. It is now recycled once its RSS passes a ceiling derived
|
|
62
|
+
from the container's cgroup limit, draining in flight work first.
|
|
63
|
+
- Bun's recursive `fs.watch` reports roughly one path per coalescing window and discards
|
|
64
|
+
the rest, so concurrent saves went unbuilt. Changes are now resolved against a
|
|
65
|
+
`SourceMtimeIndex` baseline and events only decide _when_ to look.
|
|
66
|
+
|
|
67
|
+
- 46a1a4a: fix(devkit): flush all builder ipc through BuilderChannel and isolate the boot build
|
|
68
|
+
|
|
69
|
+
`BuilderReply` only covered request responses, so recycle `process.exit` could still
|
|
70
|
+
drop unflushed events like `css-updated` and leave the backend on a stale bundle. The
|
|
71
|
+
boot `SsrBaseArtifactBuilder` also stayed in the long-lived watcher and retained most of
|
|
72
|
+
its idle RSS.
|
|
73
|
+
|
|
74
|
+
- Replace `BuilderReply` with `BuilderChannel` (`send` / `emit` / `drain`) so every
|
|
75
|
+
builder→host message awaits ipc flush before recycle exit.
|
|
76
|
+
- Run the boot base artifact build in the disposable `buildBatch` worker (`needs: ["base"]`)
|
|
77
|
+
and keep only the serializable artifact/fonts in the watcher.
|
|
78
|
+
- Split the idle resource-budget assertion so host+backend stays tight while the builder
|
|
79
|
+
can swing within its RSS recycle ceiling.
|
|
80
|
+
|
|
81
|
+
- 90c6597: fix(devkit): answer in-flight builder requests on recycle and flush replies before exit
|
|
82
|
+
|
|
83
|
+
Builder RSS recycle (and unexpected exits) left mid-flight `build-route` /
|
|
84
|
+
`build-csr` promises hanging: the host never tracked correlation ids, and even a
|
|
85
|
+
clean drain could lose a large `build-route-res` when `process.exit` truncated an
|
|
86
|
+
unflushed ipc write past the pipe buffer.
|
|
87
|
+
|
|
88
|
+
- Track in-flight ids on the host and fail them with a reloadable error when the
|
|
89
|
+
builder recycles, crashes, or is stopped.
|
|
90
|
+
- Send replies through `BuilderReply`, which awaits the ipc flush (with a timeout)
|
|
91
|
+
so recycle drain means "answered", not "truncated".
|
|
92
|
+
|
|
93
|
+
- aca901d: fix(devkit): keep builder replies matched to the backend that asked for them
|
|
94
|
+
|
|
95
|
+
`BuilderRpc` numbers its requests from 1 in each backend _process_, while the builder it
|
|
96
|
+
talks to outlives the backend. After a restart the two generations collided on id 1: the
|
|
97
|
+
builder answered the departed backend's request, the dev host relayed it, and the new
|
|
98
|
+
backend settled its own id 1 with another route's manifest delta — a page rendered against
|
|
99
|
+
client modules that were never built for it. The answer it was actually waiting for then
|
|
100
|
+
arrived to an empty pending map and was dropped, discarding the correct build too.
|
|
101
|
+
|
|
102
|
+
`BuilderRequestRouter` renumbers ids host-side, so neither the backend nor the builder
|
|
103
|
+
learns anything changed and a reply whose generation is gone is discarded rather than
|
|
104
|
+
misdelivered.
|
|
105
|
+
|
|
106
|
+
- 068158b: fix(cli): make `bun run akan <cmd>` concurrency-safe
|
|
107
|
+
|
|
108
|
+
`bun run akan` rebuilds the CLI into a shared `dist/` before every command, so two
|
|
109
|
+
commands started at once could read a half-written bundle.
|
|
110
|
+
|
|
111
|
+
- cb895b7: fix(devkit): find created directories on a filesystem with a coarse mtime clock
|
|
112
|
+
|
|
113
|
+
`SourceMtimeIndex` finds new files by noticing their directory's mtime moved, which Linux
|
|
114
|
+
stamps from a coarse clock: 400 back-to-back `mkdir`s left the parent's mtime unmoved 319
|
|
115
|
+
times on overlayfs and 324 times on ext4, smallest observable step 1ms. macOS APFS
|
|
116
|
+
(0.042ms) missed none, which is why this only ever showed up on Linux.
|
|
117
|
+
|
|
118
|
+
A directory mutated in the same millisecond as the recorded value — but after the walk that
|
|
119
|
+
recorded it — therefore left no trace, and because its files were never tracked, later edits
|
|
120
|
+
to them went unreported for the life of the process. Directories whose mtime was still fresh
|
|
121
|
+
when it was read are now re-walked on the next scan (`dirSettleMs`, 20ms default), and
|
|
122
|
+
`HmrWatcher` schedules one more scan while any remain unsettled.
|
|
123
|
+
|
|
124
|
+
- f8a9bc5: perf(devkit): cache tailwind candidate tokens across builds
|
|
125
|
+
|
|
126
|
+
The CSS rebuild read the full text of every source file on every save. Phase 2 moved css
|
|
127
|
+
compilation into a per-generation batch worker, so an in-memory cache is discarded before
|
|
128
|
+
the next save can use it — the cache goes to disk instead, the same way font subsetting
|
|
129
|
+
does, which also survives a builder recycle and a dev-host restart.
|
|
130
|
+
|
|
131
|
+
Measured on `apps/akan` (556 sources, 26800 candidates): the candidate scan drops from
|
|
132
|
+
58-60ms to 13-19ms. Note that this is a smaller share of the rebuild than expected — the
|
|
133
|
+
full CSS rebuild is ~380ms, so the scan was never the dominant cost. Nothing is written
|
|
134
|
+
when nothing was re-read.
|
|
135
|
+
|
|
136
|
+
- d973712: perf(cli): keep heavy dependencies out of the long-lived dev processes
|
|
137
|
+
|
|
138
|
+
`akan start` holds the CLI entry and the builder watcher for the whole dev session, so an
|
|
139
|
+
eagerly imported dependency is resident for the whole session too.
|
|
140
|
+
|
|
141
|
+
- The dev host reached `@inquirer/prompts` (~24MB) because `runCommands` shares a module
|
|
142
|
+
with the interactive argument fallbacks. Those now load the prompt stack on first use,
|
|
143
|
+
which for `akan start` is never.
|
|
144
|
+
- The builder watcher reached `tailwindcss` and `@tailwindcss/node` (~40MB) through the
|
|
145
|
+
`frontendBuild` barrel, which re-exports `cssCompiler` and `ssrBaseArtifactBuilder`. That
|
|
146
|
+
has been dead weight since css compilation moved into the batch worker; the watcher now
|
|
147
|
+
imports by module path.
|
|
148
|
+
|
|
149
|
+
`entryModuleGraph.test.ts` guards both by walking each built entry's chunk closure. The
|
|
150
|
+
previous check grepped the entry file alone, which cannot see a dependency reached through
|
|
151
|
+
a shared chunk and so reported both of these as absent.
|
|
152
|
+
|
|
153
|
+
- 068158b: fix(devkit): pin the dev port and bound the waits that could hang forever
|
|
154
|
+
|
|
155
|
+
- `AKAN_DEV_PORT` pins the dev port. It used to derive from an app's index in the `apps/`
|
|
156
|
+
listing, so adding an app moved a running dev server's port at its next restart.
|
|
157
|
+
- `BuilderRpc` created request promises with no timeout, and nothing else answers a lost
|
|
158
|
+
request. Since the builder is recycled routinely, a page request that landed mid
|
|
159
|
+
route-build left the SSR promise pending forever with nothing to retry. Now bounded by
|
|
160
|
+
`AKAN_BUILDER_RPC_TIMEOUT_MS` (120s default) with a message naming the likely cause.
|
|
161
|
+
|
|
162
|
+
- cc3dd40: feat: expose local-dev metadata endpoints for devtools visualization
|
|
163
|
+
|
|
164
|
+
Add four JSON endpoints, registered only when `AKAN_PUBLIC_ENV=local` (override with `AKAN_DEVTOOLS`),
|
|
165
|
+
that describe the running system for an external developer-tools UI:
|
|
166
|
+
|
|
167
|
+
- `GET /_akan/constant` — every model's Input/Object/Full/Light/Insight view, scalars, enums, filter
|
|
168
|
+
query/sort, and derived relation edges.
|
|
169
|
+
- `GET /_akan/signal` — declared and framework-generated endpoints, slices, internals, and a flattened
|
|
170
|
+
route table with fully resolved HTTP/WS paths.
|
|
171
|
+
- `GET /_akan/dictionary` — the merged i18n tree, module kinds, and flattened dotted keys (`?lang=` narrows it).
|
|
172
|
+
- `GET /_akan/deps` — the DI graph: services, adaptors, signals, uses, middleware, env, roles, and the
|
|
173
|
+
topological init stages.
|
|
174
|
+
|
|
175
|
+
They live in `AkanServer.#createBuiltinRoutes()` next to `/openapi.json`, so they stay off the `/api` prefix
|
|
176
|
+
and never enter the `serializedSignal` payload shipped to clients. Outside `local` the routes are not
|
|
177
|
+
registered at all and fall through to the SSR catch-all.
|
|
178
|
+
|
|
179
|
+
Supporting changes:
|
|
180
|
+
|
|
181
|
+
- `DictionaryRegistry` collects each `makeTrans` root, which was previously closure-private and unreachable
|
|
182
|
+
from the server.
|
|
183
|
+
- `DiLifecycle` gains a read-only `modules` accessor and retains disabled-module reasons that were only logged.
|
|
184
|
+
- `SignalResolver.getScheduleSkipReason` is now public so the reported schedule placement cannot drift from
|
|
185
|
+
the scheduler's own rules.
|
|
186
|
+
|
|
187
|
+
Secrets discipline: secret constant fields report name and type but no `default`/`example`, `env` carries
|
|
188
|
+
values for `AKAN_PUBLIC_*` only and every other key by name alone, and `uses` are reported as key plus class
|
|
189
|
+
name — never the instance. `env` inject keys are extracted by scanning the factory source, never by running it.
|
|
190
|
+
|
|
191
|
+
- 8a2b795: perf(devkit): stop retaining source text in client-entry discovery, and expire its misses
|
|
192
|
+
|
|
193
|
+
`GraphClientEntryDiscovery` is created once per builder process, so its caches live for the
|
|
194
|
+
whole dev session in the watcher.
|
|
195
|
+
|
|
196
|
+
- It kept the full text of every file the walk had touched plus a barrel-rewritten copy of
|
|
197
|
+
each, when the walk only ever asks two things of a file — is it a client entry, and what
|
|
198
|
+
does it import — both of which were already cached separately under the same key. Those
|
|
199
|
+
three caches collapse into one holding just the derived facts: measured on `apps/akan`,
|
|
200
|
+
retention after a full walk drops from 135-142MB to 125-127MB.
|
|
201
|
+
- `invalidate()` never cleared the file-existence and resolution caches. They are keyed by
|
|
202
|
+
extension-less path and by `dir\0specifier`, neither of which maps back to a file that was
|
|
203
|
+
just created, so a negative recorded before a module existed was permanent: adding a new
|
|
204
|
+
module and importing it left the import unresolved until the next config change or builder
|
|
205
|
+
recycle. Negative answers are now dropped on any invalidate; positive ones are keyed by a
|
|
206
|
+
real path and are left alone.
|
|
207
|
+
|
|
208
|
+
- 068158b: perf(devkit): cache font subsetting across dev-server boots
|
|
209
|
+
|
|
210
|
+
`FontOptimizer.optimize()` re-subset every font file on each builder boot even though it
|
|
211
|
+
already computed a config hash and wrote hashed outputs. It now skips the
|
|
212
|
+
`fonteditor-core` / `subset-font` work when the expected outputs are present, which also
|
|
213
|
+
keeps those two packages out of the common path entirely.
|
|
214
|
+
|
|
215
|
+
- 473be34: fix(devkit): hold page requests during the recycle drain, not only after the builder exits
|
|
216
|
+
|
|
217
|
+
A builder asked to recycle drains first — it stays alive finishing its queued work and refuses
|
|
218
|
+
everything new — and throughout that window the dev host still reported it as `ready`. So a route or CSR
|
|
219
|
+
request that arrived during the drain was sent, refused by the departing builder, and relayed to the
|
|
220
|
+
backend as a failure: the same dev error page the request-holding fix was written to remove, in the half
|
|
221
|
+
of the window it never covered. The hold was unreachable there, because it only runs when the send
|
|
222
|
+
itself fails.
|
|
223
|
+
|
|
224
|
+
The builder host now reports `recycling` for the drain, which `send()` refuses on and which the hold
|
|
225
|
+
decision treats like a restart. `ready` — the field `onExit` reads to tell a planned exit from a builder
|
|
226
|
+
that never came up — is deliberately unchanged.
|
|
227
|
+
|
|
228
|
+
- e5fde3b: fix(devkit): hold page requests while the builder restarts instead of failing them
|
|
229
|
+
|
|
230
|
+
A route or CSR request that arrived while the builder was recycling or restarting was answered
|
|
231
|
+
immediately with `builder is restarting; reload after the builder is ready`. Nothing retried, so the
|
|
232
|
+
browser tab showed an error for a builder that was seconds from being back — and the builder is recycled
|
|
233
|
+
routinely, whenever its RSS passes the ceiling.
|
|
234
|
+
|
|
235
|
+
Those requests are now held and replayed when the builder reports ready, which is what the idle-suspend
|
|
236
|
+
path already did for exactly this reason. `BuilderRpc`'s own timeout still bounds the wait, the queue is
|
|
237
|
+
capped so a builder that never returns cannot grow it, and anything still held when the builder is
|
|
238
|
+
stopped for good is failed rather than left silent.
|
|
239
|
+
|
|
240
|
+
- 473be34: fix(devkit): say when a build worker was killed rather than crashed
|
|
241
|
+
|
|
242
|
+
The disposable build worker holds the largest transient in the dev tree (~548MB on a mid-size app, over
|
|
243
|
+
1GB on a large one), so on a small sandbox it is the process the kernel reaches for first. A worker the
|
|
244
|
+
OOM killer takes exits with code `null` and `SIGKILL`, and the build was reported as `build worker exited
|
|
245
|
+
with code null before reporting a result` — indistinguishable from an ordinary crash, though the two have
|
|
246
|
+
opposite fixes: find the build error, or raise the memory limit.
|
|
247
|
+
|
|
248
|
+
The failure path is unchanged and still safe (that generation goes red, the last-good artifact keeps
|
|
249
|
+
serving); the message now names the signal, and calls out `SIGKILL` as most often the OOM killer.
|
|
250
|
+
|
|
251
|
+
- f28466f: fix(server): stop closing the database out from under its own schema setup
|
|
252
|
+
|
|
253
|
+
`getStore()` returns a store synchronously while `ensure()` goes on creating tables and indexes, so a
|
|
254
|
+
shutdown could close the connection mid-setup. The rejection was unhandled — `void store.ensure()` —
|
|
255
|
+
and surfaced as `RangeError: Cannot use a closed database` blamed on whatever ran next, which read as a
|
|
256
|
+
flaky test rather than a race at shutdown. Reproduced at 3 failures in 8 runs of the akanjs suite, 0 in
|
|
257
|
+
10 after the fix.
|
|
258
|
+
|
|
259
|
+
All three SQL adaptors (bun:sqlite, libsql, Postgres) now track those setups and let them finish before
|
|
260
|
+
closing. Every statement `ensure()` runs is `IF NOT EXISTS`, so one cut short is simply redone next boot.
|
|
261
|
+
|
|
262
|
+
- 51851fa: perf: cut idle/dev-save memory with phase-1 quick wins
|
|
263
|
+
|
|
264
|
+
Apply the phase-1 resource plan without architecture changes:
|
|
265
|
+
|
|
266
|
+
- Self-arming CSR rebuild — skip the dead CSR artifact until `/__csr` or `?csr=true` first needs it
|
|
267
|
+
(keeps mobile live-reload working once armed).
|
|
268
|
+
- Bound RSC worker reload accumulation with threshold/RSS recycle instead of retaining every pages
|
|
269
|
+
bundle generation.
|
|
270
|
+
- Split `@akanjs/devkit` into subpath exports and move route/overrides AST validation out of the
|
|
271
|
+
resident `executors` graph so `typescript` is not pulled into long-lived start processes.
|
|
272
|
+
- Lazy-load CLI command modules via a command manifest so unused command graphs stay cold.
|
|
273
|
+
|
|
274
|
+
Also await async endpoint guards (including in parallel) so `canPass` promises are honored.
|
|
275
|
+
|
|
276
|
+
- 1c3436f: perf: bound builder memory with RSS recycle and disposable batch workers
|
|
277
|
+
|
|
278
|
+
Apply the phase-2 bounded-builder plan so Bun.build retention no longer grows without
|
|
279
|
+
bound across a long `akan start` session:
|
|
280
|
+
|
|
281
|
+
- Report builder RSS after each work item and recycle the builder process when it crosses
|
|
282
|
+
a ceiling (`AKAN_BUILDER_MAX_RSS_MB`, else cgroup × 0.35, else 1200 MB), only when no
|
|
283
|
+
build is in flight and the generation is green.
|
|
284
|
+
- Extract shared `memoryLimit` helpers (also used by the RSC worker) and announce recovered
|
|
285
|
+
pages/css state after a recycle-triggered boot so a live backend picks up the new
|
|
286
|
+
`base-artifact.json`.
|
|
287
|
+
- Move pages/css/csr `Bun.build` work into a disposable `buildBatch` worker that exits per
|
|
288
|
+
generation (optional `AKAN_BUILD_WORKER_REUSE_COUNT`), keeping the watcher process thin.
|
|
289
|
+
|
|
290
|
+
- 128e9a3: fix(devkit): survive a container image with no `ps`
|
|
291
|
+
|
|
292
|
+
`DevStabilityHarness` shells out to `ps` to find leftover dev processes, and slim images such as
|
|
293
|
+
`oven/bun` ship without procps, so the spawn threw. It now returns the same `null` it already
|
|
294
|
+
returns for a `ps` that does not answer in time — "could not look", not "nothing is running".
|
|
295
|
+
Fixture liveness never depended on it (`process.kill(pid, 0)`), so sweeping still works.
|
|
296
|
+
|
|
297
|
+
- a5d4a8a: fix: register and correctly invoke `internal(... { process })` queue workers
|
|
298
|
+
|
|
299
|
+
`process` internals accepted jobs but never ran them. Three defects:
|
|
300
|
+
|
|
301
|
+
- `buildInternal.process` was the only scheduled factory that did not default `enabled: true`, so
|
|
302
|
+
`SignalResolver.resolveSchedule` skipped it and no worker was ever registered. Placement is now governed by
|
|
303
|
+
`serverMode`/`operationMode` alone, matching the existing `serverMode: "all"` default.
|
|
304
|
+
- Registered workers were called with the `AkanJob` rather than the declared `msg` arguments. The job payload is
|
|
305
|
+
now spread onto the declared args and deserialized against their declared types, so the `exec` signature
|
|
306
|
+
`(...msgArgs, job)` holds at runtime.
|
|
307
|
+
- `BullQueue` scoped its worker to queue `<prefix>:<key>` while enqueueing onto queue `<prefix>`, so cluster mode
|
|
308
|
+
never consumed jobs. Producer and consumer now share one queue per process key.
|
|
309
|
+
|
|
310
|
+
`resolveSchedule` also logs when a `process` internal gets no worker on the current server, since the producer is
|
|
311
|
+
installed regardless of placement.
|
|
312
|
+
|
|
313
|
+
- 473be34: fix(devkit): stop turning the builder's memory ceiling off on the first page load
|
|
314
|
+
|
|
315
|
+
The dev host stops enforcing the builder's RSS ceiling when recycling evidently cannot meet it. The
|
|
316
|
+
evidence it used was "two over-ceiling reports within 30s of a recycle" — but a builder reports after
|
|
317
|
+
every build, and one page load builds a route per navigation. On a container-derived ceiling (a 1.2GB
|
|
318
|
+
sandbox gives the builder ~420MB, against ~247MB per route build) the first page load after a recycle
|
|
319
|
+
switched the ceiling off for the rest of the session, leaving the builder unbounded on exactly the
|
|
320
|
+
deployment shape the ceiling exists for.
|
|
321
|
+
|
|
322
|
+
It now measures what it always claimed to: when a replacement builder becomes ready, before it has built
|
|
323
|
+
anything on demand, the host reads its RSS from the OS. That is the floor every future replacement lands
|
|
324
|
+
on, so a floor already over the ceiling means recycling cannot help — and only that stops enforcement,
|
|
325
|
+
with the message naming `AKAN_BUILDER_MAX_RSS_MB`. Otherwise the ceiling stands and the existing 30s
|
|
326
|
+
minimum interval bounds what it costs, now with a one-off warning when the builder keeps crossing back
|
|
327
|
+
inside that interval.
|
|
328
|
+
|
|
3
329
|
## 2.4.0
|
|
4
330
|
|
|
5
331
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1586,6 +1586,41 @@ export class SqlDocumentStore {
|
|
|
1586
1586
|
throw new Error(`Invalid database identifier: ${refName}`);
|
|
1587
1587
|
}
|
|
1588
1588
|
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* The schema setups `getStore` starts but nobody awaits.
|
|
1592
|
+
*
|
|
1593
|
+
* `getStore` returns a store synchronously while `ensure()` goes on creating tables and indexes, so
|
|
1594
|
+
* the connection could be closed out from under one — a server shutting down, or a test tearing its
|
|
1595
|
+
* fixture down. That surfaced as an unhandled `Cannot use a closed database` blamed on whatever ran
|
|
1596
|
+
* next, which made it look like a flaky test rather than a race at shutdown.
|
|
1597
|
+
*
|
|
1598
|
+
* Every statement `ensure()` runs is `IF NOT EXISTS`, so one cut short is simply redone on the next
|
|
1599
|
+
* boot; a failure *before* the close is still a real problem and still surfaces.
|
|
1600
|
+
*/
|
|
1601
|
+
class PendingStoreEnsures {
|
|
1602
|
+
readonly #pending = new Set<Promise<void>>();
|
|
1603
|
+
#closed = false;
|
|
1604
|
+
|
|
1605
|
+
track(ensure: Promise<void>): void {
|
|
1606
|
+
const tracked = ensure
|
|
1607
|
+
.catch((error: unknown) => {
|
|
1608
|
+
if (this.#closed) return;
|
|
1609
|
+
throw error;
|
|
1610
|
+
})
|
|
1611
|
+
.finally(() => {
|
|
1612
|
+
this.#pending.delete(tracked);
|
|
1613
|
+
});
|
|
1614
|
+
this.#pending.add(tracked);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
/** Let them finish against a live connection. Call before closing it. */
|
|
1618
|
+
async settle(): Promise<void> {
|
|
1619
|
+
this.#closed = true;
|
|
1620
|
+
await Promise.allSettled([...this.#pending]);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1589
1624
|
export class SqliteDatabase
|
|
1590
1625
|
extends adapt("sqliteDatabase", ({ env }) => ({
|
|
1591
1626
|
config: env((env: SqliteEnv) => {
|
|
@@ -1617,6 +1652,7 @@ export class SqliteDatabase
|
|
|
1617
1652
|
#client!: BunSqliteClient;
|
|
1618
1653
|
#stores = new Map<string, SqlDocumentStore>();
|
|
1619
1654
|
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1655
|
+
#ensures = new PendingStoreEnsures();
|
|
1620
1656
|
|
|
1621
1657
|
override async onInit() {
|
|
1622
1658
|
await mkdir(path.dirname(this.config.filePath), { recursive: true });
|
|
@@ -1634,6 +1670,7 @@ export class SqliteDatabase
|
|
|
1634
1670
|
}
|
|
1635
1671
|
|
|
1636
1672
|
override async onDestroy() {
|
|
1673
|
+
await this.#ensures.settle();
|
|
1637
1674
|
this.#db?.run("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
1638
1675
|
await this.#client?.close();
|
|
1639
1676
|
}
|
|
@@ -1647,7 +1684,7 @@ export class SqliteDatabase
|
|
|
1647
1684
|
if (existing) return existing;
|
|
1648
1685
|
const store = new SqlDocumentStore(this, constant, database, schema as DocumentSchema);
|
|
1649
1686
|
this.#stores.set(database.refName, store);
|
|
1650
|
-
|
|
1687
|
+
this.#ensures.track(store.ensure());
|
|
1651
1688
|
return store;
|
|
1652
1689
|
}
|
|
1653
1690
|
|
|
@@ -1724,6 +1761,7 @@ export class LibsqlDatabase
|
|
|
1724
1761
|
#client!: LibsqlAkanClient;
|
|
1725
1762
|
#stores = new Map<string, SqlDocumentStore>();
|
|
1726
1763
|
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1764
|
+
#ensures = new PendingStoreEnsures();
|
|
1727
1765
|
|
|
1728
1766
|
override async onInit() {
|
|
1729
1767
|
const url = this.config.url ?? "file:local.db";
|
|
@@ -1736,6 +1774,7 @@ export class LibsqlDatabase
|
|
|
1736
1774
|
}
|
|
1737
1775
|
|
|
1738
1776
|
override async onDestroy() {
|
|
1777
|
+
await this.#ensures.settle();
|
|
1739
1778
|
await this.#client?.close();
|
|
1740
1779
|
}
|
|
1741
1780
|
|
|
@@ -1748,7 +1787,7 @@ export class LibsqlDatabase
|
|
|
1748
1787
|
if (existing) return existing;
|
|
1749
1788
|
const store = new SqlDocumentStore(this, constant, database, schema as DocumentSchema);
|
|
1750
1789
|
this.#stores.set(database.refName, store);
|
|
1751
|
-
|
|
1790
|
+
this.#ensures.track(store.ensure());
|
|
1752
1791
|
return store;
|
|
1753
1792
|
}
|
|
1754
1793
|
|
|
@@ -1808,6 +1847,7 @@ export class PostgresDatabase
|
|
|
1808
1847
|
#client!: PostgresAkanClient;
|
|
1809
1848
|
#stores = new Map<string, SqlDocumentStore>();
|
|
1810
1849
|
#transaction = new AsyncLocalStorage<TransactionContext>();
|
|
1850
|
+
#ensures = new PendingStoreEnsures();
|
|
1811
1851
|
|
|
1812
1852
|
override async onInit() {
|
|
1813
1853
|
const { default: postgres } = await import("postgres");
|
|
@@ -1827,6 +1867,7 @@ export class PostgresDatabase
|
|
|
1827
1867
|
}
|
|
1828
1868
|
|
|
1829
1869
|
override async onDestroy() {
|
|
1870
|
+
await this.#ensures.settle();
|
|
1830
1871
|
await this.#client?.close();
|
|
1831
1872
|
}
|
|
1832
1873
|
|
|
@@ -1839,7 +1880,7 @@ export class PostgresDatabase
|
|
|
1839
1880
|
if (existing) return existing;
|
|
1840
1881
|
const store = new SqlDocumentStore(this, constant, database, schema as DocumentSchema, new PostgresDialect());
|
|
1841
1882
|
this.#stores.set(database.refName, store);
|
|
1842
|
-
|
|
1883
|
+
this.#ensures.track(store.ensure());
|
|
1843
1884
|
return store;
|
|
1844
1885
|
}
|
|
1845
1886
|
|