@onetype/stack-api-kit 1.0.0
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/README.md +58 -0
- package/dist/api-BZrn0c9T.d.ts +654 -0
- package/dist/chunk-UCWOCNTI.js +1749 -0
- package/dist/chunk-UCWOCNTI.js.map +1 -0
- package/dist/index.d.ts +327 -0
- package/dist/index.js +381 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.d.ts +165 -0
- package/dist/testing.js +450 -0
- package/dist/testing.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
3
|
+
|
|
4
|
+
/** Anything declared carries a sentence saying what it is for. */
|
|
5
|
+
type Described = {
|
|
6
|
+
describe: string;
|
|
7
|
+
};
|
|
8
|
+
/** A declaration whose payload is checked before it reaches anyone. */
|
|
9
|
+
type Schematic = Described & {
|
|
10
|
+
schema: z.ZodType;
|
|
11
|
+
};
|
|
12
|
+
/** What a plugin may do, named so a project can grant it. */
|
|
13
|
+
type Permission = Described;
|
|
14
|
+
/** An event a plugin publishes. Delivered after the work it announces. */
|
|
15
|
+
type Event = Schematic;
|
|
16
|
+
/**
|
|
17
|
+
* What a listener does when an event arrives.
|
|
18
|
+
*
|
|
19
|
+
* `payload` is `unknown`, never `never`: a handler typed `(payload: never)`
|
|
20
|
+
* accepts any annotation its author writes, because of contravariance, so the
|
|
21
|
+
* compiler endorses a claim about a completely different schema.
|
|
22
|
+
*/
|
|
23
|
+
type Listener<Context, Payload = unknown> = Described & {
|
|
24
|
+
handle: (payload: Payload, ctx: Context) => void | Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
/** A point where a plugin may refuse what is about to happen. */
|
|
27
|
+
type Hook = Schematic;
|
|
28
|
+
/** What a participant answers: nothing to allow, a reason to refuse. */
|
|
29
|
+
type Participant<Context, Payload = unknown> = Described & {
|
|
30
|
+
handle: (payload: Payload, ctx: Context) => string | undefined | Promise<string | undefined>;
|
|
31
|
+
};
|
|
32
|
+
/** Something a plugin can be asked to do, behind the permissions it names. */
|
|
33
|
+
type Command<Context, Input extends z.ZodType = z.ZodType> = Schematic & {
|
|
34
|
+
schema: Input;
|
|
35
|
+
requires?: readonly string[];
|
|
36
|
+
run: (input: z.infer<Input>, ctx: Context) => void | Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
/** The verbs a route may answer. */
|
|
39
|
+
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
40
|
+
/**
|
|
41
|
+
* One endpoint.
|
|
42
|
+
*
|
|
43
|
+
* `input` and `output` are both required, and neither is optional sugar.
|
|
44
|
+
* `input` is the only thing a handler ever reads: what arrived unparsed does
|
|
45
|
+
* not reach it. `output` is a whitelist of what may leave, so a column added
|
|
46
|
+
* to a table tomorrow does not appear in a response by itself.
|
|
47
|
+
*/
|
|
48
|
+
type Route<Context, Input extends z.ZodType = z.ZodType> = Described & {
|
|
49
|
+
method: Method;
|
|
50
|
+
path: string;
|
|
51
|
+
input: Input;
|
|
52
|
+
output: z.ZodType;
|
|
53
|
+
requires?: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Whether an unauthenticated caller may reach this.
|
|
56
|
+
*
|
|
57
|
+
* Absent means no. A route is closed until it says otherwise, so
|
|
58
|
+
* forgetting to think about it fails shut.
|
|
59
|
+
*/
|
|
60
|
+
public?: boolean;
|
|
61
|
+
/** Requests per window for one caller, when this route needs its own. */
|
|
62
|
+
limit?: {
|
|
63
|
+
requests: number;
|
|
64
|
+
seconds: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Request headers this route reads, lowercase.
|
|
68
|
+
*
|
|
69
|
+
* Named rather than handed the lot: a handler that can read any header
|
|
70
|
+
* can read the cookie carrying the session, and a log of its input then
|
|
71
|
+
* carries a credential. What is not named does not arrive.
|
|
72
|
+
*/
|
|
73
|
+
reads?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* What answers the request.
|
|
76
|
+
*
|
|
77
|
+
* `input` is what the route's own schema parsed, so a handler reads its
|
|
78
|
+
* fields without a cast: nothing that failed the schema reaches here.
|
|
79
|
+
* Return a value for a 200, or an `Answered` to say the status and
|
|
80
|
+
* headers as well.
|
|
81
|
+
*/
|
|
82
|
+
handle: (input: z.infer<Input>, ctx: Context) => unknown | Promise<unknown>;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* A listener, participant or command, whatever payload it was written for.
|
|
86
|
+
*
|
|
87
|
+
* Each is written against the schema it answers and stored beside others
|
|
88
|
+
* written against different ones, so a list holds "some listener" rather than
|
|
89
|
+
* one shape. Sound because the kernel parses before it calls.
|
|
90
|
+
*/
|
|
91
|
+
type Heard<Context> = Described & {
|
|
92
|
+
handle: (payload: never, ctx: Context) => void | Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
type Joined<Context> = Described & {
|
|
95
|
+
handle: (payload: never, ctx: Context) => string | undefined | Promise<string | undefined>;
|
|
96
|
+
};
|
|
97
|
+
type Run<Context> = Schematic & {
|
|
98
|
+
requires?: readonly string[];
|
|
99
|
+
run: (input: never, ctx: Context) => void | Promise<void>;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* One route, whatever its input schema.
|
|
103
|
+
*
|
|
104
|
+
* A route is written against its own schema and stored beside routes written
|
|
105
|
+
* against others, so what a list holds is "some route", not one shape. This
|
|
106
|
+
* says that without reaching for `any`.
|
|
107
|
+
*/
|
|
108
|
+
type Endpoint<Context> = Omit<Route<Context, z.ZodType>, "input" | "handle"> & {
|
|
109
|
+
input: z.ZodType;
|
|
110
|
+
handle: (input: never, ctx: Context) => unknown | Promise<unknown>;
|
|
111
|
+
};
|
|
112
|
+
/** Where a plugin's lines go. The project decides. */
|
|
113
|
+
type Logger = {
|
|
114
|
+
debug: (line: string, about?: Readonly<Record<string, unknown>>) => void;
|
|
115
|
+
info: (line: string, about?: Readonly<Record<string, unknown>>) => void;
|
|
116
|
+
warn: (line: string, about?: Readonly<Record<string, unknown>>) => void;
|
|
117
|
+
error: (line: string, about?: Readonly<Record<string, unknown>>) => void;
|
|
118
|
+
};
|
|
119
|
+
/** Who is calling, as whatever the project decided that means. */
|
|
120
|
+
type Caller = {
|
|
121
|
+
/** Stable identity, or undefined when nobody is signed in. */
|
|
122
|
+
id: string | undefined;
|
|
123
|
+
/** What this caller may do. The project fills it; the kernel enforces it. */
|
|
124
|
+
permissions: readonly string[];
|
|
125
|
+
/** What the project attached: a tenant, a role, a session. Opaque here. */
|
|
126
|
+
claims: Readonly<Record<string, unknown>>;
|
|
127
|
+
};
|
|
128
|
+
/** One outbound call, to a host the plugin declared. */
|
|
129
|
+
type Outbound = {
|
|
130
|
+
method: Method;
|
|
131
|
+
url: string;
|
|
132
|
+
body?: unknown;
|
|
133
|
+
headers?: Readonly<Record<string, string>> | undefined;
|
|
134
|
+
signal?: AbortSignal | undefined;
|
|
135
|
+
};
|
|
136
|
+
/** What every plugin function receives. */
|
|
137
|
+
type Context<Config = unknown, Services = unknown, Db = unknown> = {
|
|
138
|
+
name: string;
|
|
139
|
+
config: Config;
|
|
140
|
+
services: Services;
|
|
141
|
+
log: Logger;
|
|
142
|
+
/**
|
|
143
|
+
* What time it is, in milliseconds.
|
|
144
|
+
*
|
|
145
|
+
* Reached through the context rather than `Date.now()` so a test can pin
|
|
146
|
+
* it: what happens tomorrow is otherwise only testable by moving the
|
|
147
|
+
* machine's clock, which every other test in the process then shares.
|
|
148
|
+
*/
|
|
149
|
+
now: () => number;
|
|
150
|
+
/** Who is calling. Absent outside a request, as in setup. */
|
|
151
|
+
caller: Caller | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* The request headers this route declared in `reads`, lowercase.
|
|
154
|
+
*
|
|
155
|
+
* Empty outside a request, and empty for anything the route did not
|
|
156
|
+
* name.
|
|
157
|
+
*/
|
|
158
|
+
headers: Readonly<Record<string, string>>;
|
|
159
|
+
/**
|
|
160
|
+
* This plugin's own tables.
|
|
161
|
+
*
|
|
162
|
+
* The handle carries only what this plugin declared, so a query naming
|
|
163
|
+
* another plugin's table does not compile. The connection underneath is
|
|
164
|
+
* shared, so the boundary is the compiler's rather than the database's.
|
|
165
|
+
*/
|
|
166
|
+
db: Db;
|
|
167
|
+
/**
|
|
168
|
+
* Runs one query outside a transaction, in its turn.
|
|
169
|
+
*
|
|
170
|
+
* A query issued while another request's transaction is parked on an
|
|
171
|
+
* await joins that transaction and dies with its rollback, having told
|
|
172
|
+
* its caller it succeeded. Reads are safe without this; a write is not.
|
|
173
|
+
*/
|
|
174
|
+
write: <Made>(run: () => Promise<Made>) => Promise<Made>;
|
|
175
|
+
/**
|
|
176
|
+
* Runs work in one transaction, rolled back if it throws.
|
|
177
|
+
*
|
|
178
|
+
* The callback is handed a context of its own, not just a handle: what it
|
|
179
|
+
* emits waits for the commit, and a `tx` inside it joins this one
|
|
180
|
+
* rather than opening a second. A caller that used the outer `ctx` would
|
|
181
|
+
* be writing outside the transaction it just opened.
|
|
182
|
+
*/
|
|
183
|
+
tx: <Made>(run: (ctx: Context<Config, Services, Db>) => Promise<Made>) => Promise<Made>;
|
|
184
|
+
/** Calls a host this plugin declared in `outbound`. */
|
|
185
|
+
fetch: (call: Outbound) => Promise<unknown>;
|
|
186
|
+
events: {
|
|
187
|
+
/**
|
|
188
|
+
* Announces what happened. Inside a transaction it waits and is sent
|
|
189
|
+
* after the commit: an event about work that rolled back is a lie.
|
|
190
|
+
*/
|
|
191
|
+
emit: (event: string, payload: unknown) => void;
|
|
192
|
+
};
|
|
193
|
+
hooks: {
|
|
194
|
+
/** Runs a hook and answers the first refusal, or undefined. */
|
|
195
|
+
run: (hook: string, payload: unknown) => Promise<string | undefined>;
|
|
196
|
+
};
|
|
197
|
+
permissions: {
|
|
198
|
+
has: (permission: string) => boolean;
|
|
199
|
+
all: (permissions: readonly string[]) => boolean;
|
|
200
|
+
/** What the project attached to this caller, unread by the kernel. */
|
|
201
|
+
claims: () => Readonly<Record<string, unknown>>;
|
|
202
|
+
};
|
|
203
|
+
commands: {
|
|
204
|
+
run: (command: string, input: unknown) => Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Runs one later, in seconds from now.
|
|
207
|
+
*
|
|
208
|
+
* Only a command this plugin declares, and it runs with no caller:
|
|
209
|
+
* whatever it needs to know about whose work it is travels in the
|
|
210
|
+
* input, exactly as an event's payload does.
|
|
211
|
+
*
|
|
212
|
+
* Asked for inside a transaction, it is written by that transaction
|
|
213
|
+
* and rolls back with it. A command that throws is tried again.
|
|
214
|
+
*/
|
|
215
|
+
later: (command: string, input: unknown, inSeconds: number) => void;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Takes ownership of something that outlives a request.
|
|
219
|
+
*
|
|
220
|
+
* Services are built per request, because one holding a caller would
|
|
221
|
+
* answer the next request as the previous one. A connection is the
|
|
222
|
+
* opposite: opened once in `setup`, used by every request, closed in
|
|
223
|
+
* `teardown`. This is where it lives, one per plugin, and no plugin
|
|
224
|
+
* reaches another's.
|
|
225
|
+
*/
|
|
226
|
+
owns: <Owned>(owned: Owned) => Owned;
|
|
227
|
+
/** What this plugin took ownership of, or undefined before `setup` did. */
|
|
228
|
+
owned: <Owned>() => Owned | undefined;
|
|
229
|
+
/**
|
|
230
|
+
* What narrows every read of a table this plugin declared a `scope` for.
|
|
231
|
+
*
|
|
232
|
+
* Answers the caller's value for the declared claim, refusing when there
|
|
233
|
+
* is none. Nothing makes a query call this: one that forgets reads every
|
|
234
|
+
* scope's rows and compiles, which is why every scoped read is tested
|
|
235
|
+
* with a stranger's id.
|
|
236
|
+
*
|
|
237
|
+
* ```ts
|
|
238
|
+
* .where(and(eq(items.id, id), ctx.scoped("items")))
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
scoped: <Condition = unknown>(table: string) => Condition;
|
|
242
|
+
/**
|
|
243
|
+
* The row's scope column, filled from the caller.
|
|
244
|
+
*
|
|
245
|
+
* A condition narrows a read, and an insert has no condition: without
|
|
246
|
+
* this, a caller in one tenant can write a row stamped with another's.
|
|
247
|
+
* Spread it over what you are writing so the column is not yours to
|
|
248
|
+
* remember, or to get wrong.
|
|
249
|
+
*
|
|
250
|
+
* ```ts
|
|
251
|
+
* .values({ ...row, ...ctx.stamped("items") })
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
stamped: (table: string) => Readonly<Record<string, string>>;
|
|
255
|
+
/**
|
|
256
|
+
* The same plugin, acting for the scope this names.
|
|
257
|
+
*
|
|
258
|
+
* A listener runs on nobody's behalf, so `scoped` and `stamped` refuse
|
|
259
|
+
* there, and every method they reach refuses with them. This says whose
|
|
260
|
+
* work the payload announced, so the ordinary path works instead of a
|
|
261
|
+
* second unscoped one written beside it.
|
|
262
|
+
*
|
|
263
|
+
* ```ts
|
|
264
|
+
* handle: (gone, ctx) => Orders.dropFor(ctx.forScope(gone.shopId), gone.id)
|
|
265
|
+
* ```
|
|
266
|
+
*
|
|
267
|
+
* Refused where a caller already exists: inside a request the scope is
|
|
268
|
+
* decided by who is asking, and choosing another there is how a caller
|
|
269
|
+
* reaches rows that are not theirs.
|
|
270
|
+
*/
|
|
271
|
+
forScope: (claim: string) => Context<Config, Services, Db>;
|
|
272
|
+
/** Another plugin's services, by name. Only what `dependsOn` names. */
|
|
273
|
+
use: <Reached>(plugin: string) => Reached;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Blocks inference at this position.
|
|
277
|
+
*
|
|
278
|
+
* Services is inferred from what `services` returns and from nowhere else. A
|
|
279
|
+
* callback taking a context would otherwise be a second inference site, and
|
|
280
|
+
* two candidates for one parameter resolve to unknown.
|
|
281
|
+
*/
|
|
282
|
+
type Given<Made> = NoInfer<Made>;
|
|
283
|
+
/** Everything a plugin declares about itself. */
|
|
284
|
+
type Definition<Schema extends z.ZodType = z.ZodType, Services = unknown, Db = unknown> = Described & {
|
|
285
|
+
version: string;
|
|
286
|
+
dependsOn?: readonly string[];
|
|
287
|
+
config?: Schema;
|
|
288
|
+
permissions?: Readonly<Record<string, Permission>>;
|
|
289
|
+
/** This plugin's tables, in its own namespace. Nobody else reads them. */
|
|
290
|
+
tables?: Readonly<Record<string, unknown>>;
|
|
291
|
+
/**
|
|
292
|
+
* Which claim decides whose rows these are, and where each table carries
|
|
293
|
+
* it.
|
|
294
|
+
*
|
|
295
|
+
* The kit knows nothing about tenants: it does not know what one is, what
|
|
296
|
+
* the claim means, or whether a project has any. What it knows, once this
|
|
297
|
+
* is declared, is that a read of a named table without that column is a
|
|
298
|
+
* read of somebody else's rows, so `ctx.db` stops handing one out and
|
|
299
|
+
* `ctx.scoped` hands out the query already narrowed.
|
|
300
|
+
*
|
|
301
|
+
* Declaring it also decides the failure: a caller carrying no such claim
|
|
302
|
+
* is refused rather than defaulted, because a default tenant is
|
|
303
|
+
* everybody's tenant.
|
|
304
|
+
*/
|
|
305
|
+
scope?: {
|
|
306
|
+
describe: string;
|
|
307
|
+
claim: string;
|
|
308
|
+
tables: Readonly<Record<string, string>>;
|
|
309
|
+
};
|
|
310
|
+
/** Where its migrations live, run in dependency order before setup. */
|
|
311
|
+
migrations?: string;
|
|
312
|
+
/** Hosts this plugin may call. Anything else is refused before it dials. */
|
|
313
|
+
outbound?: readonly string[];
|
|
314
|
+
services?: (ctx: Context<z.infer<Schema>, never, Db>) => Services;
|
|
315
|
+
/**
|
|
316
|
+
* The endpoints this plugin answers.
|
|
317
|
+
*
|
|
318
|
+
* Each route carries its own input schema, so `handle` reads what that
|
|
319
|
+
* schema parsed rather than `unknown`. A handler taking a narrower input
|
|
320
|
+
* is sound here precisely because the kernel parses before it calls: what
|
|
321
|
+
* failed the schema never arrives.
|
|
322
|
+
*/
|
|
323
|
+
routes?: readonly Endpoint<Context<z.infer<Schema>, Given<Services>, Db>>[];
|
|
324
|
+
emits?: Readonly<Record<string, Event>>;
|
|
325
|
+
listens?: Readonly<Record<string, Heard<Context<z.infer<Schema>, Given<Services>, Db>>>>;
|
|
326
|
+
hooks?: Readonly<Record<string, Hook>>;
|
|
327
|
+
participates?: Readonly<Record<string, Joined<Context<z.infer<Schema>, Given<Services>, Db>>>>;
|
|
328
|
+
commands?: Readonly<Record<string, Run<Context<z.infer<Schema>, Given<Services>, Db>>>>;
|
|
329
|
+
setup?: (ctx: Context<z.infer<Schema>, Given<Services>, Db>) => void | Promise<void>;
|
|
330
|
+
teardown?: (ctx: Context<z.infer<Schema>, Given<Services>, Db>) => void | Promise<void>;
|
|
331
|
+
};
|
|
332
|
+
/** A plugin: its name, and what it declared. */
|
|
333
|
+
type Plugin = {
|
|
334
|
+
name: string;
|
|
335
|
+
definition: Definition;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
type Failure = {
|
|
339
|
+
event: string;
|
|
340
|
+
plugin: string;
|
|
341
|
+
error: unknown;
|
|
342
|
+
at: number;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/** What decides whether one caller has any budget left on one route. */
|
|
346
|
+
type Budget = {
|
|
347
|
+
take: (key: string, window: {
|
|
348
|
+
requests: number;
|
|
349
|
+
seconds: number;
|
|
350
|
+
}) => {
|
|
351
|
+
allowed: boolean;
|
|
352
|
+
resetsIn: number;
|
|
353
|
+
};
|
|
354
|
+
};
|
|
355
|
+
/** One request, as it reaches the kernel. */
|
|
356
|
+
type Incoming = {
|
|
357
|
+
method: Method;
|
|
358
|
+
path: string;
|
|
359
|
+
input: unknown;
|
|
360
|
+
caller?: Caller | undefined;
|
|
361
|
+
/** The request's headers, lowercase. A route sees only what it declared. */
|
|
362
|
+
headers?: Readonly<Record<string, string>> | undefined;
|
|
363
|
+
/**
|
|
364
|
+
* Where it came from, when nobody is signed in: an address, a key, or
|
|
365
|
+
* whatever the project counts anonymous callers by. Only a rate limit
|
|
366
|
+
* reads it.
|
|
367
|
+
*/
|
|
368
|
+
from?: string | undefined;
|
|
369
|
+
};
|
|
370
|
+
/** What the kernel answers: a status, and a body already safe to send. */
|
|
371
|
+
type Outgoing = {
|
|
372
|
+
status: number;
|
|
373
|
+
body: unknown;
|
|
374
|
+
headers?: Readonly<Record<string, string>>;
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* What the kernel needs to reach storage.
|
|
379
|
+
*
|
|
380
|
+
* A shape rather than a driver: the kernel never imports a database, so a
|
|
381
|
+
* project chooses one and a test passes its own. Narrower than what
|
|
382
|
+
* `database()` returns, which also opens, migrates and closes: the kernel
|
|
383
|
+
* needs none of that and should not be able to do it.
|
|
384
|
+
*/
|
|
385
|
+
type Storage = {
|
|
386
|
+
/** One plugin's own handle. What it holds is the project's business. */
|
|
387
|
+
of: (plugin: string) => unknown;
|
|
388
|
+
/** Runs work in one transaction, rolled back if it throws. */
|
|
389
|
+
tx: <Made>(plugin: string, run: (db: unknown) => Promise<Made>) => Promise<Made>;
|
|
390
|
+
/**
|
|
391
|
+
* Runs work that is not in a transaction, but never during someone
|
|
392
|
+
* else's. Optional, so a project may pass a store that needs no such
|
|
393
|
+
* ordering.
|
|
394
|
+
*/
|
|
395
|
+
write?: <Made>(run: () => Promise<Made>) => Promise<Made>;
|
|
396
|
+
/** Whether a transaction is open right now. For diagnosis. */
|
|
397
|
+
inTransaction?: () => boolean;
|
|
398
|
+
};
|
|
399
|
+
/** One event, as it waits to be delivered. */
|
|
400
|
+
type Announcement = {
|
|
401
|
+
id: string;
|
|
402
|
+
plugin: string;
|
|
403
|
+
name: string;
|
|
404
|
+
payload: unknown;
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* Where events wait, so one is never lost between a commit and its delivery.
|
|
408
|
+
*
|
|
409
|
+
* Without this, an event lives only in memory: the work commits, the process
|
|
410
|
+
* dies, and the listener is never called by anything. Nobody is told, because
|
|
411
|
+
* the emitter was told nothing to begin with.
|
|
412
|
+
*
|
|
413
|
+
* `keep` runs inside the emitting transaction, so an event is written exactly
|
|
414
|
+
* when the work it announces is, and rolled back with it. `sent` runs after
|
|
415
|
+
* delivery. Anything still kept at startup was interrupted, and `waiting`
|
|
416
|
+
* hands it back to be delivered again.
|
|
417
|
+
*
|
|
418
|
+
* Delivery is therefore at least once, never exactly once: a listener that
|
|
419
|
+
* writes must be able to run twice on one event without doubling anything.
|
|
420
|
+
*/
|
|
421
|
+
type Outbox = {
|
|
422
|
+
/** Writes events inside the transaction that emitted them. */
|
|
423
|
+
keep: (db: unknown, announcements: readonly Announcement[]) => void;
|
|
424
|
+
/** Marks one delivered. */
|
|
425
|
+
sent: (id: string) => Promise<void>;
|
|
426
|
+
/** What was kept but never marked sent. Read once, at startup. */
|
|
427
|
+
waiting: () => Promise<readonly Announcement[]>;
|
|
428
|
+
};
|
|
429
|
+
/** One command waiting for its moment. */
|
|
430
|
+
type Scheduled = {
|
|
431
|
+
id: string;
|
|
432
|
+
plugin: string;
|
|
433
|
+
command: string;
|
|
434
|
+
input: unknown;
|
|
435
|
+
at: number;
|
|
436
|
+
attempts: number;
|
|
437
|
+
};
|
|
438
|
+
/**
|
|
439
|
+
* Where work waits until it is time.
|
|
440
|
+
*
|
|
441
|
+
* The kernel has no clock of its own and no timer: it asks `due` on a beat
|
|
442
|
+
* the project set, runs what it is handed, and says how it went. Everything
|
|
443
|
+
* that has to survive a restart lives in the database, so a process that
|
|
444
|
+
* stops between taking a job and finishing it leaves the job takeable again.
|
|
445
|
+
*
|
|
446
|
+
* `take` is what makes one process pick up a job and not another: it must
|
|
447
|
+
* claim and return in one step, or two processes run the same work.
|
|
448
|
+
*/
|
|
449
|
+
type Schedule = {
|
|
450
|
+
/** Writes one, inside the transaction that asked for it when there is one. */
|
|
451
|
+
keep: (db: unknown, job: Scheduled) => void;
|
|
452
|
+
/** Claims what is due, at most `limit`, marking each taken. */
|
|
453
|
+
take: (now: number, limit: number) => Promise<readonly Scheduled[]>;
|
|
454
|
+
/** It ran. Forget it. */
|
|
455
|
+
done: (id: string) => Promise<void>;
|
|
456
|
+
/** It threw. Put it back for `at`, having counted the attempt. */
|
|
457
|
+
failed: (id: string, at: number) => Promise<void>;
|
|
458
|
+
/** It threw too many times. Stop trying. */
|
|
459
|
+
gaveUp: (id: string) => Promise<void>;
|
|
460
|
+
};
|
|
461
|
+
/**
|
|
462
|
+
* How a scope becomes a condition the database understands.
|
|
463
|
+
*
|
|
464
|
+
* The kernel imports no driver, so it cannot build one: it knows which table
|
|
465
|
+
* and which value, and the project turns that into whatever its store speaks.
|
|
466
|
+
*/
|
|
467
|
+
type Narrowing = (table: string, column: string, value: string) => unknown;
|
|
468
|
+
/** What the kernel needs to call another server. */
|
|
469
|
+
type Dialer = (call: Outbound) => Promise<unknown>;
|
|
470
|
+
|
|
471
|
+
/** Where a line goes. The project decides; a plugin never writes directly. */
|
|
472
|
+
type Log = (level: "debug" | "info" | "warn" | "error", plugin: string, line: string, about?: Readonly<Record<string, unknown>>) => void;
|
|
473
|
+
/** What a project gives the kernel. */
|
|
474
|
+
type Options = {
|
|
475
|
+
plugins: readonly Plugin[];
|
|
476
|
+
config?: Readonly<Record<string, unknown>>;
|
|
477
|
+
db?: Storage;
|
|
478
|
+
dial?: Dialer;
|
|
479
|
+
log?: Log;
|
|
480
|
+
/**
|
|
481
|
+
* What counts requests against a route's declared budget.
|
|
482
|
+
*
|
|
483
|
+
* Omit it and a `limit` is inert, which is why `start` says so rather
|
|
484
|
+
* than letting a declared budget quietly enforce nothing.
|
|
485
|
+
*/
|
|
486
|
+
budget?: Budget;
|
|
487
|
+
/**
|
|
488
|
+
* Where events wait between the transaction that emitted them and the
|
|
489
|
+
* listener that hears them.
|
|
490
|
+
*
|
|
491
|
+
* Without one, an event that is emitted lives only in memory: the work
|
|
492
|
+
* commits, the process stops, and nothing ever calls the listener. With
|
|
493
|
+
* one, delivery is at least once, so a listener that writes must survive
|
|
494
|
+
* being called twice.
|
|
495
|
+
*/
|
|
496
|
+
outbox?: Outbox;
|
|
497
|
+
/**
|
|
498
|
+
* What the current time is, in milliseconds.
|
|
499
|
+
*
|
|
500
|
+
* A test pins it to make tomorrow reachable without moving the machine's
|
|
501
|
+
* clock, which every other test in the process would then share.
|
|
502
|
+
*/
|
|
503
|
+
now?: () => number;
|
|
504
|
+
/**
|
|
505
|
+
* Where work waits until it is time, and how often to look.
|
|
506
|
+
*
|
|
507
|
+
* Without one, `ctx.commands.later` refuses: a plugin that can ask for
|
|
508
|
+
* later work in a deployment that cannot run it would be told nothing.
|
|
509
|
+
*/
|
|
510
|
+
schedule?: Schedule;
|
|
511
|
+
/** How often to ask the schedule what is due, in milliseconds. */
|
|
512
|
+
beat?: number;
|
|
513
|
+
/**
|
|
514
|
+
* How many times a scheduled command may throw before it is abandoned.
|
|
515
|
+
*
|
|
516
|
+
* Eight by default, which is roughly four minutes of backing off. A job
|
|
517
|
+
* that has failed that often is failing on something a retry will not
|
|
518
|
+
* fix, and the line saying it gave up is worth more than the ninth try.
|
|
519
|
+
*/
|
|
520
|
+
attempts?: number;
|
|
521
|
+
/**
|
|
522
|
+
* How a declared scope becomes a condition the store understands.
|
|
523
|
+
*
|
|
524
|
+
* The kernel imports no driver, so a project that declares a scope also
|
|
525
|
+
* says how to narrow by it.
|
|
526
|
+
*/
|
|
527
|
+
narrow?: Narrowing;
|
|
528
|
+
/**
|
|
529
|
+
* How long a hook participant has to answer, in milliseconds.
|
|
530
|
+
*
|
|
531
|
+
* A participant that never answers holds the request open, and a throw is
|
|
532
|
+
* already a refusal, so silence is treated as one too.
|
|
533
|
+
*/
|
|
534
|
+
patience?: number;
|
|
535
|
+
};
|
|
536
|
+
/** A route, and the plugin it came from. */
|
|
537
|
+
type Registered = {
|
|
538
|
+
plugin: string;
|
|
539
|
+
method: Method;
|
|
540
|
+
path: string;
|
|
541
|
+
describe: string;
|
|
542
|
+
requires: readonly string[];
|
|
543
|
+
public: boolean;
|
|
544
|
+
limit: {
|
|
545
|
+
requests: number;
|
|
546
|
+
seconds: number;
|
|
547
|
+
} | undefined;
|
|
548
|
+
/** The request headers this route declared it reads, lowercase. */
|
|
549
|
+
reads: readonly string[];
|
|
550
|
+
};
|
|
551
|
+
/** What a project holds after createKernel. */
|
|
552
|
+
type Kernel = {
|
|
553
|
+
start: () => Promise<void>;
|
|
554
|
+
stop: () => Promise<void>;
|
|
555
|
+
started: () => boolean;
|
|
556
|
+
routes: () => readonly Registered[];
|
|
557
|
+
handle: (incoming: Incoming) => Promise<Outgoing>;
|
|
558
|
+
context: (plugin: string, caller?: Caller) => Context;
|
|
559
|
+
events: {
|
|
560
|
+
failures: () => readonly Failure[];
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Runs whatever the schedule says is due, once, and waits for it.
|
|
564
|
+
*
|
|
565
|
+
* What the beat does on a timer, asked for. A test moves its clock and
|
|
566
|
+
* calls this instead of waiting a real second for an interval it does not
|
|
567
|
+
* control.
|
|
568
|
+
*/
|
|
569
|
+
due: () => Promise<void>;
|
|
570
|
+
run: (command: string, input: unknown, caller?: Caller) => Promise<void>;
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* Builds a kernel from what the plugins declared.
|
|
574
|
+
*
|
|
575
|
+
* Nothing runs here: `start` validates first, and either brings up every
|
|
576
|
+
* plugin or throws. A half-started kernel behaves according to where it
|
|
577
|
+
* stopped, which is not a state anyone can reason about.
|
|
578
|
+
*/
|
|
579
|
+
declare function createKernel(options: Options): Kernel;
|
|
580
|
+
|
|
581
|
+
/** What opening a database needs to know. */
|
|
582
|
+
type Opening = {
|
|
583
|
+
/** A path, or ":memory:" for one that lives as long as the process. */
|
|
584
|
+
file: string;
|
|
585
|
+
/** How long a writer waits for another to finish, in milliseconds. */
|
|
586
|
+
busyMs?: number;
|
|
587
|
+
/** Whether to keep the write-ahead log. Off for :memory:, which has none. */
|
|
588
|
+
wal?: boolean;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
/** Where one plugin keeps its migrations. */
|
|
592
|
+
type Source = {
|
|
593
|
+
plugin: string;
|
|
594
|
+
from: string;
|
|
595
|
+
};
|
|
596
|
+
/** One migration file, as it sits on disk. */
|
|
597
|
+
type Step = {
|
|
598
|
+
plugin: string;
|
|
599
|
+
name: string;
|
|
600
|
+
sql: string;
|
|
601
|
+
hash: string;
|
|
602
|
+
};
|
|
603
|
+
/** What went wrong, in a sentence naming the file. */
|
|
604
|
+
declare class MigrationFault extends Error {
|
|
605
|
+
readonly plugin: string;
|
|
606
|
+
readonly step: string | undefined;
|
|
607
|
+
constructor(message: string, plugin: string, step?: string);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
type Tables = Readonly<Record<string, unknown>>;
|
|
611
|
+
type Handle = ReturnType<typeof drizzle>;
|
|
612
|
+
|
|
613
|
+
/** What building a store needs: where the file is, and who owns what. */
|
|
614
|
+
type Settings = Opening & {
|
|
615
|
+
tables: Readonly<Record<string, Tables>>;
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* What a project holds after opening a database.
|
|
619
|
+
*
|
|
620
|
+
* Wider than the kernel's `Storage`: this one also migrates and closes, which
|
|
621
|
+
* a plugin has no business doing and the kernel never asks for.
|
|
622
|
+
*/
|
|
623
|
+
/**
|
|
624
|
+
* What `start` needs of a database, whichever one it is.
|
|
625
|
+
*
|
|
626
|
+
* Wider than the kernel's `Storage`: this one also migrates and closes, which
|
|
627
|
+
* a plugin has no business doing and the kernel never asks for. `of` answers
|
|
628
|
+
* `unknown` because the kit does not know what database it was given; a
|
|
629
|
+
* plugin names the shape it expects through `definePlugin.over`.
|
|
630
|
+
*/
|
|
631
|
+
type Store<Handed = unknown> = {
|
|
632
|
+
of: (plugin: string) => Handed;
|
|
633
|
+
/** An outbox in this same database, when the store can hold one. */
|
|
634
|
+
outbox?: () => Outbox;
|
|
635
|
+
/** A schedule in this same database, for work asked for later. */
|
|
636
|
+
schedule?: () => Schedule;
|
|
637
|
+
/** How a declared scope becomes a condition over the tables it was given. */
|
|
638
|
+
narrowing?: () => Narrowing;
|
|
639
|
+
tx: <Made>(plugin: string, run: (db: unknown) => Promise<Made>) => Promise<Made>;
|
|
640
|
+
write: <Made>(run: () => Promise<Made>) => Promise<Made>;
|
|
641
|
+
inTransaction: () => boolean;
|
|
642
|
+
migrate: (sources: readonly Source[]) => Step[];
|
|
643
|
+
close: () => void;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Opens a database and holds one handle per plugin over it.
|
|
648
|
+
*
|
|
649
|
+
* Built by the project rather than reached for: two stores can exist in one
|
|
650
|
+
* process without seeing each other, which is what a test needs.
|
|
651
|
+
*/
|
|
652
|
+
declare function database(settings: Settings): Store<Handle>;
|
|
653
|
+
|
|
654
|
+
export { type Announcement as A, type Budget as B, type Context as C, type Definition as D, type Endpoint as E, type Failure as F, type Handle as H, type Incoming as I, type Joined as J, type Kernel as K, type Listener as L, type Method as M, type Narrowing as N, type Outbox as O, type Participant as P, type Route as R, type Schedule as S, type Tables as T, type Command as a, type Plugin as b, type Caller as c, type Outbound as d, type Store as e, type Opening as f, type Dialer as g, type Logger as h, type Described as i, type Event as j, type Heard as k, type Hook as l, type Log as m, MigrationFault as n, type Options as o, type Outgoing as p, type Permission as q, type Registered as r, type Run as s, type Scheduled as t, type Schematic as u, type Source as v, type Step as w, type Storage as x, createKernel as y, database as z };
|