@midsummer-ai/sdk 0.0.0-stage → 0.1.0-next.1305

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.
Files changed (79) hide show
  1. package/LICENSE +177 -0
  2. package/README.md +4 -2
  3. package/agents/contract.d.mts +9 -0
  4. package/agents/contract.mjs +221 -0
  5. package/agents/index.d.mts +128 -0
  6. package/agents/index.mjs +203 -0
  7. package/agents/stand-in.d.mts +35 -0
  8. package/agents/stand-in.mjs +149 -0
  9. package/ai/contract.d.mts +10 -0
  10. package/ai/contract.mjs +77 -0
  11. package/ai/index.d.mts +94 -0
  12. package/ai/index.mjs +193 -0
  13. package/ai/stand-in.d.mts +7 -0
  14. package/ai/stand-in.mjs +41 -0
  15. package/build-SQ44jwSa.d.mts +41 -0
  16. package/client.d.mts +45 -0
  17. package/client.mjs +106 -0
  18. package/connection-CxD_Nry5.mjs +1911 -0
  19. package/contract.d.mts +10 -0
  20. package/contract.mjs +469 -0
  21. package/database/build.d.mts +2 -0
  22. package/database/build.mjs +579 -0
  23. package/database/client.d.mts +60 -0
  24. package/database/client.mjs +133 -0
  25. package/database/contract.d.mts +11 -0
  26. package/database/contract.mjs +164 -0
  27. package/database/errors.d.mts +23 -0
  28. package/database/errors.mjs +13 -0
  29. package/database/server.d.mts +3 -0
  30. package/database/server.mjs +320 -0
  31. package/database/stand-in.d.mts +99 -0
  32. package/database/stand-in.mjs +430 -0
  33. package/email/client.d.mts +34 -0
  34. package/email/client.mjs +131 -0
  35. package/email/contract.d.mts +11 -0
  36. package/email/contract.mjs +99 -0
  37. package/email/index.d.mts +74 -0
  38. package/email/index.mjs +59 -0
  39. package/email/smtp.d.mts +22 -0
  40. package/email/smtp.mjs +296 -0
  41. package/email/stand-in.d.mts +13 -0
  42. package/email/stand-in.mjs +51 -0
  43. package/index-DdoK0y7D.d.mts +192 -0
  44. package/index-sgdyoA2P.d.mts +212 -0
  45. package/index.d.mts +2 -0
  46. package/index.mjs +137 -0
  47. package/names-DfCBKupJ.mjs +115 -0
  48. package/package.json +152 -4
  49. package/predict-Drfm_8pO.mjs +179 -0
  50. package/queues/contract.d.mts +13 -0
  51. package/queues/contract.mjs +278 -0
  52. package/queues/index.d.mts +184 -0
  53. package/queues/index.mjs +126 -0
  54. package/queues/stand-in.d.mts +7 -0
  55. package/queues/stand-in.mjs +110 -0
  56. package/queues/wire.d.mts +9 -0
  57. package/queues/wire.mjs +199 -0
  58. package/s3.d.mts +43 -0
  59. package/s3.mjs +1003 -0
  60. package/schema-BF-Y1EVu.mjs +142 -0
  61. package/schema-DS8x1Dn8.mjs +115 -0
  62. package/schema-f7E9uzUn.d.mts +74 -0
  63. package/server-C7O7CICU.d.mts +182 -0
  64. package/socket.d.mts +67 -0
  65. package/socket.mjs +263 -0
  66. package/stand-in.d.mts +7 -0
  67. package/stand-in.mjs +185 -0
  68. package/store-DDuOr-_F.mjs +237 -0
  69. package/tabs-4FLTfg1c.mjs +457 -0
  70. package/vector/contract.d.mts +10 -0
  71. package/vector/contract.mjs +635 -0
  72. package/vector/index.d.mts +2 -0
  73. package/vector/index.mjs +108 -0
  74. package/vector/stand-in.d.mts +11 -0
  75. package/vector/stand-in.mjs +169 -0
  76. package/vector/wire.d.mts +25 -0
  77. package/vector/wire.mjs +175 -0
  78. package/wire.d.mts +54 -0
  79. package/wire.mjs +731 -0
@@ -0,0 +1,99 @@
1
+ import { t as Built } from "../build-SQ44jwSa.mjs";
2
+ import { d as Row, i as FunctionReference, r as FunctionKind, u as RegisteredFunction } from "../server-C7O7CICU.mjs";
3
+ //#region src/database/stand-in.d.ts
4
+ /** A Database as its client reaches it: by function name or reference. A refusal throws `DatabaseError`. */
5
+ interface DatabaseClient {
6
+ query(fn: string | FunctionReference, args?: unknown): Promise<unknown>;
7
+ /** `tag` identifies the call: sent again under it, the mutation applies once. */
8
+ mutation(fn: string | FunctionReference, args?: unknown, options?: {
9
+ readonly tag?: string;
10
+ }): Promise<unknown>;
11
+ /** `tag` identifies the call, as a mutation's does; a server that keeps results runs it once for it. */
12
+ action(fn: string | FunctionReference, args?: unknown, options?: {
13
+ readonly tag?: string;
14
+ }): Promise<unknown>;
15
+ /** Gives `onValue` the query's value now and again after every change it sees; returns a stop. */
16
+ watch(fn: string | FunctionReference, args: unknown, onValue: (value: unknown) => void): () => void;
17
+ /** The same client for a signed-in caller, by the Project's sign-in's subject. */
18
+ as(subject: string): DatabaseClient;
19
+ /** A new call identity. */
20
+ tag(): Promise<string>;
21
+ }
22
+ /** A build's functions, loaded: each by its name, with the schema's tables and indexes. */
23
+ interface Loaded {
24
+ readonly functions: ReadonlyMap<string, RegisteredFunction>;
25
+ readonly tables: Built["tables"];
26
+ readonly indexes: Built["indexes"];
27
+ }
28
+ interface StandIn {
29
+ /** Serves `loaded`'s functions from now on. The rows stay. */
30
+ serve(loaded: Loaded): void;
31
+ /**
32
+ * Runs `work` between mutations, in their order: none commits while it runs, so what it reads of
33
+ * the rows stays true until it ends (checking rows against a schema, then serving it). `work`
34
+ * must not run a mutation itself.
35
+ */
36
+ exclusive<T>(work: () => T | Promise<T>): Promise<T>;
37
+ /** A client with no signed-in caller. */
38
+ readonly client: DatabaseClient;
39
+ /**
40
+ * A client for any caller: `user` signed in, or the server's own code (`server`), with
41
+ * `internal` functions too. What a Database Key or a run by hand calls as.
42
+ */
43
+ caller(caller: Caller): DatabaseClient;
44
+ /** What kind of function `name` is, or undefined when there is none: the wire sends actions as mutations. */
45
+ kind(name: string): FunctionKind | undefined;
46
+ /** Every row, by table and id. */
47
+ rows(): Record<string, Record<string, Row>>;
48
+ /**
49
+ * Commits what `write` returns, between mutations, as one commit of its own as a mutation's would
50
+ * be: told to `committed` and seen by every watch. `write` is given the rows as they are then, and
51
+ * may throw to commit nothing. How a Database's rows are put back as a restore point held them.
52
+ */
53
+ commit(write: (rows: Record<string, Record<string, Stored>>) => ReadonlyArray<Change>): Promise<ReadonlyArray<Change>>;
54
+ /** Stops the scheduler's timers. */
55
+ close(): void;
56
+ }
57
+ /**
58
+ * Writes a build's files under `folder` and loads them, each import of the surface naming this SDK's
59
+ * `server.ts`. They reach it through a shim beside them, `_generated/server.mjs`, that re-exports the
60
+ * module loaded here: a file on disk can't import one inside a compiled CLI, where this SDK's files
61
+ * live only in the binary. Each load is a folder of its own, so a module loaded before never stands in
62
+ * for a newer one.
63
+ */
64
+ declare function loadDatabase(built: Built, folder: string): Promise<Loaded>;
65
+ /** A row as the Stand-in keeps it: its fields and its version, which counts its writes. */
66
+ type Stored = Row & {
67
+ readonly _version: number;
68
+ };
69
+ /** One row a mutation changed: its table, its id and what it is now, null when deleted. */
70
+ interface Change {
71
+ readonly table: string;
72
+ readonly id: string;
73
+ readonly row: Stored | null;
74
+ }
75
+ interface Caller {
76
+ readonly user: string | null;
77
+ readonly server: boolean;
78
+ readonly internal: boolean;
79
+ }
80
+ /** Orders index keys as the Database does: by type (null, numbers, strings, booleans), then by value. */
81
+ declare function compare(a: unknown, b: unknown): number;
82
+ /**
83
+ * `rows` are the rows it starts with, as `rows()` gave them (a branch's, from its parent);
84
+ * `committed` is told every mutation's changes as it commits them, in order. `admits` is asked as a
85
+ * mutation starts, and what it returns is asked in the same step as the mutation commits, with nothing
86
+ * between: so the commit can require what held at the start (a Database's lifecycle version). A reason
87
+ * refuses the commit, and the mutation is refused as `unavailable` with nothing it wrote kept.
88
+ * `read` is told how many rows each read gave a function, a mutation's own writes included.
89
+ */
90
+ declare function databaseStandIn(options?: {
91
+ readonly clock?: () => number;
92
+ readonly random?: (bytes: number) => Uint8Array;
93
+ readonly rows?: Readonly<Record<string, Readonly<Record<string, Stored>>>>;
94
+ readonly committed?: (changes: ReadonlyArray<Change>) => void;
95
+ readonly admits?: () => () => string | null;
96
+ readonly read?: (rows: number) => void;
97
+ }): StandIn;
98
+ //#endregion
99
+ export { Caller, Change, DatabaseClient, Loaded, StandIn, Stored, compare, databaseStandIn, loadDatabase };
@@ -0,0 +1,430 @@
1
+ import { a as shapeError, i as indexFields } from "../schema-BF-Y1EVu.mjs";
2
+ import { InsufficientError, NotFoundError, Refusal, isFunction } from "./server.mjs";
3
+ import { DatabaseError } from "./errors.mjs";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/database/stand-in.ts
8
+ /**
9
+ * The Database's Stand-in, for `midsummer dev`: a Project's own `db/` functions run in memory, with
10
+ * the Database's meaning. Mutations run one at a time, each writing all of what it wrote or none of
11
+ * it; a mutation sent again under the same tag applies once; arguments, results and writes are
12
+ * checked against their shapes; a watched query is given its value again after every change; the
13
+ * scheduler runs what it is given. It passes the Database's contract
14
+ * (`@midsummer-ai/sdk/database/contract`) except durability and scale, and holds no engine code.
15
+ *
16
+ * const built = buildDatabase(sources, strip)
17
+ * const database = databaseStandIn()
18
+ * database.serve(await loadDatabase(built, folder))
19
+ * const id = await database.client.as("ada").mutation("messages.send", { channel: "a", body: "hi" })
20
+ *
21
+ * The Project's code runs in this process, as it does on its author's machine. Crons do not run
22
+ * here yet.
23
+ */
24
+ const SURFACE = /(\bfrom\s*)(["'])(@midsummer(?:-ai)?\/sdk\/server|\.\/_generated\/server(?:\.m?js)?)\2/gu;
25
+ /** Where the loaded surface waits for the shim each load imports it through. */
26
+ const HELD = Symbol.for("midsummer.database.surface");
27
+ let loads = 0;
28
+ /**
29
+ * Writes a build's files under `folder` and loads them, each import of the surface naming this SDK's
30
+ * `server.ts`. They reach it through a shim beside them, `_generated/server.mjs`, that re-exports the
31
+ * module loaded here: a file on disk can't import one inside a compiled CLI, where this SDK's files
32
+ * live only in the binary. Each load is a folder of its own, so a module loaded before never stands in
33
+ * for a newer one.
34
+ */
35
+ async function loadDatabase(built, folder) {
36
+ const surface = await import("./server.mjs");
37
+ globalThis[HELD] = surface;
38
+ const names = Object.keys(surface).filter((name) => name !== "default" && /^[A-Za-z_$][\w$]*$/.test(name));
39
+ const dir = join(folder, `load-${process.pid}-${loads += 1}`);
40
+ await mkdir(join(dir, "_generated"), { recursive: true });
41
+ await writeFile(join(dir, "_generated", "api.mjs"), built.generated["api.mjs"]);
42
+ await writeFile(join(dir, "_generated", "server.mjs"), `const surface = globalThis[Symbol.for("midsummer.database.surface")];\nexport const { ${names.join(", ")} } = surface;\n`);
43
+ for (const [name, text] of Object.entries(built.files)) await writeFile(join(dir, name), text.replace(SURFACE, (_all, from, quote) => `${from}${quote}./_generated/server.mjs${quote}`));
44
+ const functions = /* @__PURE__ */ new Map();
45
+ for (const fn of built.functions) {
46
+ const exported = (await import(pathToFileURL(join(dir, fn.file)).href))[fn.export];
47
+ if (!isFunction(exported)) throw new Error(`${built.from[fn.file]}: ${fn.export} is not a function`);
48
+ functions.set(fn.name, exported);
49
+ }
50
+ return {
51
+ functions,
52
+ tables: built.tables,
53
+ indexes: built.indexes
54
+ };
55
+ }
56
+ /** A value as the wire carries it: JSON, `undefined` gone. */
57
+ const wire = (value) => value === void 0 ? null : JSON.parse(JSON.stringify(value));
58
+ const hex = (bytes) => [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
59
+ const nameOf = (fn) => typeof fn === "string" ? fn : fn.name;
60
+ /** Orders index keys as the Database does: by type (null, numbers, strings, booleans), then by value. */
61
+ function compare(a, b) {
62
+ const rank = (x) => x === null || x === void 0 ? 0 : typeof x === "number" ? 1 : typeof x === "string" ? 2 : typeof x === "boolean" ? 3 : 4;
63
+ const ra = rank(a), rb = rank(b);
64
+ if (ra !== rb) return ra - rb;
65
+ if (ra === 4) return 0;
66
+ return a < b ? -1 : a > b ? 1 : 0;
67
+ }
68
+ /** Why a call was refused, as the client carries it. */
69
+ function refusalOf(error) {
70
+ if (error instanceof DatabaseError) return error;
71
+ if (error instanceof Refusal) return new DatabaseError(error.why, error.message, error.detail);
72
+ if (error instanceof InsufficientError) return new DatabaseError("short", error.message, error.detail);
73
+ if (error instanceof NotFoundError) return new DatabaseError("notfound", error.message, error.detail);
74
+ return new DatabaseError("refused", error instanceof Error ? error.message : String(error));
75
+ }
76
+ /**
77
+ * `rows` are the rows it starts with, as `rows()` gave them (a branch's, from its parent);
78
+ * `committed` is told every mutation's changes as it commits them, in order. `admits` is asked as a
79
+ * mutation starts, and what it returns is asked in the same step as the mutation commits, with nothing
80
+ * between: so the commit can require what held at the start (a Database's lifecycle version). A reason
81
+ * refuses the commit, and the mutation is refused as `unavailable` with nothing it wrote kept.
82
+ * `read` is told how many rows each read gave a function, a mutation's own writes included.
83
+ */
84
+ function databaseStandIn(options = {}) {
85
+ const clock = options.clock ?? Date.now;
86
+ const random = options.random ?? ((bytes) => crypto.getRandomValues(new Uint8Array(bytes)));
87
+ const tables = new Map(Object.entries(options.rows ?? {}).map(([name, rows]) => [name, new Map(Object.entries(rows))]));
88
+ const answers = /* @__PURE__ */ new Map();
89
+ const watchers = /* @__PURE__ */ new Set();
90
+ const scheduled = /* @__PURE__ */ new Map();
91
+ let loaded = null;
92
+ let turn = Promise.resolve();
93
+ const tableOf = (name) => {
94
+ let table = tables.get(name);
95
+ if (!table) tables.set(name, table = /* @__PURE__ */ new Map());
96
+ return table;
97
+ };
98
+ /**
99
+ * The rows a function reads: the committed ones, and those `overlay` wrote, which a mutation's own
100
+ * reads see. Each is a copy, so a function that changes a value it read changes only its copy: a
101
+ * kept row changes only by a write that commits, and so does every Database or branch sharing it.
102
+ */
103
+ function reading(overlay) {
104
+ const get = (table, id) => {
105
+ const written = overlay?.get(table);
106
+ const row = written?.has(id) ? written.get(id) : tables.get(table)?.get(id);
107
+ return row ? structuredClone(row) : null;
108
+ };
109
+ const counted = (table, id) => {
110
+ const row = get(table, id);
111
+ if (row) options.read?.(1);
112
+ return row;
113
+ };
114
+ const range = (table, index, bounds) => {
115
+ const rows = [.../* @__PURE__ */ new Set([...tables.get(table)?.keys() ?? [], ...overlay?.get(table)?.keys() ?? []])].flatMap((id) => {
116
+ const row = get(table, id);
117
+ return row ? [{
118
+ ...row,
119
+ _id: id
120
+ }] : [];
121
+ });
122
+ let fields = [];
123
+ if (index !== "_id") {
124
+ const declared = loaded?.indexes?.[table]?.[index];
125
+ if (declared === void 0) throw new Refusal("invalid", `${table} has no index ${index}`);
126
+ fields = indexFields(declared);
127
+ }
128
+ const keyOf = (row) => index === "_id" ? [row._id] : [...fields.map((field) => row[field]), row._id];
129
+ const eq = bounds.eq ?? [];
130
+ const at = eq.length;
131
+ const kept = rows.filter((row) => {
132
+ const key = keyOf(row);
133
+ if (eq.some((value, k) => compare(key[k], value) !== 0)) return false;
134
+ const next = key[at];
135
+ if (bounds.gte !== void 0 && compare(next, bounds.gte) < 0) return false;
136
+ if (bounds.gt !== void 0 && compare(next, bounds.gt) <= 0) return false;
137
+ if (bounds.lte !== void 0 && compare(next, bounds.lte) > 0) return false;
138
+ if (bounds.lt !== void 0 && compare(next, bounds.lt) >= 0) return false;
139
+ return true;
140
+ });
141
+ kept.sort((a, b) => {
142
+ const ka = keyOf(a), kb = keyOf(b);
143
+ for (let k = 0; k < ka.length; k += 1) {
144
+ const c = compare(ka[k], kb[k]);
145
+ if (c) return c;
146
+ }
147
+ return 0;
148
+ });
149
+ if (bounds.order === "desc") kept.reverse();
150
+ const page = kept.slice(0, bounds.limit);
151
+ options.read?.(page.length);
152
+ return page;
153
+ };
154
+ return {
155
+ get: counted,
156
+ range,
157
+ peek: get
158
+ };
159
+ }
160
+ function schedule(ms, name, args) {
161
+ const id = `s${hex(random(8))}`;
162
+ const entry = {
163
+ name,
164
+ args: wire(args),
165
+ due: clock() + ms,
166
+ state: "pending"
167
+ };
168
+ scheduled.set(id, entry);
169
+ entry.timer = setTimeout(() => {
170
+ if (entry.state !== "pending") return;
171
+ entry.state = "running";
172
+ invoke(name, entry.args, {
173
+ user: null,
174
+ server: true,
175
+ internal: true
176
+ }).then(() => {
177
+ entry.state = "success";
178
+ }, (error) => {
179
+ entry.state = "failed";
180
+ entry.error = refusalOf(error).message;
181
+ });
182
+ }, ms);
183
+ return id;
184
+ }
185
+ const scheduledRow = (id) => {
186
+ const entry = scheduled.get(id);
187
+ return entry ? {
188
+ name: entry.name,
189
+ args: entry.args,
190
+ due: entry.due,
191
+ state: entry.state,
192
+ ...entry.error ? { error: entry.error } : {}
193
+ } : null;
194
+ };
195
+ const cancelScheduled = (id) => {
196
+ const entry = scheduled.get(id);
197
+ if (!entry || entry.state !== "pending") return false;
198
+ clearTimeout(entry.timer);
199
+ entry.state = "canceled";
200
+ return true;
201
+ };
202
+ function functionFor(name, kind, caller) {
203
+ const fn = loaded?.functions.get(name);
204
+ if (!fn) throw new DatabaseError("no function", `no function ${name}`);
205
+ if (fn.options.kind !== kind) throw new DatabaseError("no function", `no ${kind} ${name} (it is a ${fn.options.kind})`);
206
+ if (fn.options.internal && !caller.internal) throw new DatabaseError("internal", `${name} is internal: no client may call it`);
207
+ return fn;
208
+ }
209
+ function checked(fn, args) {
210
+ if (fn.options.args) {
211
+ const why = shapeError(fn.options.args, args ?? {}, "args");
212
+ if (why) throw new DatabaseError("invalid", why);
213
+ }
214
+ }
215
+ function answered(fn, value) {
216
+ if (fn.options.returns) {
217
+ const why = shapeError(fn.options.returns, value, "returns");
218
+ if (why) throw new DatabaseError("invalid", why);
219
+ }
220
+ return wire(value);
221
+ }
222
+ const refusedRows = (what) => () => {
223
+ throw new DatabaseError("refused", `a query ${what}: only a mutation writes`);
224
+ };
225
+ async function runQuery(fn, args, caller) {
226
+ const { get, range } = reading();
227
+ const rows = {
228
+ get,
229
+ range,
230
+ set: refusedRows("writes"),
231
+ delete: refusedRows("deletes"),
232
+ now: clock(),
233
+ user: caller.user,
234
+ server: caller.server,
235
+ issuer: null,
236
+ internal: caller.internal,
237
+ schema: loaded?.tables ?? null,
238
+ runAfter: refusedRows("schedules"),
239
+ scheduledRow,
240
+ cancelScheduled: refusedRows("cancels")
241
+ };
242
+ return answered(fn, await fn.handler(rows, args));
243
+ }
244
+ function runMutation(fn, args, caller, tag) {
245
+ const run = async () => {
246
+ const admitted = options.admits?.();
247
+ if (tag !== void 0) {
248
+ const had = answers.get(tag);
249
+ if (had) {
250
+ if (had.error) throw had.error;
251
+ return had.value;
252
+ }
253
+ }
254
+ const overlay = /* @__PURE__ */ new Map();
255
+ const later = [];
256
+ const { get, range, peek } = reading(overlay);
257
+ const written = (table) => {
258
+ let map = overlay.get(table);
259
+ if (!map) overlay.set(table, map = /* @__PURE__ */ new Map());
260
+ return map;
261
+ };
262
+ const rows = {
263
+ get,
264
+ range,
265
+ now: clock(),
266
+ user: caller.user,
267
+ server: caller.server,
268
+ issuer: null,
269
+ internal: caller.internal,
270
+ call: tag ?? `c${hex(random(8))}`,
271
+ schema: loaded?.tables ?? null,
272
+ set: (table, id, fields) => written(table).set(id, structuredClone({
273
+ ...fields,
274
+ _version: (peek(table, id)?._version ?? 0) + 1
275
+ })),
276
+ delete: (table, id) => written(table).set(id, null),
277
+ runAfter: (ms, name, args) => {
278
+ const id = `s${hex(random(8))}`;
279
+ later.push({
280
+ ms,
281
+ name,
282
+ args
283
+ });
284
+ return id;
285
+ },
286
+ scheduledRow,
287
+ cancelScheduled
288
+ };
289
+ try {
290
+ const value = answered(fn, await fn.handler(rows, args));
291
+ const refused = admitted?.() ?? null;
292
+ if (refused !== null) throw new DatabaseError("unavailable", refused);
293
+ const changes = [];
294
+ for (const [table, changed] of overlay) for (const [id, row] of changed) {
295
+ if (row) tableOf(table).set(id, row);
296
+ else tables.get(table)?.delete(id);
297
+ changes.push({
298
+ table,
299
+ id,
300
+ row
301
+ });
302
+ }
303
+ if (changes.length) options.committed?.(changes);
304
+ for (const run of later) schedule(run.ms, run.name, run.args);
305
+ if (tag !== void 0) answers.set(tag, { value });
306
+ if (overlay.size) for (const watcher of watchers) watcher.run();
307
+ return value;
308
+ } catch (error) {
309
+ const refusal = refusalOf(error);
310
+ if (tag !== void 0) answers.set(tag, { error: refusal });
311
+ throw refusal;
312
+ }
313
+ };
314
+ const result = turn.then(run, run);
315
+ turn = result.catch(() => void 0);
316
+ return result;
317
+ }
318
+ async function runAction(fn, args, caller) {
319
+ const { get, range } = reading();
320
+ const capabilities = {
321
+ get: async (table, id) => get(table, id),
322
+ range: async (table, index, bounds) => range(table, index, bounds),
323
+ user: caller.user,
324
+ server: caller.server,
325
+ issuer: null,
326
+ internal: caller.internal,
327
+ fetch: (url, init) => fetch(url, init),
328
+ now: clock,
329
+ random,
330
+ run: (name, args) => invokeAny(name, args, {
331
+ ...caller,
332
+ internal: true
333
+ }),
334
+ schedule: async (ms, name, args) => schedule(ms, name, args),
335
+ scheduled: async (id) => {
336
+ const row = scheduledRow(id);
337
+ return row ? {
338
+ name: row.name,
339
+ args: row.args,
340
+ at: row.due,
341
+ state: row.state,
342
+ ...row.error ? { error: row.error } : {}
343
+ } : null;
344
+ },
345
+ cancel: async (id) => cancelScheduled(id)
346
+ };
347
+ return answered(fn, await fn.handler(capabilities, args));
348
+ }
349
+ async function invoke(name, args, caller, kind, tag) {
350
+ const fn = kind ? functionFor(name, kind, caller) : loaded?.functions.get(name);
351
+ if (!fn) throw new DatabaseError("no function", `no function ${name}`);
352
+ if (!kind) functionFor(name, fn.options.kind, caller);
353
+ const given = wire(args ?? {});
354
+ try {
355
+ checked(fn, given);
356
+ if (fn.options.kind === "query") return await runQuery(fn, given, caller);
357
+ if (fn.options.kind === "mutation") return await runMutation(fn, given, caller, tag);
358
+ return await runAction(fn, given, caller);
359
+ } catch (error) {
360
+ throw refusalOf(error);
361
+ }
362
+ }
363
+ const invokeAny = (name, args, caller) => invoke(name, args, caller);
364
+ const clientFor = (user) => callerClient({
365
+ user,
366
+ server: false,
367
+ internal: false
368
+ });
369
+ function callerClient(caller) {
370
+ return {
371
+ query: (fn, args) => invoke(nameOf(fn), args, caller, "query"),
372
+ mutation: (fn, args, options = {}) => invoke(nameOf(fn), args, caller, "mutation", options.tag),
373
+ action: (fn, args) => invoke(nameOf(fn), args, caller, "action"),
374
+ watch(fn, args, onValue) {
375
+ let last, stopped = false;
376
+ const watcher = { async run() {
377
+ try {
378
+ const value = await invoke(nameOf(fn), args, caller, "query");
379
+ const text = JSON.stringify(value);
380
+ if (!stopped && text !== last) {
381
+ last = text;
382
+ onValue(value);
383
+ }
384
+ } catch {}
385
+ } };
386
+ watchers.add(watcher);
387
+ watcher.run();
388
+ return () => {
389
+ stopped = true;
390
+ watchers.delete(watcher);
391
+ };
392
+ },
393
+ as: (subject) => clientFor(subject),
394
+ tag: async () => `t${hex(random(12))}`
395
+ };
396
+ }
397
+ function exclusive(work) {
398
+ const result = turn.then(work, work);
399
+ turn = result.catch(() => void 0);
400
+ return result;
401
+ }
402
+ return {
403
+ serve(next) {
404
+ loaded = next;
405
+ for (const watcher of watchers) watcher.run();
406
+ },
407
+ exclusive,
408
+ client: clientFor(null),
409
+ caller: callerClient,
410
+ kind: (name) => loaded?.functions.get(name)?.options.kind,
411
+ rows: () => Object.fromEntries([...tables].map(([name, rows]) => [name, Object.fromEntries(rows)])),
412
+ commit(write) {
413
+ return exclusive(() => {
414
+ const changes = write(Object.fromEntries([...tables].map(([name, rows]) => [name, Object.fromEntries(rows)])));
415
+ for (const change of changes) if (change.row) tableOf(change.table).set(change.id, change.row);
416
+ else tables.get(change.table)?.delete(change.id);
417
+ if (changes.length) {
418
+ options.committed?.(changes);
419
+ for (const watcher of watchers) watcher.run();
420
+ }
421
+ return changes;
422
+ });
423
+ },
424
+ close() {
425
+ for (const entry of scheduled.values()) clearTimeout(entry.timer);
426
+ }
427
+ };
428
+ }
429
+ //#endregion
430
+ export { compare, databaseStandIn, loadDatabase };
@@ -0,0 +1,34 @@
1
+ import { CheckedMessage, EmailEngine, EmailErrorCode, SentMessage } from "./index.mjs";
2
+ //#region src/email/client.d.ts
3
+ declare const MESSAGES_PATH = "/email/messages";
4
+ type Fetch = (url: string, init: RequestInit) => Promise<Response>;
5
+ /** Email's engine at `origin` (the API's) for the Email key `key`. */
6
+ declare function client(origin: string, options: {
7
+ readonly key: string;
8
+ readonly fetch?: Fetch;
9
+ }): EmailEngine;
10
+ /**
11
+ * The platform's side of the wire: checks the request again, as it comes from the network, and hands
12
+ * the key and the Message to `send`, which throws an `EmailError` for a refusal.
13
+ */
14
+ declare function serveMessages(send: (key: string, message: CheckedMessage) => Promise<SentMessage>): (request: Request) => Promise<Response>;
15
+ /** What a bound Email handle returns: the Message as sent, or the refusal, which crosses a Service Binding as data. */
16
+ type BoundAnswer = SentMessage | {
17
+ readonly error: {
18
+ readonly code: EmailErrorCode;
19
+ readonly message: string;
20
+ };
21
+ };
22
+ /**
23
+ * What Midsummer hands a Deployment as `context.email`: sends from the Sending domains its
24
+ * Environment binds, with no key.
25
+ *
26
+ * await email(boundEmail(context.email)).send({ from: "Acme <hi@acme.dev>", to: ["ada@example.com"], subject: "Hi", text: "Hello" })
27
+ */
28
+ declare function boundEmail(handle: {
29
+ send(message: CheckedMessage): Promise<BoundAnswer>;
30
+ }): EmailEngine;
31
+ /** The platform's side of a bound handle: the same checks as `serveMessages`, a refusal returned as data. */
32
+ declare function serveBound(send: (message: CheckedMessage) => Promise<SentMessage>): (message: unknown) => Promise<BoundAnswer>;
33
+ //#endregion
34
+ export { BoundAnswer, MESSAGES_PATH, boundEmail, client, serveBound, serveMessages };
@@ -0,0 +1,131 @@
1
+ import { EmailError, email } from "./index.mjs";
2
+ //#region src/email/client.ts
3
+ /**
4
+ * Email's wire: code holding an Email key sends through `client`, and the platform replies with
5
+ * `serveMessages`. One Message is one POST of JSON to `<origin>/email/messages` with
6
+ * `authorization: Bearer <the key's secret>`; the answer is the `SentMessage`, or
7
+ * `{ "error": { "code", "message" } }` with the code's status.
8
+ *
9
+ * const mail = email(client("https://api.<site>", { key }))
10
+ */
11
+ const MESSAGES_PATH = "/email/messages";
12
+ const STATUS = {
13
+ invalid: 400,
14
+ forbidden: 401,
15
+ unverified: 422,
16
+ unavailable: 503
17
+ };
18
+ /** Email's engine at `origin` (the API's) for the Email key `key`. */
19
+ function client(origin, options) {
20
+ const url = `${origin.replace(/\/+$/, "")}${MESSAGES_PATH}`;
21
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
22
+ return { async send(message) {
23
+ let response;
24
+ try {
25
+ response = await fetcher(url, {
26
+ method: "POST",
27
+ headers: {
28
+ authorization: `Bearer ${options.key}`,
29
+ "content-type": "application/json"
30
+ },
31
+ body: JSON.stringify(message)
32
+ });
33
+ } catch {
34
+ throw new EmailError("unavailable", "Email could not be reached");
35
+ }
36
+ const body = await response.json().catch(() => void 0);
37
+ if (response.ok && body && typeof body.id === "string") return body;
38
+ const code = body?.error?.code;
39
+ throw new EmailError(code && code in STATUS ? code : "unavailable", body?.error?.message ?? `Email answered ${response.status}`);
40
+ } };
41
+ }
42
+ /**
43
+ * The platform's side of the wire: checks the request again, as it comes from the network, and hands
44
+ * the key and the Message to `send`, which throws an `EmailError` for a refusal.
45
+ */
46
+ function serveMessages(send) {
47
+ const refuse = (code, message) => Response.json({ error: {
48
+ code,
49
+ message
50
+ } }, { status: STATUS[code] });
51
+ return async (request) => {
52
+ if (request.method !== "POST") return Response.json({ error: {
53
+ code: "invalid",
54
+ message: "Use POST."
55
+ } }, { status: 405 });
56
+ const key = /^Bearer (\S+)$/.exec(request.headers.get("authorization") ?? "")?.[1];
57
+ if (!key) return refuse("forbidden", "Send with an Email key: authorization: Bearer <key>.");
58
+ const text = await request.text();
59
+ if (text.length > 2e6) return refuse("invalid", "A Message is at most 2 MB.");
60
+ let body;
61
+ try {
62
+ body = JSON.parse(text);
63
+ } catch {
64
+ body = void 0;
65
+ }
66
+ if (!isMessage(body)) return refuse("invalid", "The body is not a Message.");
67
+ try {
68
+ const sent = await email({ send: (message) => send(key, message) }).send(body);
69
+ return Response.json({
70
+ id: sent.id,
71
+ state: sent.state,
72
+ suppressed: sent.suppressed,
73
+ ...sent.error !== void 0 ? { error: sent.error } : {}
74
+ });
75
+ } catch (error) {
76
+ if (error instanceof EmailError) return refuse(error.code, error.message);
77
+ throw error;
78
+ }
79
+ };
80
+ }
81
+ /**
82
+ * What Midsummer hands a Deployment as `context.email`: sends from the Sending domains its
83
+ * Environment binds, with no key.
84
+ *
85
+ * await email(boundEmail(context.email)).send({ from: "Acme <hi@acme.dev>", to: ["ada@example.com"], subject: "Hi", text: "Hello" })
86
+ */
87
+ function boundEmail(handle) {
88
+ return { async send(message) {
89
+ let answer;
90
+ try {
91
+ answer = await handle.send(message);
92
+ } catch {
93
+ throw new EmailError("unavailable", "Email could not be reached");
94
+ }
95
+ if ("id" in answer) return answer;
96
+ throw new EmailError(answer.error.code in STATUS ? answer.error.code : "unavailable", answer.error.message);
97
+ } };
98
+ }
99
+ /** The platform's side of a bound handle: the same checks as `serveMessages`, a refusal returned as data. */
100
+ function serveBound(send) {
101
+ return async (message) => {
102
+ if (!isMessage(message)) return { error: {
103
+ code: "invalid",
104
+ message: "The body is not a Message."
105
+ } };
106
+ try {
107
+ const sent = await email({ send }).send(message);
108
+ return {
109
+ id: sent.id,
110
+ state: sent.state,
111
+ suppressed: sent.suppressed,
112
+ ...sent.error !== void 0 ? { error: sent.error } : {}
113
+ };
114
+ } catch (error) {
115
+ if (error instanceof EmailError) return { error: {
116
+ code: error.code,
117
+ message: error.message
118
+ } };
119
+ throw error;
120
+ }
121
+ };
122
+ }
123
+ const strings = (value) => Array.isArray(value) && value.every((item) => typeof item === "string");
124
+ const optional = (value) => value === void 0 || typeof value === "string";
125
+ function isMessage(value) {
126
+ if (typeof value !== "object" || value === null) return false;
127
+ const m = value;
128
+ return typeof m.from === "string" && strings(m.to) && (m.cc === void 0 || strings(m.cc)) && (m.bcc === void 0 || strings(m.bcc)) && typeof m.subject === "string" && optional(m.replyTo) && optional(m.text) && optional(m.html) && optional(m.key);
129
+ }
130
+ //#endregion
131
+ export { MESSAGES_PATH, boundEmail, client, serveBound, serveMessages };