@akanjs/cli 3.0.0-beta.3 → 3.0.0-beta.5

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/.build-stamp CHANGED
@@ -1 +1 @@
1
- d492352c3c58e1fe064cb199f210489a9405247a400b36a8a688d51f36c67173
1
+ f9a390615e90ca7105c69229c1dc5ec435a0ef90eb86456f57e6f47146898c45
@@ -121,6 +121,12 @@ apps and libs never import it directly (`no-import-external-library`) — everyt
121
121
  reader produced**, not on the file that was picked: a reader that uploads and answers a `url` has already paid
122
122
  what the ceiling exists for, so refusing its 6 MB photo would refuse it for a cost it does not incur. Only the
123
123
  built-in path checks the source size, and there the file itself is the carrier.
124
+ - **An app may also build attachments itself and hand them to `session.send([{ role: "user", text, attachments }])`**,
125
+ which is why `MessageAttachment` is exported from `akanjs/ui` — the composer's path is typed by the `AttachReader`
126
+ callback, that one is not, and an object assembled with no nameable type is the one that gets a field wrong.
127
+ Nothing downstream trusts the result: an attachment whose `mimeType` is not a string is noted like any other
128
+ unreadable one, and its chip renders its name with no preview, because the host wrote the object and the declared
129
+ type is a claim rather than a check. A field that arrives wrong costs that attachment, never the turn.
124
130
  - **A `url` carrier is an address the *provider* fetches, so a reader that uploads has to know its storage is
125
131
  reachable.** This is the one way an attachment fails with nobody reporting it: the default storage backend
126
132
  serves a path only the app can resolve, the adaptor hands it over as written, and the model answers about a
@@ -464,6 +464,24 @@ Full contract — credential handshake, room revalidation, socket cleanup scopin
464
464
  - **A cleanup registered with `ws.on("disconnect" | "unsubscribe", fn)` is scoped to the call that registered it**
465
465
  — a room for a `pubsub` subscribe, the socket for a `message` handler. Cleanup that must happen either way
466
466
  registers for both.
467
+ - **A call's deadline is the endpoint's `{ timeout: <ms> }`, and there is nowhere else to put one.** It bounds
468
+ both ends: the `Timeout` middleware — registered by default, and standing aside for every endpoint that
469
+ declares none — rejects the call with `base.error.gatewayTimeout`, and the same value is serialized to the
470
+ client as that call's request budget. Declared nowhere, a call takes the client's own default of **30
471
+ seconds**, so an endpoint whose real work runs longer (provisioning, a firmware flow, an external
472
+ orchestration) has to declare one or it dies mid-flight with a transport error that does not say whether the
473
+ server ever received it. A caller overrides it per call with `fetch.x(…, { timeout })` — `false` waits as long
474
+ as the runtime will — and an app moves its own default with `fetch.instance.setTimeout(ms)`. An upload is
475
+ never bounded. **Losing the race does not cancel the work**: the handler runs to completion with nobody
476
+ holding its result, so a deadline is an answer to the caller, not an undo.
477
+ - **A cached answer is the endpoint's `{ cache: <ms> }`, and only a `query` taking no internal argument may have
478
+ one.** The `Cache` middleware is registered by default and stands aside for every endpoint that declares
479
+ nothing. Internal arguments are how a call learns who is asking (`.with(Self)`), so an endpoint that has them
480
+ answers per caller and one shared entry would be one caller's answer handed to the next — that endpoint and
481
+ every `mutation` are named in the log and left uncached. Guards run on every hit; what is cached is the
482
+ handler's result, so `resolveReturn` still masks fields and resolves relations per call; a cache backend that
483
+ is down is warned about and the call runs uncached. The default chain is `Logging → Timeout → Cache → handler`,
484
+ and there is no `Retry` middleware.
467
485
 
468
486
  ### Authorization Defaults
469
487
 
@@ -727,6 +745,7 @@ what every developer must know even when not building one.
727
745
  - Put display and predicate logic on the `Light<Model>` class rather than in a util module — see Module File Playbook.
728
746
  - Defaults are a literal for scalars and a thunk for anything constructed. Arrays are `field([T])`; optional is the postfix `.optional()`.
729
747
  - **`field.visual(T)` is a field the page renders and an agent never sees** — a blur placeholder, a rendered HTML body, a serialized geometry. It stays an ordinary stored `property` (persistence, search, forms and the page response untouched) and is stripped wherever a value is masked for an AI caller: every in-page-agent read and every MCP result, along with the MCP readable schema. Unlike `hidden`/`secret` it is cost, not secrecy — nothing is refused over one. Reach for it whenever a field is bulky and useless to a model; that is cheaper than every tool learning to avoid it.
748
+ - **A `field.hidden` / `field.secret` value reads `null` on the client, and the type still declares it.** `SignalContext.resolveReturn` skips both field types while it builds an endpoint's response, and hydration writes `null` over the key rather than leaving it absent — the first branch of the loop in `constant/getDefault.ts`, ahead of the field's own default. So the value is a deliberate `null` behind a type promising a `string`, every use of it typechecks, and the failure lands wherever it is finally dereferenced instead of where it was read. **Guard with `??` or `== null`**: `=== undefined`, a destructuring default and an optional parameter default all catch only a missing key and sail straight past this one. A projection (`pickById(id, { secret: true })`) widens the *server's* read, never the response. If a screen needs the value, the field is neither hidden nor secret; if it only needs to be cheap for a model rather than unseen, that is `field.visual`.
730
749
 
731
750
  ### Scalar & Field Type Reference
732
751
 
@@ -9,6 +9,7 @@ Use this whenever generating constants or scalar constants. It defines the curre
9
9
  - `field(Type).optional()` marks a nullable or optional business value when absence is valid.
10
10
  - `field.hidden(Type).optional()` is for internal state that should not be treated as a normal public field.
11
11
  - `field.secret(Type).optional()` is for sensitive values that should not be selected by default.
12
+ - Neither survives an endpoint response: both are stripped on the way out and hydration writes `null` over the key, while the generated type still declares the field — so a client holds a deliberate `null` behind a type that promises a value, and only `??` / `== null` catch it. A field a browser has to read is neither.
12
13
  - `field([Type])` defines arrays; provide empty array defaults when the app expects list operations.
13
14
 
14
15
  ## Current Akan Patterns
@@ -28,6 +28,55 @@ mutation is mounted under. `conventions` carries the invariants — this is the
28
28
  neither again, and cleanup that must happen either way registers for both. From a `message` handler it belongs to
29
29
  the socket and runs at close. A handler that throws is logged and never blocks the rest of the teardown.
30
30
 
31
+ ## Request Deadlines
32
+
33
+ - **`{ timeout: <ms> }` in an endpoint's signal option is the whole configuration surface**, and it reaches both
34
+ ends of the call. Server side, the `Timeout` middleware races the handler against it and answers
35
+ `base.error.gatewayTimeout` (504) once it is spent. Client side, `FetchSerializer` ships the value on the
36
+ serialized endpoint and `fetch.<endpoint>()` uses it as that request's `AbortSignal.timeout` budget, so the
37
+ browser does not abandon a call the server is still allowed to be working on.
38
+ - **The middleware is registered by default and has no default of its own.** An endpoint that declares no
39
+ `timeout` is passed straight through — a default here would put a deadline on every endpoint in the app that
40
+ nobody asked for, which is why the middleware used to be opt-in and, being opt-in, made the declared value
41
+ dead. It clears its timer in a `finally`: the race's loser is a pending timer per call otherwise, and with a
42
+ five-minute budget that is a five-minute hold on the event loop for a call that answered in a millisecond.
43
+ - **Without a declaration the client's own default applies — 30 seconds** (`DEFAULT_TIMEOUT_MS`,
44
+ `fetch/client/httpClient.ts`), and the server imposes nothing. Nothing else bounds a request: `Bun.serve` runs
45
+ with `idleTimeout: 0`, so a long call is only ever ended by one of these two.
46
+ - **Precedence is caller, then endpoint, then client default.** `fetch.x(…, { timeout: 5_000 })` overrides what
47
+ the endpoint declared; `{ timeout: false }` waits as long as the runtime will. An app moves its own default
48
+ with `fetch.instance.setTimeout(ms)` — `lib/useClient.ts` is generated, so the `timeout` option on
49
+ `FetchClient.build` belongs to the template that writes it, not to app code.
50
+ - **A file upload is never bounded**, on any of those paths: a large body on a slow uplink is a request that is
51
+ working, and the request body is the only thing the client can tell that from.
52
+ - **The deadline answers the caller; it does not undo the call.** `Promise.race` cannot cancel `next()`, so a
53
+ handler that timed out keeps running and keeps writing. Reach for a `timeout` to bound what a *caller* waits
54
+ for, not to bound an effect — an operation that must not half-happen needs its own idempotency or compensation.
55
+ - The failure is the same `base.error.gatewayTimeout` key whichever side gave up, so the user reads one sentence
56
+ in their own language either way. What differs is the status code — 504 from the server's middleware, 408 from
57
+ the client's own abort — and nothing downstream branches on that.
58
+
59
+ ## Cached Answers
60
+
61
+ - **`{ cache: <ms> }` in an endpoint's signal option is the whole cache surface.** The `Cache` middleware is
62
+ registered by default and stands aside unless an endpoint declares one — it used to be opt-in and to ignore the
63
+ declared value for a hard-coded 60 seconds, which is the same shape the `timeout` bug had.
64
+ - **Only a `query` that takes no internal argument may carry one.** The handler's inputs are its declared
65
+ arguments plus its internal ones, so an endpoint with none of the latter answers the same thing to everyone who
66
+ may read it — which is the only answer a shared entry can hold. An endpoint that takes `.with(Self)` answers
67
+ per caller, and one entry would be one caller's answer handed to the next; that endpoint and every `mutation`
68
+ are named once in the log and left uncached rather than silently ignored.
69
+ - **The guards run on every hit.** A cache hit skips `next()`, and `next()` is what runs them, so the middleware
70
+ calls `context.checkGuards()` itself before handing the entry over. Shared is not public.
71
+ - **The entry is the handler's result, not the response.** The middleware wraps execution, so `resolveReturn`
72
+ still runs per call on the cached value — field masking, `hidden`/`secret` stripping and relation resolution are
73
+ never cached, and a relation still costs its own load. The cache saves the handler, not the serialization.
74
+ - **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
75
+ uncached; a failed write is warned and dropped.
76
+ - The default middleware chain is `Logging → Timeout → Cache → handler`: a hung cache backend is bounded by the
77
+ same deadline as the handler it stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
78
+ whose failure it cannot classify replays whatever the first attempt already did.
79
+
31
80
  ## Binary Pubsub
32
81
 
33
82
  - **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/cli",
3
- "version": "3.0.0-beta.3",
3
+ "version": "3.0.0-beta.5",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -33,7 +33,7 @@
33
33
  "@langchain/openai": "^1.4.6",
34
34
  "@tailwindcss/node": "^4.3.0",
35
35
  "@trapezedev/project": "^7.1.4",
36
- "akanjs": "3.0.0-beta.3",
36
+ "akanjs": "3.0.0-beta.5",
37
37
  "chalk": "^5.6.2",
38
38
  "commander": "^14.0.3",
39
39
  "dayjs": "^1.11.20",