@akanjs/cli 3.1.0-rc.3 → 3.1.0-rc.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 +1 -1
- package/{CodeAgent-ctk4fh2b.js → CodeAgent-rkjyxgec.js} +2 -2
- package/{CodeTui-mrxtrdzw.js → CodeTui-h7pyj6xa.js} +1 -1
- package/TunnelShare-661cxz5e.js +14 -0
- package/{agent.command-ncre6kwz.js → agent.command-3qxf4xcj.js} +5 -5
- package/akanCode.js +2 -2
- package/{application.command-54mawreg.js → application.command-m1kcd7jf.js} +12 -11
- package/{applicationBuildRunner-8573zr2w.js → applicationBuildRunner-dj0xkrep.js} +16 -9
- package/buildBatch.proc.js +7 -7
- package/{cloud.command-nyemv0m3.js → cloud.command-3jzym8q7.js} +17 -16
- package/cloud.script-r4nskbc6.js +24 -0
- package/{code.command-spsrzg63.js → code.command-0ad6gw2a.js} +6 -6
- package/{context.command-768zv7z4.js → context.command-0qx447mb.js} +17 -16
- package/context.runner-1vsjztjg.js +25 -0
- package/{dependencyScanner-06z2dmrr.js → dependencyScanner-3afj2vt1.js} +1 -1
- package/{guideline.command-4ycwc51q.js → guideline.command-cv5bmrx2.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +450 -1084
- package/guidelines/cssRule/cssRule.instruction.md +16 -1
- package/guidelines/fieldRule/fieldRule.instruction.md +57 -0
- package/guidelines/framework/framework.instruction.md +4 -7
- package/guidelines/mcpRule/mcpRule.instruction.md +11 -8
- package/guidelines/modelConstant/modelConstant.instruction.md +2 -1
- package/guidelines/modelStore/modelStore.instruction.md +9 -0
- package/guidelines/pageRule/pageRule.instruction.md +80 -0
- package/guidelines/queryRule/queryRule.instruction.md +35 -8
- package/guidelines/runtimeRule/runtimeRule.instruction.md +89 -2
- package/guidelines/ssrRule/ssrRule.instruction.md +13 -0
- package/guidelines/transportRule/transportRule.instruction.md +76 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +4 -4
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +49 -4
- package/incrementalBuilder.proc.js +4 -4
- package/{index-ec080x99.js → index-192ha4xh.js} +356 -307
- package/{index-c4h5b5nq.js → index-1p98exm2.js} +2 -2
- package/{index-xf40a295.js → index-1xj2t7cb.js} +7 -7
- package/{index-3675mvjw.js → index-20bxxyvn.js} +80 -80
- package/{index-c9zjb3tw.js → index-35hc95h5.js} +1 -1
- package/{index-w9b71b6y.js → index-4mhy6qme.js} +4 -4
- package/{index-vhfjzda3.js → index-5edzv8fn.js} +5 -5
- package/{index-k9rrten6.js → index-5nd1wtkv.js} +92 -94
- package/index-62md4dwy.js +260 -0
- package/{index-2z3kykvd.js → index-665zp9rs.js} +38 -15
- package/{index-qvghfv6v.js → index-6xn2n442.js} +4 -4
- package/{index-00seamy5.js → index-86jt2a0e.js} +1 -1
- package/{index-xm5tmpv0.js → index-8fp3b47h.js} +419 -418
- package/{index-91rt5bye.js → index-9z426msb.js} +17 -12
- package/{index-fjqvd5mx.js → index-awbdfcf2.js} +4 -4
- package/{index-3hx1z96c.js → index-bqbf654f.js} +1 -1
- package/{index-wjqbc7sf.js → index-cpb3nvma.js} +57 -58
- package/{index-1a7nhq56.js → index-d47ypebv.js} +17 -15
- package/{index-r5d6rq19.js → index-d72hfmdm.js} +13 -13
- package/{index-1nyxvz8j.js → index-fmzcs8v6.js} +4 -4
- package/{index-wanhh011.js → index-fyq1z6kt.js} +29 -27
- package/{index-qf8g88j9.js → index-g1mbppa9.js} +238 -236
- package/{index-jj7yapwq.js → index-h47cf1gg.js} +7 -7
- package/{index-q5abh0jq.js → index-j4mkhz93.js} +3 -3
- package/{index-3x7yeecy.js → index-jw1zm273.js} +28 -30
- package/{index-q958tqqg.js → index-m9jj544v.js} +1 -1
- package/{index-8935h3n1.js → index-mdjc93e1.js} +1 -1
- package/{index-dmsvd2rv.js → index-pydbnfez.js} +2 -2
- package/{index-3ddt3qjg.js → index-qwa5k1j8.js} +1 -1
- package/{index-8eqkxwsx.js → index-tamvyvj3.js} +3 -3
- package/{index-wdze51wz.js → index-trpasv9q.js} +260 -259
- package/{index-2ahj896a.js → index-tvehn2sp.js} +1 -1
- package/{index-bv210r6t.js → index-va06nr10.js} +1086 -1074
- package/{index-kmamh5bv.js → index-vk0pa0mn.js} +20 -21
- package/{index-fvn6f5wx.js → index-w44cy4vs.js} +2216 -2216
- package/index.js +22 -22
- package/{library.command-88ahgych.js → library.command-erp2f56n.js} +3 -3
- package/{localRegistry.command-ykea1m7b.js → localRegistry.command-xxpqgxjw.js} +15 -14
- package/{module.command-zky4b7m9.js → module.command-gs2r615e.js} +5 -5
- package/{package.command-vh8pcedh.js → package.command-7343f74r.js} +4 -4
- package/package.json +2 -2
- package/{page.command-fyqeqxc3.js → page.command-4h4a8pnh.js} +3 -3
- package/{pipeline.command-r5t30py6.js → pipeline.command-a2nrk26m.js} +3 -3
- package/{primitive.command-a0516zfk.js → primitive.command-w6t0asqz.js} +6 -6
- package/{projectTypecheck-7djd4z50.js → projectTypecheck-f534enqj.js} +1 -1
- package/{quality.command-k7jcnfn0.js → quality.command-77x7vbbx.js} +4 -4
- package/{repair.command-85zg2an3.js → repair.command-y6sstev7.js} +5 -5
- package/{scalar.command-g5vmqwca.js → scalar.command-20b8cy0k.js} +4 -4
- package/{subspace.command-mp4evgsj.js → subspace.command-92xkc0s0.js} +11 -10
- package/templates/lib/cnst.ts +0 -1
- package/templates/workspaceRoot/.gitignore.template +24 -53
- package/{tunnel.command-kbnr99sj.js → tunnel.command-2kw8vt9g.js} +8 -7
- package/{workflow.command-9y9p1n8y.js → workflow.command-2djwmpqf.js} +10 -10
- package/{workspace.command-jmec2kjg.js → workspace.command-5se6spqb.js} +28 -27
- package/TunnelShare-rj74jd6x.js +0 -13
- package/cloud.script-ht6yb203.js +0 -23
- package/context.runner-tf1b53qq.js +0 -24
- package/index-n68jx79w.js +0 -120
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
# Transport Rule — Guards, Sockets,
|
|
1
|
+
# Transport Rule — Guards, Sockets, Deadlines, Rate Limits, Server-To-Server Calls, Bytes, HTTP Verbs
|
|
2
2
|
|
|
3
3
|
How a call reaches a signal: what a guard may read on either transport, how a websocket carries its credential
|
|
4
|
-
and its per-connection state,
|
|
5
|
-
|
|
4
|
+
and its per-connection state, how long a call may take and what it may cache or count, how one server calls
|
|
5
|
+
another, which body types and bytes cross the wire, when a pubsub payload leaves JSON for a binary frame, and which
|
|
6
|
+
HTTP verb a mutation is mounted under. `conventions` carries the invariants — this is the full contract behind them.
|
|
6
7
|
|
|
7
8
|
## Guards And Transports
|
|
8
9
|
|
|
9
10
|
- Guards run on both HTTP and websocket calls. Read the caller with `context.get<T>("account")` (`pkgs/akanjs/signal/signalContext.ts`) instead of branching on `getHttpContext()` / `getWebSocketContext()`.
|
|
10
11
|
- Slice-level `guards` only reach the generated query/mutation endpoints. A `pubsub`/`message` endpoint is unguarded unless it declares its own `guards` in its signal option.
|
|
12
|
+
- **Every endpoint that declares no guards is named in a boot `warn`**, since it answers anyone over HTTP and
|
|
13
|
+
WebSocket — a named slice's `init()`, a root slice, a `pubsub`/`message`/`query`/`mutation`. Write
|
|
14
|
+
`guards: [Public]` to keep one open on purpose.
|
|
15
|
+
- **A generated verb is taken off HTTP with `guards: { cru: false }`** (or `get`/`create`/`update`/`remove:
|
|
16
|
+
false`), and a verb whose guards resolve to none is not mounted at all. `mcp: { cru: false }` does **not** do
|
|
17
|
+
this: it keeps the verbs off the agent shelf while HTTP serves them to whoever passes their guards.
|
|
11
18
|
- A pubsub room is authorized once, at subscribe. When a socket's credential changes the framework re-runs each room's guards and unsubscribes the ones that now fail (`SignalResolver.revalidateWsRooms`), so guards must stay side-effect free and safe to re-run. Each re-check runs on a fresh `SignalContext`, so a guard that memoizes per context answers anew.
|
|
12
19
|
- **Arguments are parsed after the `"account"` guards** (`SignalContext.init` reads the body, `exec` parses): the order is middlewares → `account` guards → parse → `resource` guards → internal arguments → handler, whatever order `guards: [...]` lists them in. A caller an account guard refuses never reaches a scalar parser, which is what keeps a slow parser from being a pre-auth denial of service. A guard or middleware that reads `context.args` / `getArg()` earlier still gets the value, parsed on first read — a mismarked account guard keeps working. One visible consequence: an unauthenticated call with a malformed argument answers 401/403 instead of 400.
|
|
13
20
|
- A websocket carries its credential in the handshake snapshot on `ws.data` (`AppWsData`); clients that hold the token in memory send it with `fetch.setJwt(...)`, which forwards an auth frame over the socket.
|
|
@@ -24,7 +31,8 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
24
31
|
- **A device or another server proves itself with a machine token.** `MachineToken.issue({ kind, id, version },
|
|
25
32
|
{ expiresAt? })` in `akanjs/server` signs an EdDSA JWT (`typ: akan-machine+jwt`, `aud: <app>/<environment>`) with
|
|
26
33
|
`AKAN_MACHINE_PRIVATE_KEY`; it never expires unless asked. `akan machine-keygen <file>` writes that key to a file
|
|
27
|
-
and prints only the public one. A server with either key
|
|
34
|
+
and prints only the public one. A server with either key (`AKAN_MACHINE_PRIVATE_KEY` or
|
|
35
|
+
`AKAN_MACHINE_PUBLIC_KEY`) verifies the bearer of every call — HTTP, and the
|
|
28
36
|
websocket's auth frame — after every lib middleware, and sets `account.machine = { kind, id, version }` from a
|
|
29
37
|
verified token; it takes off a `machine` any other middleware or a session's claims put there, so a staff token
|
|
30
38
|
can never pass for one, and a session middleware already reads the EdDSA token as anonymous. `.with(Machine)` hands
|
|
@@ -51,6 +59,26 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
51
59
|
socket's close, arrives while the subscribe is still running, the subscribe answers `subscribe: false` and runs its
|
|
52
60
|
`unsubscribe` cleanup (and its `disconnect` one when the socket closed) instead of joining the room.
|
|
53
61
|
|
|
62
|
+
## Writing A Guard
|
|
63
|
+
|
|
64
|
+
- **Resource guards are `Can<Verb><Model>` classes in `srvkit/guards.ts`** that `implements Guard` with an
|
|
65
|
+
`async canPass(context)`. They **fail closed**: no resource named ⇒ `false`; a load that throws ⇒ `logger.warn`
|
|
66
|
+
then `false`. Admin bypass goes first.
|
|
67
|
+
- **Keep `static name = "User";` on guard classes.** `fetch` serializes guard names and the API explorer filters
|
|
68
|
+
on them; it looks like dead code, and deleting it breaks the UI. Comment it so the next reader knows.
|
|
69
|
+
- **Every guard class declares `static scope: GuardScope`, required with no default.** `"account"` means the
|
|
70
|
+
verdict reads the caller and nothing about the call, so it can be evaluated with no arguments — which is what
|
|
71
|
+
lets an MCP listing hide what this caller certainly cannot use. `"resource"` means it needs the call's arguments
|
|
72
|
+
(`context.getArg()`) and fails closed without them, so it is never evaluated for a listing: the entry stays
|
|
73
|
+
visible and is stopped at call time. Getting it wrong is not a type error, so the marker is mandatory rather than
|
|
74
|
+
defaulted — `SignedIn` / `Admin` / role checks are `"account"`, and every `Can<Verb><Model>` is `"resource"`.
|
|
75
|
+
What a mismarked guard does to a listing, and `static agents = false` on `Person`: `mcpRule`.
|
|
76
|
+
- **Guards ship with the library that owns the model** and are imported by its own signals through the package
|
|
77
|
+
path, so a mounting app inherits authorization and cannot forget it.
|
|
78
|
+
- The acting user arrives via `.with(Self)` / `.with(CurrentUserId)` / `.with(Me)`; never trust a client-supplied
|
|
79
|
+
id. Services re-check ownership even when a guard already gated the call — two independent gates.
|
|
80
|
+
- `srvkit/guards.ts` earns real comments: explain what would leak without each guard.
|
|
81
|
+
|
|
54
82
|
## Request Deadlines
|
|
55
83
|
|
|
56
84
|
- **`{ timeout: <ms> }` in an endpoint's signal option is the whole configuration surface**, and it reaches both
|
|
@@ -65,7 +93,9 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
65
93
|
five-minute budget that is a five-minute hold on the event loop for a call that answered in a millisecond.
|
|
66
94
|
- **Without a declaration the client's own default applies — 30 seconds** (`DEFAULT_TIMEOUT_MS`,
|
|
67
95
|
`fetch/client/httpClient.ts`), and the server imposes nothing. Nothing else bounds a request: `Bun.serve` runs
|
|
68
|
-
with `idleTimeout: 0`, so a long call is only ever ended by one of these two.
|
|
96
|
+
with `idleTimeout: 0`, so a long call is only ever ended by one of these two. An endpoint whose real work runs
|
|
97
|
+
longer — provisioning, a firmware flow, an external orchestration — therefore has to declare one, or it dies
|
|
98
|
+
mid-flight with a transport error that does not say whether the server ever received it.
|
|
69
99
|
- **Precedence is caller, then endpoint, then client default.** `fetch.x(…, { timeout: 5_000 })` overrides what
|
|
70
100
|
the endpoint declared; `{ timeout: false }` waits as long as the runtime will. An app moves its own default
|
|
71
101
|
with `fetch.instance.setTimeout(ms)` — `lib/useClient.ts` is generated, so the `timeout` option on
|
|
@@ -102,9 +132,10 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
102
132
|
values for its TTL, as the handler returned them.
|
|
103
133
|
- **A cache backend that is down does not take the endpoint down.** A failed read is warned and the call runs
|
|
104
134
|
uncached; a failed write is warned and dropped.
|
|
105
|
-
-
|
|
106
|
-
`
|
|
107
|
-
handler: a hung cache backend is bounded by the same
|
|
135
|
+
- A call runs the cross-site check and an `ip` rate limit, then the body read, then the default middleware chain
|
|
136
|
+
`Logging → Timeout → <lib middlewares>`, and inside it an `account` rate limit → account guards → argument
|
|
137
|
+
parse → resource guards → internal arguments → cache → handler: a hung cache backend is bounded by the same
|
|
138
|
+
deadline as the handler it stands in for. **`Retry` was removed** — an automatic re-run of an endpoint
|
|
108
139
|
whose failure it cannot classify replays whatever the first attempt already did.
|
|
109
140
|
|
|
110
141
|
## Repeated Mutations
|
|
@@ -157,6 +188,43 @@ mutation is mounted under. `conventions` carries the invariants — this is the
|
|
|
157
188
|
`{ ok: true } | { ok: false, retryAfterMs }` and is never switched off — throw the module's own `Err` with the
|
|
158
189
|
wait when it refuses.
|
|
159
190
|
|
|
191
|
+
## Calling Another Server
|
|
192
|
+
|
|
193
|
+
- **A server-side `fetch.clone({ origin })` opens no websocket unless `connect: true`**, and neither does a clone
|
|
194
|
+
of such a clone: it makes one-off calls to another server, and a socket it opened would reconnect forever with
|
|
195
|
+
nobody to close it. It never carries the visitor's session either: a call to another origin sends only the `jwt`
|
|
196
|
+
its caller gave the clone.
|
|
197
|
+
- **Retry a call with `retry`, never with a loop of your own.** `fetch.clone({ origin, retry: {} })` (or a call's
|
|
198
|
+
own `{ retry }`) tries again on an unreachable server, a 502/503/504 and a 429 after its Retry-After, backing off
|
|
199
|
+
with jitter until the call's one deadline runs out. A mutation retries under one `Idempotency-Key` it mints, or
|
|
200
|
+
the `idempotencyKey` you name, and the server runs it once per key, caller and arguments (Repeated Mutations
|
|
201
|
+
above); a signed-out call, one whose handler returns a `Response`, and a call through MCP run every time. A 4xx
|
|
202
|
+
is never retried.
|
|
203
|
+
- **A failure another process reported travels as itself.** A server-to-server `fetch.<endpoint>(..., { origin })`
|
|
204
|
+
restores the remote `Err` — the same class, key and `data` — so a hop that rethrows it answers its own caller
|
|
205
|
+
with that error and the dictionary translates it. Never wrap the catch in a `new Error`, and never re-key it as a
|
|
206
|
+
`new Err` of your own: both discard what the endpoint chose to say, and a bare `Error` is generalized to
|
|
207
|
+
`Internal Server Error` on the way out.
|
|
208
|
+
- `String(err)` and `${err}` read `Err: <key> {…data}`, the data as one line of JSON with secret-named keys masked,
|
|
209
|
+
so a ``logger.warn(`…: ${String(error)}`)`` keeps the exit code or status the `Err` carried.
|
|
210
|
+
|
|
211
|
+
## Body Types And Bytes
|
|
212
|
+
|
|
213
|
+
- `.body(...)` / `.param(...)` args accept `ConstantFieldTypeInput` only: scalars, model refs, or `enumOf(...)`.
|
|
214
|
+
Numbers are `Int` or `Float` — `Number` is rejected (`pkgs/akanjs/signal/endpointInfo.ts`).
|
|
215
|
+
- **`Upload` is valid only inside a mutation flagged for file upload**:
|
|
216
|
+
`mutation([cnst.File], { fileUpload: true }).body("files", [Upload])`, as the `file` module does. It is not a
|
|
217
|
+
model field type. The other body args of such a mutation arrive typed as in a JSON body. A file's `type` on the
|
|
218
|
+
server is what its name implies, not its bytes — check the bytes when the type matters.
|
|
219
|
+
- **`Binary` is raw bytes on the wire.** It is `Uint8Array` on both sides — a Node `Buffer` is one, so a server
|
|
220
|
+
handler may pass one straight in — and it accepts base64 in either direction, so the same declaration serves a
|
|
221
|
+
JSON body and a binary frame. A `Binary` return travels as base64 in an HTTP body. It is never a model field
|
|
222
|
+
(`fieldRule`).
|
|
223
|
+
- **Never send bytes as `Any`.** `Any` passes a `Buffer` through untouched and `JSON.stringify` then spells it
|
|
224
|
+
`{ type: "Buffer", data: number[] }`, which `JSON.parse` never restores; the result is 3.6x the wire and a shape
|
|
225
|
+
that only breaks at the first byte-offset read. A return declared `Any` that turns out to be bytes fails instead
|
|
226
|
+
of reaching the caller as `{}`, and MCP refuses a `Binary` return for the same reason it refuses `Any`.
|
|
227
|
+
|
|
160
228
|
## Binary Pubsub
|
|
161
229
|
|
|
162
230
|
- **`pubsub(Binary)` sends its payload in a websocket binary frame**, skipping the JSON `{ type: "pub" }`
|
|
@@ -230,10 +230,10 @@ Six worked recipes carry the full code for the most frequent Akan changes, toget
|
|
|
230
230
|
| Recipe | Covers |
|
|
231
231
|
|---|---|
|
|
232
232
|
| 1. Adding a field to a model | the five `constant.ts` classes, the dictionary entry, the store form, `akan sync` |
|
|
233
|
-
| 2. Injecting a dependency into a service | `service<srv.XService>()`, `plug(Adapter)`, `env(...)`, `use<T>()` |
|
|
233
|
+
| 2. Injecting a dependency into a service | `service<srv.XService>()`, `plug(Adapter)`, `env(...)`, `use<T>()`, writing an `adapt()` adapter |
|
|
234
234
|
| 3. Creating and using a slice | `slice()` guards, the generated `init<Model><Suffix>`, `Load.Units` hydration |
|
|
235
235
|
| 4. Creating a mutation endpoint | the document chain method, the service call, the signal `guards`, the store action |
|
|
236
|
-
| 5. Internal triggers | `internal()` with interval and cron |
|
|
236
|
+
| 5. Internal triggers | `internal()` with interval and cron, queue retries and the dead letter |
|
|
237
237
|
| 6. Creating an insight | the `XInsight` class, the aggregation, the dashboard tile |
|
|
238
238
|
| Data Flow Summary | how a value travels page → store → signal → service → document → DB |
|
|
239
239
|
| Auto-Generated API Reference | what signal, service, store, and document each generate — never hand-write these |
|
|
@@ -244,7 +244,7 @@ existing content first and change only the relevant sections — never rewrite a
|
|
|
244
244
|
## Modeling & Query Gotchas
|
|
245
245
|
|
|
246
246
|
A short list of things the type system does not always catch. The full rules for text search, `cascade`, and the
|
|
247
|
-
query-level writes are in
|
|
247
|
+
query-level writes are in `queryRule`; these are the shapes that build green and fail later.
|
|
248
248
|
|
|
249
249
|
- **Slices return a query, not a list.** A slice `exec` must return `this.<model>Service.query<Filter>(...)`, never
|
|
250
250
|
a `list<Filter>(...)` / `listBy...(...)` array. Returning an array type-checks but fails at runtime with
|
|
@@ -258,7 +258,7 @@ query-level writes are in the convention set above; these are the shapes that bu
|
|
|
258
258
|
**bare** — `this.userModel.pickById(id, { passwordHash: true })`, likewise `pickOne` / `find` / `findOne` /
|
|
259
259
|
`findById`. A generated filter accessor takes it **inside the option object** —
|
|
260
260
|
`this.userModel.findByAccountId(accountId, statuses, { select: { passwordHash: true } })`. Handing `{ select: … }`
|
|
261
|
-
to the facade
|
|
261
|
+
to the facade projects a field named `select`, which no model has, and throws `Unknown document field path`.
|
|
262
262
|
- **`cascade` names a direction, and the wrong one is a data loss** — `removeRef` on the relation the owner holds,
|
|
263
263
|
`removeWith` on the child's reference to its owner. A query-level removal fires no hooks and therefore no cascade.
|
|
264
264
|
- **`q.search()` is a filter node, not a slice requirement.** `bySearch: filter().arg("text", String).query((text,
|
|
@@ -66,11 +66,12 @@ const form = st.use.taskForm();
|
|
|
66
66
|
|
|
67
67
|
### Recipe 2: Injecting a Dependency into a Service
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
registered under `<refName>` — the `Service`/`Signal` suffix is required
|
|
69
|
+
Four patterns: injecting an **external adapter** (`use<>()`), another **module's service** (`service<>()`),
|
|
70
|
+
a **predefined framework adapter** (`plug()`), or **an adapter you own** (`adapt()` + `plug()`). A field named
|
|
71
|
+
`<refName>Service` resolves to the service registered under `<refName>` — the `Service`/`Signal` suffix is required
|
|
72
|
+
and stripped to derive the lookup key.
|
|
72
73
|
|
|
73
|
-
> **For a new adapter you own, prefer the `adapt()` shape in pattern
|
|
74
|
+
> **For a new adapter you own, prefer the `adapt()` shape in pattern D over the `option.ts` registration in
|
|
74
75
|
> pattern A.** An `adapt()` class self-registers and is injected with `plug(TheClass)`, so it never touches
|
|
75
76
|
> `option.ts`. Pattern A is the legacy constructor-style shape: recognise it, keep it working, and migrate one
|
|
76
77
|
> only when you are already changing it.
|
|
@@ -147,6 +148,46 @@ storage directly: declare `image: field(File).optional()` (or `images: field([Fi
|
|
|
147
148
|
store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "removeRef" }` to that
|
|
148
149
|
field when the file belongs to the model alone, and removing the model removes the file and its stored object.
|
|
149
150
|
|
|
151
|
+
**D. An adapter you own — `adapt()` in `srvkit/`**
|
|
152
|
+
|
|
153
|
+
An injected singleton is an `adapt()` class in `srvkit/`. Write new adapters this way.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
export class AdminNoti extends adapt("adminNoti" as const, ({ use, env, plug, memory }) => ({
|
|
157
|
+
discordApi: use<DiscordApi>(),
|
|
158
|
+
workspaceRoot: env(() => `~/build/${getEnv().environment}`),
|
|
159
|
+
masterHost: plug(MasterHost),
|
|
160
|
+
tokenMap: memory(Map, { of: String }),
|
|
161
|
+
})) {}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
- Inject it with `plug(TheClass)` from a service or from another adapter. Destructure only the injectors you use,
|
|
165
|
+
and write the registration key `as const`.
|
|
166
|
+
- **Do not register an `adapt()` class in `lib/option.ts`.** It self-registers, and `plug(Class)` uses the class
|
|
167
|
+
itself as the token. `option.ts` is now only for legacy constructor-style adapters and for widening the options
|
|
168
|
+
type.
|
|
169
|
+
- `this.logger` is provided; never construct a `Logger` inside an `adapt()` class. Lifecycle work goes in
|
|
170
|
+
`override async onInit()`.
|
|
171
|
+
- **`adapt()` is for singletons only.** A per-use value object stays a plain class you `new` at the call site. If
|
|
172
|
+
there is not exactly one per process that a service needs injected, it is a plain class.
|
|
173
|
+
- **`memory(...)` takes a scalar or model class, not only a primitive** — `memory(Map, { of: CallStateInput })`
|
|
174
|
+
serializes through the constant and travels as JSON text, so the Redis and sqlite-backed caches round-trip the
|
|
175
|
+
same declaration. Never hand-encode JSON into a `String` memory. A memory is scoped to the service or adaptor
|
|
176
|
+
that declares it; `ttl` (milliseconds) sets how long each write lives unless the `set` call names its own
|
|
177
|
+
`expireAt`, and a `Map` entry expires on its own.
|
|
178
|
+
|
|
179
|
+
Conventions that hold for both adapter shapes (A and D):
|
|
180
|
+
|
|
181
|
+
- Route every remote call through one private `#api<T>(path, init?)` with `signal: AbortSignal.timeout(20_000)`.
|
|
182
|
+
- Paginate with `for (let page = 1; ; page += 1)` broken by `if (pageItems.length < 100) break;`.
|
|
183
|
+
- Resolve secrets as `process.env.X ?? options.x ?? deterministicGenerator(...)` **inside a function**, never at
|
|
184
|
+
module scope.
|
|
185
|
+
- Extend a function by appending an optional trailing parameter with a default, never by changing arity.
|
|
186
|
+
- Parameters: up to three required primitives positional; optional flags in a trailing `{ … } = {}`; four or more
|
|
187
|
+
parameters, or any two same-typed strings, in one named destructured object.
|
|
188
|
+
- Release locks in `finally`. Load heavy optional dependencies through a module-level memoized promise
|
|
189
|
+
(`puppeteerLoad ??= import("puppeteer")`).
|
|
190
|
+
|
|
150
191
|
---
|
|
151
192
|
|
|
152
193
|
### Recipe 3: Creating and Using a Slice
|
|
@@ -285,6 +326,10 @@ async cleanupStaleTasks() {
|
|
|
285
326
|
- `initialize()` — runs once on service startup
|
|
286
327
|
- `process(Return).msg(Type)` — message queue consumer
|
|
287
328
|
|
|
329
|
+
A `process` job's `backoff: { type: "exponential", delay, maxDelay }` caps the wait between attempts, and the
|
|
330
|
+
internal's `onFailed(job, error)` runs once a job has failed for good — its last attempt, or bullmq's stall limit —
|
|
331
|
+
the dead letter, on either queue.
|
|
332
|
+
|
|
288
333
|
---
|
|
289
334
|
|
|
290
335
|
### Recipe 6: Creating an Insight (Aggregation / Dashboard Stats)
|
|
@@ -3,12 +3,12 @@ import {
|
|
|
3
3
|
ApplicationBuildReporter,
|
|
4
4
|
HmrWatcher,
|
|
5
5
|
WatchRootResolver
|
|
6
|
-
} from "./index-
|
|
6
|
+
} from "./index-5nd1wtkv.js";
|
|
7
7
|
import {
|
|
8
8
|
CodegenLock,
|
|
9
9
|
WorkspaceExecutor,
|
|
10
10
|
AppExecutor
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-va06nr10.js";
|
|
12
12
|
import"./index-52w0fvh8.js";
|
|
13
13
|
import {
|
|
14
14
|
GraphClientEntryDiscovery,
|
|
@@ -19,10 +19,10 @@ import {
|
|
|
19
19
|
DevChangePlanner,
|
|
20
20
|
DevGeneratedIndexSync,
|
|
21
21
|
SsrDevBundler
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-w44cy4vs.js";
|
|
23
23
|
import {
|
|
24
24
|
ServerGraphFile
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-192ha4xh.js";
|
|
26
26
|
import {
|
|
27
27
|
prepareDevWatchBatch
|
|
28
28
|
} from "./index-a5vmm6x4.js";
|