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

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-CDk7H1FL.mjs +1871 -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 +126 -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 +96 -0
  32. package/database/stand-in.mjs +422 -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-BzI9at5x.mjs +237 -0
  69. package/tabs-D-P8_ci_.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
package/socket.mjs ADDED
@@ -0,0 +1,263 @@
1
+ import { h as StorageError } from "./names-DfCBKupJ.mjs";
2
+ import { streamsOf } from "./index.mjs";
3
+ import { repeatable, serve } from "./wire.mjs";
4
+ //#region src/socket.ts
5
+ /**
6
+ * Storage's wire over a WebSocket: one connection carries every call, so a client pays for one HTTP
7
+ * request when it connects, not one a call. The frames are the wire's own (`@midsummer-ai/sdk/wire`),
8
+ * each sent with a request id, replied to in any order; `remote` batches over it as over HTTP.
9
+ *
10
+ * const files = storage(remote(socket("wss://api.<site>/storage/socket", { key, secret }), { batch: true }))
11
+ * // an app uses `client` (`@midsummer-ai/sdk/client`): this over one connection, with signed HTTP behind it
12
+ * // on the server, a session a connection:
13
+ * const session = socketSession({ engine, credentials })
14
+ * ws.send(session.greeting); ws.onmessage = async (m) => { const reply = await session.receive(m.data); if (reply) ws.send(reply) }
15
+ *
16
+ * A connection opens with a challenge: the server sends a random one, and the client replies with its
17
+ * Storage Key's id and the HMAC-SHA256 of the challenge under its secret, so the secret never travels.
18
+ * Then each message is a 4-byte little-endian request id followed by a frame, both ways. The key
19
+ * reaches what it would over S3: its own Buckets, by name, writing only if it may and while its
20
+ * Bucket's writes are not paused. A connection lives a minute, or 4,000 calls: the server then says it is closing,
21
+ * replies to what it was sent, and closes, and the client opens a new one for its next call, so a key
22
+ * revoked or a Bucket paused reaches every connection within a minute.
23
+ */
24
+ const encoder = new TextEncoder();
25
+ const SOCKET_LIFE_MS = 6e4;
26
+ const SOCKET_FRAMES = 4e3;
27
+ const SESSION_CONCURRENCY = 32;
28
+ const hex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
29
+ async function sign(secret, challenge) {
30
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
31
+ name: "HMAC",
32
+ hash: "SHA-256"
33
+ }, false, ["sign"]);
34
+ return hex(await crypto.subtle.sign("HMAC", key, encoder.encode(challenge)));
35
+ }
36
+ function sameHex(a, b) {
37
+ if (a.length !== b.length) return false;
38
+ let difference = 0;
39
+ for (let i = 0; i < a.length; i += 1) difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
40
+ return difference === 0;
41
+ }
42
+ /** The id and frame of a message, or a message from them. */
43
+ function tagged(id, frame) {
44
+ const message = new Uint8Array(4 + frame.length);
45
+ new DataView(message.buffer).setUint32(0, id, true);
46
+ message.set(frame, 4);
47
+ return message;
48
+ }
49
+ const CONNECT_MS = 1e4;
50
+ const FALLBACK_MS = 6e4;
51
+ const SENDS = 3;
52
+ const OPEN = 1;
53
+ /** A connection that never opened: nothing was sent on it, so the call may go another way. */
54
+ var Unreached = class extends StorageError {
55
+ constructor() {
56
+ super("unavailable", "Storage could not be reached");
57
+ }
58
+ };
59
+ /**
60
+ * A transport over WebSockets to `url` (`connections` of them, 16 when not given), each call sent on
61
+ * the one with the fewest calls in flight: one connection is one invocation on the server, so a few
62
+ * carry more at once. Each opens on its first call and again after it closes. A call in flight when its
63
+ * connection drops (a server going away on deploy closes every connection) is sent again on a new one
64
+ * when the wire says repeating it is safe (`repeatable`), and is otherwise `unavailable`, since it may
65
+ * have been carried out. Where no connection opens, or the runtime has no WebSocket, calls go over
66
+ * `fallback` for a minute, then a connection is tried again; without one they are `unavailable`.
67
+ */
68
+ function socket(url, options) {
69
+ const lines = Array.from({ length: options.connections ?? 16 }, () => line(url, options));
70
+ const { fallback } = options;
71
+ const clock = options.clock ?? Date.now;
72
+ const noSocket = !options.WebSocket && typeof globalThis.WebSocket !== "function";
73
+ let socketAfter = 0;
74
+ return {
75
+ async send(frame) {
76
+ if (fallback && (noSocket || clock() < socketAfter)) return fallback.send(frame);
77
+ if (noSocket) throw new Unreached();
78
+ try {
79
+ return await lines.reduce((least, one) => one.inFlight() < least.inFlight() ? one : least).send(frame);
80
+ } catch (error) {
81
+ if (!(error instanceof Unreached) || !fallback) throw error;
82
+ socketAfter = clock() + FALLBACK_MS;
83
+ return fallback.send(frame);
84
+ }
85
+ },
86
+ close() {
87
+ for (const one of lines) one.close();
88
+ }
89
+ };
90
+ }
91
+ /** One connection of a socket transport. */
92
+ function line(url, options) {
93
+ const Socket = options.WebSocket ?? globalThis.WebSocket;
94
+ const waiting = /* @__PURE__ */ new Map();
95
+ let open = null;
96
+ let next = 0;
97
+ function connect() {
98
+ if (open) return open;
99
+ let ws;
100
+ try {
101
+ ws = new Socket(url);
102
+ } catch {
103
+ return Promise.reject(new Unreached());
104
+ }
105
+ const opening = new Promise((ready, refused) => {
106
+ ws.binaryType = "arraybuffer";
107
+ const late = setTimeout(() => (closed(), ws.close(1e3, "too slow")), CONNECT_MS);
108
+ const lost = (error) => {
109
+ clearTimeout(late);
110
+ open = null;
111
+ refused(error);
112
+ ws.close(1e3, "refused");
113
+ };
114
+ ws.addEventListener("message", (event) => {
115
+ if (typeof event.data === "string") {
116
+ const said = JSON.parse(event.data);
117
+ if (said.challenge !== void 0) sign(options.secret, said.challenge).then((signature) => ws.send(JSON.stringify({
118
+ key: options.key,
119
+ signature
120
+ })));
121
+ else if (said.ready) clearTimeout(late), ready(ws);
122
+ else if (said.closing) open = null;
123
+ else lost(new StorageError("forbidden", said.refused ?? "Storage refused the key"));
124
+ return;
125
+ }
126
+ const message = new Uint8Array(event.data);
127
+ const id = new DataView(message.buffer, message.byteOffset).getUint32(0, true);
128
+ waiting.get(id)?.done(message.subarray(4));
129
+ waiting.delete(id);
130
+ });
131
+ const closed = () => {
132
+ clearTimeout(late);
133
+ if (open === opening) open = null;
134
+ refused(new Unreached());
135
+ for (const [id, call] of waiting) if (call.on === ws) waiting.delete(id), call.lost();
136
+ };
137
+ ws.addEventListener("close", closed);
138
+ ws.addEventListener("error", closed);
139
+ });
140
+ return open = opening;
141
+ }
142
+ return {
143
+ inFlight: () => waiting.size,
144
+ async send(frame) {
145
+ for (let sends = 0;;) {
146
+ const opening = connect();
147
+ const ws = await opening;
148
+ const id = next = next + 1 >>> 0;
149
+ const sent = ws.readyState === OPEN;
150
+ const answer = await new Promise((done) => {
151
+ waiting.set(id, {
152
+ on: ws,
153
+ done,
154
+ lost: () => done(null)
155
+ });
156
+ if (sent) ws.send(tagged(id, frame));
157
+ else waiting.delete(id), done(null);
158
+ });
159
+ if (answer) return answer;
160
+ if (!sent) {
161
+ if (open === opening) open = null;
162
+ continue;
163
+ }
164
+ sends += 1;
165
+ if (!repeatable(frame)) throw new StorageError("unavailable", "The connection to Storage closed before it answered; the call may have been carried out");
166
+ if (sends === SENDS) throw new StorageError("unavailable", "The connection to Storage closed before it answered");
167
+ }
168
+ },
169
+ close() {
170
+ open?.then((ws) => ws.close(1e3, "done"), () => {});
171
+ }
172
+ };
173
+ }
174
+ /**
175
+ * `engine` as a Storage Key reaches it: its Buckets by name (every Bucket, under its own name, when it
176
+ * names none), reading always, writing only with `write` access and while the Bucket's writes are not
177
+ * paused. Metering is the platform's, never a key's.
178
+ */
179
+ function keyed(engine, credential) {
180
+ const id = (name) => {
181
+ const map = credential.buckets;
182
+ if (!map) return name;
183
+ if (!Object.hasOwn(map, name)) throw new StorageError("forbidden", `The key may not use the Bucket ${name}`);
184
+ return map[name];
185
+ };
186
+ const writable = (name) => {
187
+ if (credential.access !== "write") throw new StorageError("forbidden", "The key may only read");
188
+ if (credential.paused?.includes(name)) throw new StorageError("quota", `Writes to the Bucket ${name} are paused: its Team reached its budget`);
189
+ return id(name);
190
+ };
191
+ const metering = async () => {
192
+ throw new StorageError("forbidden", "Metering is not answered to a key");
193
+ };
194
+ return {
195
+ put: (bucket, ...rest) => engine.put(writable(bucket), ...rest),
196
+ get: (bucket, ...rest) => engine.get(id(bucket), ...rest),
197
+ head: (bucket, ...rest) => engine.head(id(bucket), ...rest),
198
+ delete: (bucket, ...rest) => engine.delete(writable(bucket), ...rest),
199
+ list: (bucket, ...rest) => engine.list(id(bucket), ...rest),
200
+ copy: (bucket, ...rest) => engine.copy(writable(bucket), ...rest),
201
+ createUpload: (bucket, ...rest) => engine.createUpload(writable(bucket), ...rest),
202
+ uploadPart: (bucket, ...rest) => engine.uploadPart(writable(bucket), ...rest),
203
+ completeUpload: (bucket, ...rest) => engine.completeUpload(writable(bucket), ...rest),
204
+ abortUpload: (bucket, ...rest) => engine.abortUpload(writable(bucket), ...rest),
205
+ usage: metering,
206
+ operations: metering,
207
+ ...streamsOf(engine, id, writable)
208
+ };
209
+ }
210
+ /** A server's side of one connection: the handshake against `credentials`, then each frame served by `engine` as the key reaches it. */
211
+ function socketSession(options) {
212
+ const challenge = hex(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)).buffer);
213
+ let answer = null;
214
+ let closing = false, inFlight = 0, frames = 0, drained = null;
215
+ const turns = [];
216
+ let settle;
217
+ const settled = new Promise((resolve) => settle = resolve);
218
+ return {
219
+ greeting: JSON.stringify({ challenge }),
220
+ closing: JSON.stringify({ closing: true }),
221
+ life: SOCKET_LIFE_MS,
222
+ spent: () => frames >= SOCKET_FRAMES,
223
+ idle() {
224
+ closing = true;
225
+ return inFlight === 0 ? Promise.resolve() : new Promise((resolve) => drained = resolve);
226
+ },
227
+ settled,
228
+ async receive(message) {
229
+ if (typeof message === "string") {
230
+ if (answer) return null;
231
+ let said;
232
+ try {
233
+ said = JSON.parse(message);
234
+ } catch {
235
+ said = {};
236
+ }
237
+ const credential = typeof said.key === "string" ? await options.credentials(said.key) : null;
238
+ if (!credential || typeof said.signature !== "string" || !sameHex(await sign(credential.secret, challenge), said.signature)) {
239
+ settle("refused");
240
+ return JSON.stringify({ refused: "The key is unknown, or the signature does not match" });
241
+ }
242
+ answer = serve(keyed(options.engine, credential));
243
+ settle("ready");
244
+ return JSON.stringify({ ready: true });
245
+ }
246
+ if (!answer) return null;
247
+ const bytes = message instanceof Uint8Array ? message : message instanceof Blob ? new Uint8Array(await message.arrayBuffer()) : new Uint8Array(message);
248
+ const id = new DataView(bytes.buffer, bytes.byteOffset).getUint32(0, true);
249
+ inFlight += 1;
250
+ frames += 1;
251
+ if (inFlight > SESSION_CONCURRENCY) await new Promise((go) => turns.push(go));
252
+ try {
253
+ return tagged(id, await answer(bytes.subarray(4)));
254
+ } finally {
255
+ inFlight -= 1;
256
+ turns.shift()?.();
257
+ if (closing && inFlight === 0) drained?.();
258
+ }
259
+ }
260
+ };
261
+ }
262
+ //#endregion
263
+ export { keyed, sameHex, sign, socket, socketSession };
package/stand-in.d.mts ADDED
@@ -0,0 +1,7 @@
1
+ import { m as StorageEngine } from "./index-sgdyoA2P.mjs";
2
+ //#region src/stand-in.d.ts
3
+ declare function standIn(options: {
4
+ readonly clock: () => number;
5
+ }): StorageEngine;
6
+ //#endregion
7
+ export { standIn };
package/stand-in.mjs ADDED
@@ -0,0 +1,185 @@
1
+ import { f as resolveRange, h as StorageError, m as sha256, p as sameSizes, u as compareKeys } from "./names-DfCBKupJ.mjs";
2
+ //#region src/stand-in.ts
3
+ /**
4
+ * Storage's Stand-in: the Capability's behaviour in memory, for `midsummer dev` and tests. It holds
5
+ * no engine code and passes the contract (`@midsummer-ai/sdk/contract`) except durability and scale.
6
+ *
7
+ * const files = storage(standIn({ clock: Date.now }))
8
+ *
9
+ * A Bucket exists from its first put. Everything is lost when the process ends.
10
+ */
11
+ function standIn(options) {
12
+ const buckets = /* @__PURE__ */ new Map();
13
+ const calls = /* @__PURE__ */ new Map();
14
+ const count = (bucket, write) => {
15
+ let list = calls.get(bucket);
16
+ if (!list) calls.set(bucket, list = []);
17
+ list.push({
18
+ at: options.clock(),
19
+ write
20
+ });
21
+ };
22
+ const uploads = /* @__PURE__ */ new Map();
23
+ const uploadOf = (bucket, key, uploadId) => {
24
+ const upload = uploads.get(uploadId);
25
+ if (!upload || upload.bucket !== bucket || upload.key !== key) throw new StorageError("not_found", "no such upload, or it is finished", "upload");
26
+ return upload;
27
+ };
28
+ const bucketOf = (name) => {
29
+ let bucket = buckets.get(name);
30
+ if (!bucket) buckets.set(name, bucket = /* @__PURE__ */ new Map());
31
+ return bucket;
32
+ };
33
+ const read = (held, range) => {
34
+ if (!range) return {
35
+ ...held.object,
36
+ body: held.body.slice()
37
+ };
38
+ const at = resolveRange(range, held.object.size);
39
+ return {
40
+ ...held.object,
41
+ body: held.body.slice(at.offset, at.offset + at.length),
42
+ range: at
43
+ };
44
+ };
45
+ const engine = {
46
+ async put(bucket, key, body, put) {
47
+ count(bucket, true);
48
+ const current = buckets.get(bucket)?.get(key);
49
+ if (put.ifNoneMatch && current) throw new StorageError("conflict", `"${key}" already holds an Object`);
50
+ if (put.ifMatch !== void 0 && current?.object.etag !== put.ifMatch) throw new StorageError("conflict", `"${key}" does not hold the etag ${put.ifMatch}`);
51
+ const object = {
52
+ key,
53
+ size: body.length,
54
+ etag: await sha256(body),
55
+ contentType: put.contentType,
56
+ metadata: { ...put.metadata },
57
+ uploadedAt: options.clock()
58
+ };
59
+ bucketOf(bucket).set(key, {
60
+ object,
61
+ body: body.slice()
62
+ });
63
+ return object;
64
+ },
65
+ async get(bucket, key, range, limit) {
66
+ count(bucket, false);
67
+ const held = buckets.get(bucket)?.get(key);
68
+ if (!held) return null;
69
+ return read(held, range ?? (limit !== void 0 && held.body.length > limit ? {
70
+ offset: 0,
71
+ length: limit
72
+ } : void 0));
73
+ },
74
+ async head(bucket, key) {
75
+ count(bucket, false);
76
+ return buckets.get(bucket)?.get(key)?.object ?? null;
77
+ },
78
+ async delete(bucket, key) {
79
+ count(bucket, true);
80
+ buckets.get(bucket)?.delete(key);
81
+ },
82
+ async list(bucket, { prefix, delimiter, after, limit }) {
83
+ count(bucket, false);
84
+ const keys = [...buckets.get(bucket)?.keys() ?? []].filter((key) => key.startsWith(prefix)).sort(compareKeys);
85
+ const objects = [], prefixes = [];
86
+ let last;
87
+ for (const key of keys) {
88
+ if (after !== void 0 && (compareKeys(key, after) <= 0 || delimiter !== void 0 && after.endsWith(delimiter) && key.startsWith(after))) continue;
89
+ const cut = delimiter === void 0 ? -1 : key.indexOf(delimiter, prefix.length);
90
+ const folded = cut === -1 ? void 0 : key.slice(0, cut + delimiter.length);
91
+ if (folded !== void 0 && folded === prefixes.at(-1)) continue;
92
+ if (objects.length + prefixes.length === limit) return {
93
+ objects,
94
+ prefixes,
95
+ next: last
96
+ };
97
+ if (folded !== void 0) prefixes.push(last = folded);
98
+ else objects.push(buckets.get(bucket).get(last = key).object);
99
+ }
100
+ return {
101
+ objects,
102
+ prefixes
103
+ };
104
+ },
105
+ async copy(bucket, from, to, copy) {
106
+ count(bucket, true);
107
+ const source = buckets.get(bucket)?.get(from);
108
+ if (!source) throw new StorageError("not_found", `"${from}" holds nothing`);
109
+ if (copy.ifNoneMatch && buckets.get(bucket)?.has(to)) throw new StorageError("conflict", `"${to}" already holds an Object`);
110
+ const object = {
111
+ ...source.object,
112
+ key: to,
113
+ uploadedAt: options.clock()
114
+ };
115
+ bucketOf(bucket).set(to, {
116
+ object,
117
+ body: source.body
118
+ });
119
+ return object;
120
+ },
121
+ async createUpload(bucket, key, upload) {
122
+ count(bucket, true);
123
+ const uploadId = crypto.randomUUID();
124
+ uploads.set(uploadId, {
125
+ bucket,
126
+ key,
127
+ options: upload,
128
+ parts: /* @__PURE__ */ new Map()
129
+ });
130
+ return { uploadId };
131
+ },
132
+ async uploadPart(bucket, key, uploadId, partNumber, body) {
133
+ count(bucket, true);
134
+ const upload = uploadOf(bucket, key, uploadId);
135
+ const part = {
136
+ partNumber,
137
+ etag: await sha256(body),
138
+ size: body.length
139
+ };
140
+ upload.parts.set(partNumber, {
141
+ part,
142
+ body: body.slice()
143
+ });
144
+ return part;
145
+ },
146
+ async completeUpload(bucket, key, uploadId, named) {
147
+ const upload = uploadOf(bucket, key, uploadId);
148
+ const parts = named.map(({ partNumber, etag }, i) => {
149
+ const sent = upload.parts.get(partNumber);
150
+ if (!sent || sent.part.etag !== etag) throw new StorageError("invalid", `part ${partNumber} was not sent with that etag`, "part");
151
+ if (i < named.length - 1 && sent.part.size < 5242880) throw new StorageError("invalid", `part ${partNumber} is smaller than 5 MiB and is not the last`, "part-too-small");
152
+ return sent.body;
153
+ });
154
+ sameSizes(parts.map((p) => p.length), named);
155
+ const whole = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
156
+ let at = 0;
157
+ for (const p of parts) whole.set(p, at), at += p.length;
158
+ const object = await engine.put(bucket, key, whole, upload.options);
159
+ uploads.delete(uploadId);
160
+ return object;
161
+ },
162
+ async abortUpload(bucket, key, uploadId) {
163
+ count(bucket, true);
164
+ uploadOf(bucket, key, uploadId);
165
+ uploads.delete(uploadId);
166
+ },
167
+ async operations(bucket, from, to) {
168
+ const window = (calls.get(bucket) ?? []).filter((call) => call.at >= from && call.at < to);
169
+ return {
170
+ reads: window.filter((call) => !call.write).length,
171
+ writes: window.filter((call) => call.write).length
172
+ };
173
+ },
174
+ async usage(bucket) {
175
+ const held = [...buckets.get(bucket)?.values() ?? []];
176
+ return {
177
+ objects: held.length,
178
+ bytes: held.reduce((sum, { object }) => sum + object.size, 0)
179
+ };
180
+ }
181
+ };
182
+ return engine;
183
+ }
184
+ //#endregion
185
+ export { standIn };
@@ -0,0 +1,237 @@
1
+ import "./connection-CDk7H1FL.mjs";
2
+ //#region src/database/client/store.mjs
3
+ const STORES = [
4
+ "ops",
5
+ "views",
6
+ "meta"
7
+ ];
8
+ /** A store in IndexedDB database `db:<name>`. */
9
+ function idbStore(name, options) {
10
+ return new ConnectionStore(idbBackend(name), options);
11
+ }
12
+ var ConnectionStore = class {
13
+ /** `backend`: `{ load() -> { ops: [record], views: [[key, record]], meta: { k: v } }, write(batch) -> Promise }`. */
14
+ constructor(backend, { delay = 50, views = 2e3, onResumed = null } = {}) {
15
+ this.backend = backend;
16
+ this.delay = delay;
17
+ this.maxViews = views;
18
+ this.onResumed = onResumed;
19
+ this.ops = /* @__PURE__ */ new Map();
20
+ this.views = /* @__PURE__ */ new Map();
21
+ this.rows = /* @__PURE__ */ new Map();
22
+ this.seq = 0;
23
+ this.dirV = -1;
24
+ this.resumed = [];
25
+ this.batch = null;
26
+ this.timer = null;
27
+ this.writing = Promise.resolve();
28
+ }
29
+ /** Called by Connection.connect before the socket opens: loads, routes by the stored map, queues the stored ops. */
30
+ async attach(g) {
31
+ this.g = g;
32
+ const { ops, views, meta } = await this.backend.load();
33
+ if (meta.dir) {
34
+ g.setDir(meta.dir);
35
+ this.dirV = meta.dir.v;
36
+ }
37
+ if (meta.esc && !g.esc) g.esc = meta.esc;
38
+ for (const [key, rec] of views) {
39
+ this.views.set(key, rec);
40
+ this.indexRow(rec);
41
+ }
42
+ ops.sort((x, y) => x.seq - y.seq);
43
+ for (const rec of ops) {
44
+ this.seq = Math.max(this.seq, rec.seq);
45
+ if (rec.at && Date.now() - rec.at > 864e5) {
46
+ this.del("ops", rec.t);
47
+ const answer = Promise.resolve({
48
+ ok: false,
49
+ why: "expired",
50
+ t: rec.t,
51
+ sentAt: rec.at
52
+ });
53
+ this.resumed.push({
54
+ op: rec.op,
55
+ answer
56
+ });
57
+ if (this.onResumed) this.onResumed(rec.op, answer);
58
+ continue;
59
+ }
60
+ this.ops.set(rec.t, rec);
61
+ const answer = g.send(g.unitOf(rec.route ?? ""), { ...rec.op }, true, null, rec.route);
62
+ this.resumed.push({
63
+ op: rec.op,
64
+ answer
65
+ });
66
+ if (this.onResumed) this.onResumed(rec.op, answer);
67
+ }
68
+ if (typeof addEventListener === "function" && typeof document !== "undefined") {
69
+ addEventListener("pagehide", () => this.flush());
70
+ document.addEventListener("visibilitychange", () => {
71
+ if (document.visibilityState === "hidden") this.flush();
72
+ });
73
+ }
74
+ }
75
+ /** A tagged op that must land was issued (again: ignored). */
76
+ issued(op, route) {
77
+ if (this.ops.has(op.t)) return;
78
+ const { i: _i, ...plain } = op;
79
+ const rec = {
80
+ t: op.t,
81
+ seq: this.seq += 1,
82
+ op: plain,
83
+ route
84
+ };
85
+ this.ops.set(op.t, rec);
86
+ this.put("ops", op.t, rec);
87
+ }
88
+ /** First put on the wire: from now on its outcome is in doubt until answered. */
89
+ sent(op) {
90
+ const rec = this.ops.get(op.t);
91
+ if (rec && !rec.at) {
92
+ rec.at = Date.now();
93
+ this.put("ops", op.t, rec);
94
+ }
95
+ }
96
+ /** Its answer: kept while it waits for its durable receipt (`pend`), else done. */
97
+ answered(op, a) {
98
+ if (!a.pend) this.drop(op.t);
99
+ }
100
+ /** Its durable receipt. */
101
+ durable(op) {
102
+ if (op.t) this.drop(op.t);
103
+ }
104
+ drop(t) {
105
+ if (this.ops.delete(t)) this.del("ops", t);
106
+ }
107
+ /** A watch's new value (without any optimistic overlay). */
108
+ saw(w) {
109
+ const view = w.q.k === "g" ? w.state : this.g.view(w);
110
+ if (view === void 0) return;
111
+ const rec = {
112
+ q: w.q,
113
+ view
114
+ };
115
+ this.views.delete(w.key);
116
+ this.views.set(w.key, rec);
117
+ this.indexRow(rec);
118
+ this.put("views", w.key, rec);
119
+ while (this.views.size > this.maxViews) {
120
+ const old = this.views.keys().next().value;
121
+ this.views.delete(old);
122
+ this.del("views", old);
123
+ }
124
+ }
125
+ /** The stored value of a watch (a row view marked stale), or undefined. */
126
+ cached(w) {
127
+ const rec = this.views.get(w.key);
128
+ if (!rec) return void 0;
129
+ return w.q.k === "g" ? {
130
+ ...rec.view,
131
+ stale: true
132
+ } : rec.view;
133
+ }
134
+ /** A row's last stored value `{ v, value }` (value null: gone), or undefined. */
135
+ row(key) {
136
+ return this.rows.get(key);
137
+ }
138
+ indexRow(rec) {
139
+ if (rec.q.k === "g") this.rows.set(`${rec.q.tb ?? "rows"}/${rec.q.id}`, rec.view.gone ? {
140
+ v: 0,
141
+ value: null
142
+ } : {
143
+ v: rec.view.v,
144
+ value: rec.view.value
145
+ });
146
+ }
147
+ put(store, key, value) {
148
+ const b = this.open(store);
149
+ b.del.delete(key);
150
+ b.put.set(key, value);
151
+ }
152
+ del(store, key) {
153
+ const b = this.open(store);
154
+ b.put.delete(key);
155
+ b.del.add(key);
156
+ }
157
+ open(store) {
158
+ this.batch ??= Object.fromEntries(STORES.map((s) => [s, {
159
+ put: /* @__PURE__ */ new Map(),
160
+ del: /* @__PURE__ */ new Set()
161
+ }]));
162
+ if (!this.timer) {
163
+ this.timer = setTimeout(() => this.flush(), this.delay);
164
+ this.timer.unref?.();
165
+ }
166
+ return this.batch[store];
167
+ }
168
+ /** Writes what changed in one transaction; resolves when it (and every write before it) is stored. */
169
+ flush() {
170
+ clearTimeout(this.timer);
171
+ this.timer = null;
172
+ const g = this.g;
173
+ if (g?.dir && g.dir.v > this.dirV) {
174
+ this.dirV = g.dir.v;
175
+ this.put("meta", "dir", g.dir);
176
+ if (g.esc) this.put("meta", "esc", g.esc);
177
+ clearTimeout(this.timer);
178
+ this.timer = null;
179
+ }
180
+ const batch = this.batch;
181
+ this.batch = null;
182
+ if (batch) this.writing = this.writing.then(() => this.backend.write(batch)).catch((e) => {
183
+ this.error = e;
184
+ });
185
+ return this.writing;
186
+ }
187
+ /** Flushes and stops (the Connection keeps running without it). */
188
+ async close() {
189
+ await this.flush();
190
+ this.backend.close?.();
191
+ }
192
+ };
193
+ const req = (r) => new Promise((ok, fail) => {
194
+ r.onsuccess = () => ok(r.result);
195
+ r.onerror = () => fail(r.error);
196
+ });
197
+ function idbBackend(name) {
198
+ let opened = null;
199
+ const db = () => opened ??= new Promise((ok, fail) => {
200
+ const r = indexedDB.open(`db:${name}`, 1);
201
+ r.onupgradeneeded = () => {
202
+ for (const s of STORES) if (!r.result.objectStoreNames.contains(s)) r.result.createObjectStore(s);
203
+ };
204
+ r.onsuccess = () => ok(r.result);
205
+ r.onerror = () => fail(r.error);
206
+ });
207
+ return {
208
+ async load() {
209
+ const t = (await db()).transaction(STORES, "readonly");
210
+ const all = (s) => Promise.all([req(t.objectStore(s).getAllKeys()), req(t.objectStore(s).getAll())]).then(([k, v]) => k.map((key, n) => [key, v[n]]));
211
+ const [ops, views, meta] = await Promise.all(STORES.map(all));
212
+ return {
213
+ ops: ops.map(([, v]) => v),
214
+ views,
215
+ meta: Object.fromEntries(meta)
216
+ };
217
+ },
218
+ async write(batch) {
219
+ const t = (await db()).transaction(STORES, "readwrite");
220
+ for (const s of STORES) {
221
+ const os = t.objectStore(s);
222
+ for (const [k, v] of batch[s].put) os.put(v, k);
223
+ for (const k of batch[s].del) os.delete(k);
224
+ }
225
+ await new Promise((ok, fail) => {
226
+ t.oncomplete = ok;
227
+ t.onerror = t.onabort = () => fail(t.error);
228
+ });
229
+ },
230
+ close() {
231
+ opened?.then((d) => d.close(), () => {});
232
+ opened = null;
233
+ }
234
+ };
235
+ }
236
+ //#endregion
237
+ export { idbStore };