@cimplify/cli 0.15.2 → 0.16.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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -2
  3. package/dist/add-SXC6RUPQ.mjs +140 -0
  4. package/dist/{add-KRIIKAS4.mjs → add-XQVQLD3F.mjs} +4 -4
  5. package/dist/ai-NQH5L4I6.mjs +17 -0
  6. package/dist/{assets-CWB72PZG.mjs → assets-FZGVOQXH.mjs} +12 -7
  7. package/dist/{auth-step-up-Z6KOTTV7.mjs → auth-step-up-67LCPDNL.mjs} +5 -5
  8. package/dist/build-J4ZUIOPX.mjs +625 -0
  9. package/dist/{cancel-ZHVVILOU.mjs → cancel-VGJHZJ4J.mjs} +4 -4
  10. package/dist/capture-RAJ2B3BH.mjs +69 -0
  11. package/dist/chunk-25ZPFHLF.mjs +51 -0
  12. package/dist/chunk-47ZXSY47.mjs +52 -0
  13. package/dist/{chunk-NK6VC264.mjs → chunk-54J3XR4T.mjs} +2 -2
  14. package/dist/chunk-5HP2INMY.mjs +10631 -0
  15. package/dist/chunk-7JR6JCFF.mjs +32 -0
  16. package/dist/chunk-7PQL7E7K.mjs +68 -0
  17. package/dist/chunk-AKACUJ3W.mjs +81 -0
  18. package/dist/chunk-BYNE553A.mjs +2945 -0
  19. package/dist/{chunk-NYXA6X56.mjs → chunk-CLQYPTJ2.mjs} +31 -11
  20. package/dist/chunk-CNZAOVSK.mjs +104 -0
  21. package/dist/chunk-CPBESPWK.mjs +5576 -0
  22. package/dist/{chunk-GRR5U2M5.mjs → chunk-DYWUPHV3.mjs} +11 -7
  23. package/dist/chunk-EAV2P4N3.mjs +1941 -0
  24. package/dist/{chunk-KN2YSKXZ.mjs → chunk-EDZSOL5O.mjs} +14 -6
  25. package/dist/chunk-EGQIPYUN.mjs +696 -0
  26. package/dist/{chunk-DMLQBAGC.mjs → chunk-EOO4UOQD.mjs} +1 -1
  27. package/dist/chunk-EYEMPIMP.mjs +74 -0
  28. package/dist/chunk-G2ZEFSQJ.mjs +46 -0
  29. package/dist/{chunk-TH4XENKH.mjs → chunk-I4HNHPEM.mjs} +1 -1
  30. package/dist/chunk-JEVSFNPT.mjs +363 -0
  31. package/dist/chunk-JKECUAAV.mjs +2169 -0
  32. package/dist/chunk-KMHORI4Y.mjs +638 -0
  33. package/dist/chunk-KNPYBZ6B.mjs +502 -0
  34. package/dist/{chunk-3QUK5BPY.mjs → chunk-KOD4GBQ5.mjs} +8 -1
  35. package/dist/chunk-KQNZKV6E.mjs +535 -0
  36. package/dist/{chunk-MXYUAJEW.mjs → chunk-LLRUOIMU.mjs} +5 -2
  37. package/dist/{chunk-F2UI225N.mjs → chunk-LNMITJ4Z.mjs} +6 -6
  38. package/dist/chunk-MEM3MDV4.mjs +6463 -0
  39. package/dist/chunk-NY4YT7O6.mjs +84 -0
  40. package/dist/{chunk-A6WHBSXZ.mjs → chunk-Q6CF4C3E.mjs} +15 -13
  41. package/dist/chunk-RP66A52P.mjs +86 -0
  42. package/dist/{chunk-I2UP7YTG.mjs → chunk-SAXGWS2K.mjs} +1 -1
  43. package/dist/chunk-SCKQ4XSD.mjs +41 -0
  44. package/dist/chunk-SF44L2OD.mjs +1152 -0
  45. package/dist/chunk-SGT5IJVW.mjs +28 -0
  46. package/dist/{chunk-WEAB6HHQ.mjs → chunk-SS4JTMIV.mjs} +8 -8
  47. package/dist/chunk-T4MW3PD4.mjs +1456 -0
  48. package/dist/chunk-THATLMEX.mjs +1227 -0
  49. package/dist/{chunk-MLE4MVYC.mjs → chunk-TT4YI33G.mjs} +2 -2
  50. package/dist/chunk-TYV2SVFN.mjs +101 -0
  51. package/dist/chunk-VHK7WBO3.mjs +189 -0
  52. package/dist/chunk-VSA44UKR.mjs +391 -0
  53. package/dist/chunk-XL54SQJ3.mjs +42 -0
  54. package/dist/{chunk-5KYR6WJB.mjs → chunk-XQAHREKO.mjs} +17 -1
  55. package/dist/chunk-ZUCDXRFI.mjs +2011 -0
  56. package/dist/create-4S2EAUR3.mjs +10 -0
  57. package/dist/credits-NJOAOPPW.mjs +33 -0
  58. package/dist/{deploy-JE6TUYCL.mjs → deploy-26Y4UGZZ.mjs} +14 -14
  59. package/dist/dev-MOYURRWQ.mjs +401 -0
  60. package/dist/dev-WL43Q7VD.mjs +8 -0
  61. package/dist/dispatcher.mjs +131 -30
  62. package/dist/{doctor-HFB4IBYK.mjs → doctor-E6ETD2GJ.mjs} +12 -19
  63. package/dist/{domains-BZKGIDF4.mjs → domains-LOTQMVOR.mjs} +6 -6
  64. package/dist/engine-runtime-fetch-CIE7S7PB.mjs +10 -0
  65. package/dist/{env-FZNXKLGL.mjs → env-3PDFC5XF.mjs} +7 -7
  66. package/dist/env-MOTYOPM5.mjs +19 -0
  67. package/dist/explain-K5QEVWCD.mjs +251 -0
  68. package/dist/{inspect-X3M7WSTL.mjs → inspect-Q3E2DHYY.mjs} +3 -3
  69. package/dist/inspect-SJ4WBAXO.mjs +86 -0
  70. package/dist/introspect-7F34YN5V.mjs +9 -0
  71. package/dist/kernel-DEGWMHFL.mjs +662 -0
  72. package/dist/kernel-plugin.js +6862 -0
  73. package/dist/keys-W5SN3DPK.mjs +71 -0
  74. package/dist/lines-XGJYTQ7P.mjs +56 -0
  75. package/dist/{link-4NM2QMI5.mjs → link-NKUAVNM2.mjs} +6 -5
  76. package/dist/{lint-DDZ7HUQN.mjs → lint-G4YPQSSO.mjs} +2 -2
  77. package/dist/{list-2MFOQKKQ.mjs → list-TDR7K2JB.mjs} +3 -3
  78. package/dist/lock-EJPW2LM3.mjs +57 -0
  79. package/dist/{login-RLMDEWAL.mjs → login-AYLU7JU7.mjs} +10 -8
  80. package/dist/{logout-Z57X6HPE.mjs → logout-3M6VMHLH.mjs} +2 -2
  81. package/dist/{logs-36BLYJHJ.mjs → logs-4EL5SOXN.mjs} +5 -5
  82. package/dist/logs-4QQGG2XY.mjs +44 -0
  83. package/dist/mcp-4R2R6E5M.mjs +1077 -0
  84. package/dist/{mock-EVER34IX.mjs → mock-AK24GSP2.mjs} +2 -2
  85. package/dist/outdated-JEKCVLTG.mjs +58 -0
  86. package/dist/port-SBZ7X3EH.mjs +2408 -0
  87. package/dist/{projects-4XRNHJAH.mjs → projects-7JKTC5WJ.mjs} +9 -7
  88. package/dist/publish-QQCX47HK.mjs +20 -0
  89. package/dist/remix-NLLYD3BI.mjs +125 -0
  90. package/dist/repo-Z4G4EW3N.mjs +9 -0
  91. package/dist/report-M7F4ZYP7.mjs +2 -0
  92. package/dist/rollback-2WQ2CW7T.mjs +43 -0
  93. package/dist/{rollback-WCRWXSE3.mjs → rollback-ZGU32HGT.mjs} +6 -6
  94. package/dist/scene-HXARHUM7.mjs +324 -0
  95. package/dist/{status-VA326DWD.mjs → status-C3ZSLZ7Q.mjs} +5 -5
  96. package/dist/status-WEQOC25K.mjs +18 -0
  97. package/dist/test-ZPBWV4VX.mjs +233 -0
  98. package/dist/testers-A3LMXOJJ.mjs +49 -0
  99. package/dist/{unlink-H3BO3DOQ.mjs → unlink-4G4FE4RJ.mjs} +2 -2
  100. package/dist/{update-YYOTN7MG.mjs → update-GHKKM5FS.mjs} +4 -4
  101. package/dist/{whoami-CQPPARRS.mjs → whoami-3QQN6G2A.mjs} +3 -3
  102. package/dist/workshop-CD74D2LK.mjs +1703 -0
  103. package/package.json +11 -7
  104. package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -1
  105. package/templates/storefront-auto/package.json +1 -1
  106. package/templates/storefront-bakery/package.json +1 -1
  107. package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -1
  108. package/templates/storefront-fashion/package.json +1 -1
  109. package/templates/storefront-grocery/package.json +1 -1
  110. package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -1
  111. package/templates/storefront-pharmacy/package.json +1 -1
  112. package/templates/storefront-restaurant/package.json +1 -1
  113. package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -1
  114. package/templates/storefront-retail/package.json +1 -1
  115. package/templates/storefront-services/package.json +1 -1
  116. package/dist/chunk-ARIDYKLO.mjs +0 -6463
  117. package/dist/dev-V7JSUVOI.mjs +0 -8
  118. package/dist/explain-O5AC73EE.mjs +0 -251
  119. package/dist/introspect-2NGESQZE.mjs +0 -8
  120. package/dist/repo-X3KZ5R64.mjs +0 -9
@@ -0,0 +1,1152 @@
1
+ #!/usr/bin/env node
2
+ import { packageData } from './chunk-CPBESPWK.mjs';
3
+ import { createHash } from 'crypto';
4
+ import { existsSync, readdirSync, promises, mkdirSync, statSync } from 'fs';
5
+ import path from 'path';
6
+
7
+ var DEV_DATA_ROUTE = "/_ctx/data";
8
+ var LOCAL_ENVIRONMENT = "local";
9
+ var DATA_DIR = [".cimplify", "dev", "data"];
10
+ var DATA_FILE = "data.sqlite";
11
+ var LOCAL_PARTICIPANT = "local-player";
12
+ var DATA_FAMILIES = /* @__PURE__ */ new Set(["db", "tx", "kv", "cache", "counter", "queue", "search", "analytics"]);
13
+ var OWNER_COLUMN = "ix_owner";
14
+ var RESERVED_PREFIX = "ix_";
15
+ var META_DB_KEY = "db";
16
+ var META_STAMP_KEY = "stamp";
17
+ var DEFAULT_PAGE_LIMIT = 50;
18
+ var MAX_PAGE_LIMIT = 100;
19
+ var MAX_NAME_BYTES = 64;
20
+ var MAX_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
21
+ var MAX_DB_PARAMETERS = 64;
22
+ var KV_KEYS_LIMIT = 1e6;
23
+ var KV_BYTES_LIMIT = 256 * 1024 * 1024;
24
+ var DB_ROWS_LIMIT = 1e7;
25
+ var STORAGE_LIMIT = 1024 * 1024 * 1024;
26
+ var CACHE_MAX_ENTRY_BYTES = 64 * 1024;
27
+ var QUEUE_DEPTH_LIMIT = 1e5;
28
+ var SEARCH_DOCUMENTS_LIMIT = 1e6;
29
+ var TX_HOLD_MS = 3e4;
30
+ var NAMESPACE_HEX = 16;
31
+ var SLOT_HEX = 4;
32
+ var UINT32 = 4294967296;
33
+ var TTL_SEPARATOR = ":";
34
+ var KV_KIND = "k";
35
+ var CACHE_KIND = "c";
36
+ var COUNTER_KINDS = { sum: "n", rate_limit: "r", ranked_set: "z" };
37
+ var SCOPE_CHARS = { personal: "p", interactive: "i", shared: "s" };
38
+ var SCOPES_BY_CHAR = { p: "personal", i: "interactive", s: "shared" };
39
+ var SQL_TYPES = {
40
+ integer: "INTEGER",
41
+ real: "REAL",
42
+ boolean: "INTEGER",
43
+ timestamp: "INTEGER",
44
+ text: "TEXT",
45
+ json: "TEXT",
46
+ blob_ref: "TEXT"
47
+ };
48
+ var COMPARISONS = { eq: " = ", ne: " <> ", lt: " < ", le: " <= ", gt: " > ", ge: " >= " };
49
+ var RESERVED_WORDS = /* @__PURE__ */ new Set([
50
+ "all",
51
+ "alter",
52
+ "and",
53
+ "any",
54
+ "as",
55
+ "asc",
56
+ "between",
57
+ "by",
58
+ "case",
59
+ "check",
60
+ "column",
61
+ "create",
62
+ "default",
63
+ "delete",
64
+ "desc",
65
+ "distinct",
66
+ "drop",
67
+ "else",
68
+ "end",
69
+ "exists",
70
+ "false",
71
+ "from",
72
+ "full",
73
+ "group",
74
+ "having",
75
+ "in",
76
+ "index",
77
+ "inner",
78
+ "insert",
79
+ "into",
80
+ "is",
81
+ "join",
82
+ "key",
83
+ "left",
84
+ "like",
85
+ "limit",
86
+ "not",
87
+ "null",
88
+ "offset",
89
+ "on",
90
+ "or",
91
+ "order",
92
+ "outer",
93
+ "primary",
94
+ "right",
95
+ "select",
96
+ "set",
97
+ "settings",
98
+ "table",
99
+ "then",
100
+ "true",
101
+ "union",
102
+ "unique",
103
+ "update",
104
+ "user",
105
+ "using",
106
+ "values",
107
+ "when",
108
+ "where",
109
+ "with"
110
+ ]);
111
+ var PLATFORM_TABLES = [
112
+ "CREATE TABLE IF NOT EXISTS ix_meta (key TEXT PRIMARY KEY, version INTEGER, schema TEXT)",
113
+ "CREATE TABLE IF NOT EXISTS ix_kv (ns TEXT NOT NULL, part TEXT NOT NULL, k TEXT NOT NULL, value TEXT, stamp INTEGER NOT NULL, expires_at INTEGER, PRIMARY KEY (ns, part, k))",
114
+ "CREATE TABLE IF NOT EXISTS ix_cache (ns TEXT NOT NULL, part TEXT NOT NULL, k TEXT NOT NULL, value TEXT, expires_at INTEGER NOT NULL, PRIMARY KEY (ns, part, k))",
115
+ "CREATE TABLE IF NOT EXISTS ix_counters (id TEXT NOT NULL, part TEXT NOT NULL, value INTEGER NOT NULL, PRIMARY KEY (id, part))",
116
+ "CREATE TABLE IF NOT EXISTS ix_windows (id TEXT NOT NULL, part TEXT NOT NULL, k TEXT NOT NULL, started INTEGER NOT NULL, used INTEGER NOT NULL, PRIMARY KEY (id, part, k))",
117
+ "CREATE TABLE IF NOT EXISTS ix_ranks (id TEXT NOT NULL, part TEXT NOT NULL, member TEXT NOT NULL, score REAL NOT NULL, PRIMARY KEY (id, part, member))",
118
+ "CREATE TABLE IF NOT EXISTS ix_jobs (job_id TEXT PRIMARY KEY, queue TEXT NOT NULL, entry TEXT NOT NULL, payload TEXT, status TEXT NOT NULL, attempt INTEGER NOT NULL, next_run_at INTEGER)",
119
+ "CREATE TABLE IF NOT EXISTS ix_search (idx TEXT NOT NULL, part TEXT NOT NULL, doc TEXT NOT NULL, classification TEXT NOT NULL, body TEXT, fields TEXT, PRIMARY KEY (idx, part, doc))",
120
+ "CREATE TABLE IF NOT EXISTS ix_topics (topic TEXT NOT NULL, sequence INTEGER NOT NULL, message TEXT, PRIMARY KEY (topic, sequence))",
121
+ "CREATE TABLE IF NOT EXISTS ix_events (name TEXT NOT NULL, at INTEGER NOT NULL, properties TEXT)",
122
+ "CREATE TABLE IF NOT EXISTS ix_metrics (name TEXT NOT NULL, kind TEXT NOT NULL, at INTEGER NOT NULL, value REAL NOT NULL)"
123
+ ];
124
+ var Refused = class extends Error {
125
+ constructor(error) {
126
+ super(error.code);
127
+ this.error = error;
128
+ }
129
+ error;
130
+ };
131
+ function refuse(error) {
132
+ throw new Refused(error);
133
+ }
134
+ function invalid(field, problem) {
135
+ refuse({ code: "invalid_arguments", field, problem });
136
+ }
137
+ function denied(reason) {
138
+ refuse({ code: "denied", reason });
139
+ }
140
+ function notFound(resource) {
141
+ refuse({ code: "not_found", resource });
142
+ }
143
+ function isIdentifier(name2) {
144
+ return /^[a-z][a-z0-9_]{0,62}$/.test(name2) && !name2.startsWith(RESERVED_PREFIX) && !RESERVED_WORDS.has(name2);
145
+ }
146
+ function hex(name2) {
147
+ return createHash("sha256").update(name2).digest("hex").slice(0, NAMESPACE_HEX);
148
+ }
149
+ function stampOf(n) {
150
+ return { high: Math.floor(n / UINT32), low: n % UINT32 };
151
+ }
152
+ function stampNumber(stamp) {
153
+ if (stamp === null || stamp === void 0 || typeof stamp !== "object") return null;
154
+ const { high, low } = stamp;
155
+ if (typeof high !== "number" || typeof low !== "number") return null;
156
+ return high * UINT32 + low;
157
+ }
158
+ function sessionToken(n) {
159
+ return n.toString(16).padStart(16, "0");
160
+ }
161
+ function consistency(n) {
162
+ return { stamp: stampOf(n), session: sessionToken(n) };
163
+ }
164
+ function pageLimit(page) {
165
+ const limit = page?.limit;
166
+ if (limit === null || limit === void 0) return DEFAULT_PAGE_LIMIT;
167
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) invalid("page.limit", "out_of_range");
168
+ return limit;
169
+ }
170
+ function pageOffset(page) {
171
+ const cursor = page?.cursor;
172
+ if (cursor === null || cursor === void 0) return 0;
173
+ const offset = Number(cursor);
174
+ if (!Number.isInteger(offset) || offset < 0) invalid("page.cursor", "malformed");
175
+ return offset;
176
+ }
177
+ function name(value, field) {
178
+ if (typeof value !== "string" || value.length === 0 || Buffer.byteLength(value) > MAX_NAME_BYTES) invalid(field, "out_of_range");
179
+ return value;
180
+ }
181
+ function text(value, field) {
182
+ if (typeof value !== "string") invalid(field, "malformed");
183
+ return value;
184
+ }
185
+ function encode(value) {
186
+ return JSON.stringify(value ?? null);
187
+ }
188
+ function decode(raw) {
189
+ return typeof raw === "string" ? JSON.parse(raw) : null;
190
+ }
191
+ function sqlValue(value, field) {
192
+ switch (value.type) {
193
+ case "null":
194
+ return null;
195
+ case "integer":
196
+ case "real":
197
+ case "timestamp":
198
+ if (typeof value.value !== "number") invalid(field, "malformed");
199
+ return value.value;
200
+ case "boolean":
201
+ return value.value === true ? 1 : 0;
202
+ case "text":
203
+ case "blob":
204
+ return text(value.value, field);
205
+ case "json":
206
+ return encode(value.value);
207
+ default:
208
+ return invalid(field, "malformed");
209
+ }
210
+ }
211
+ function cell(raw, columnType) {
212
+ if (raw === null || raw === void 0) return { type: "null" };
213
+ switch (columnType) {
214
+ case "integer":
215
+ return { type: "integer", value: Number(raw) };
216
+ case "real":
217
+ return { type: "real", value: Number(raw) };
218
+ case "boolean":
219
+ return { type: "boolean", value: Number(raw) !== 0 };
220
+ case "timestamp":
221
+ return { type: "timestamp", value: Number(raw) };
222
+ case "json":
223
+ return { type: "json", value: decode(raw) };
224
+ case "blob_ref":
225
+ return { type: "blob", value: String(raw) };
226
+ case "text":
227
+ return { type: "text", value: String(raw) };
228
+ default:
229
+ if (typeof raw === "number") return Number.isInteger(raw) ? { type: "integer", value: raw } : { type: "real", value: raw };
230
+ if (typeof raw === "bigint") return { type: "integer", value: Number(raw) };
231
+ return { type: "text", value: String(raw) };
232
+ }
233
+ }
234
+ function positional(sql) {
235
+ let out = "";
236
+ let quote = null;
237
+ for (let i = 0; i < sql.length; i++) {
238
+ const c = sql[i];
239
+ if (quote !== null) {
240
+ out += c;
241
+ if (c === quote) quote = null;
242
+ continue;
243
+ }
244
+ if (c === "'" || c === '"') {
245
+ quote = c;
246
+ out += c;
247
+ continue;
248
+ }
249
+ if (c === "$" && /[0-9]/.test(sql[i + 1] ?? "")) {
250
+ out += "?";
251
+ continue;
252
+ }
253
+ out += c;
254
+ }
255
+ return out;
256
+ }
257
+ function createTable(table) {
258
+ const owned = table.row_scope === "owner";
259
+ const columns = table.columns.map((c) => `${c.name} ${SQL_TYPES[c.column_type] ?? "TEXT"}${c.nullable ? "" : " NOT NULL"}`);
260
+ if (owned) columns.push(`${OWNER_COLUMN} TEXT NOT NULL`);
261
+ const key = owned ? [OWNER_COLUMN, ...table.primary_key] : [...table.primary_key];
262
+ return [`CREATE TABLE IF NOT EXISTS ${table.name} (${columns.join(", ")}, PRIMARY KEY (${key.join(", ")}))`, ...table.indexes.map((i) => createIndex(table, i))];
263
+ }
264
+ function createIndex(table, index) {
265
+ const columns = table.row_scope === "owner" ? [OWNER_COLUMN, ...index.columns] : [...index.columns];
266
+ return `CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${index.name} ON ${table.name} (${columns.join(", ")})`;
267
+ }
268
+ function planMigration(applied, declared) {
269
+ for (const table of declared.tables) {
270
+ const valid = isIdentifier(table.name) && table.columns.every((c) => isIdentifier(c.name)) && table.primary_key.length > 0 && table.primary_key.every((k) => table.columns.some((c) => c.name === k)) && table.indexes.every((i) => isIdentifier(i.name) && i.columns.length > 0 && i.columns.every((c) => table.columns.some((col) => col.name === c)));
271
+ if (!valid) return { kind: "identifier" };
272
+ }
273
+ if (applied.tables.some((old) => !declared.tables.some((t) => t.name === old.name))) return { kind: "destructive" };
274
+ const script = [];
275
+ for (const table of declared.tables) {
276
+ const old = applied.tables.find((t) => t.name === table.name);
277
+ if (old === void 0) {
278
+ script.push(...createTable(table));
279
+ continue;
280
+ }
281
+ const kept = old.row_scope === table.row_scope && old.primary_key.join(",") === table.primary_key.join(",") && old.columns.every((c) => table.columns.some((n) => n.name === c.name && n.column_type === c.column_type)) && old.indexes.every((i) => table.indexes.some((n) => JSON.stringify(n) === JSON.stringify(i)));
282
+ if (!kept) return { kind: "destructive" };
283
+ for (const column of table.columns) {
284
+ if (old.columns.some((c) => c.name === column.name)) continue;
285
+ if (!column.nullable) return { kind: "required_column" };
286
+ script.push(`ALTER TABLE ${table.name} ADD COLUMN ${column.name} ${SQL_TYPES[column.column_type] ?? "TEXT"}`);
287
+ }
288
+ for (const index of table.indexes) {
289
+ if (!old.indexes.some((i) => JSON.stringify(i) === JSON.stringify(index))) script.push(createIndex(table, index));
290
+ }
291
+ }
292
+ return { kind: "apply", script };
293
+ }
294
+ function localDataDir(root, environment = LOCAL_ENVIRONMENT) {
295
+ return path.join(root, ...DATA_DIR, environment);
296
+ }
297
+ function localEnvironments(root) {
298
+ const dir = path.join(root, ...DATA_DIR);
299
+ if (!existsSync(dir)) return [];
300
+ return readdirSync(dir).filter((entry) => existsSync(path.join(dir, entry, DATA_FILE)));
301
+ }
302
+ async function resetLocalData(root, environment = null) {
303
+ const targets = environment === null ? localEnvironments(root) : [environment];
304
+ const removed = [];
305
+ for (const target of targets) {
306
+ const dir = localDataDir(root, target);
307
+ if (!existsSync(dir)) continue;
308
+ await promises.rm(dir, { recursive: true, force: true });
309
+ removed.push(target);
310
+ }
311
+ return removed;
312
+ }
313
+ function serverSide(caller) {
314
+ return caller.location !== "client";
315
+ }
316
+ function partition(caller, scope, write) {
317
+ if (scope === "shared" && write && !serverSide(caller)) denied("server_writer_required");
318
+ return scope === "personal" ? `p:${caller.participant}` : "";
319
+ }
320
+ function namespaceId(scope, kind, label) {
321
+ return `${SCOPE_CHARS[scope] ?? invalid("scope", "malformed")}${kind}${hex(label)}`;
322
+ }
323
+ function parseNamespace(id, kind, field) {
324
+ if (typeof id !== "string" || id.length !== 2 + NAMESPACE_HEX || id[1] !== kind || !/^[0-9a-f]+$/.test(id.slice(2))) invalid(field, "malformed");
325
+ const scope = SCOPES_BY_CHAR[id[0]];
326
+ if (scope === void 0) invalid(field, "malformed");
327
+ return { id, scope };
328
+ }
329
+ function slot(id) {
330
+ return Number.parseInt(id.slice(2, 2 + SLOT_HEX), 16);
331
+ }
332
+ var LocalData = class {
333
+ constructor(options) {
334
+ this.options = options;
335
+ this.environment = options.environment ?? LOCAL_ENVIRONMENT;
336
+ this.file = path.join(localDataDir(options.root, this.environment), DATA_FILE);
337
+ this.participant = options.participant ?? LOCAL_PARTICIPANT;
338
+ this.location = options.location ?? "client";
339
+ this.now = options.now ?? Date.now;
340
+ }
341
+ options;
342
+ environment;
343
+ file;
344
+ db = null;
345
+ participant;
346
+ location;
347
+ now;
348
+ transactions = /* @__PURE__ */ new Map();
349
+ cached = /* @__PURE__ */ new Map();
350
+ async open() {
351
+ if (this.db !== null) return this.db;
352
+ const sqlite = await import('sqlite').catch(() => null);
353
+ if (sqlite === null) refuse({ code: "unavailable", family: "db", retry_after_ms: null });
354
+ mkdirSync(path.dirname(this.file), { recursive: true });
355
+ const db = new sqlite.DatabaseSync(this.file);
356
+ db.exec("PRAGMA journal_mode = WAL");
357
+ for (const statement of PLATFORM_TABLES) db.exec(statement);
358
+ this.db = db;
359
+ return db;
360
+ }
361
+ close() {
362
+ this.cached.clear();
363
+ this.db?.close();
364
+ this.db = null;
365
+ }
366
+ async answer(call, location = this.location) {
367
+ try {
368
+ const db = await this.open();
369
+ const caller = { participant: this.participant, location };
370
+ return { result: this.dispatch(db, caller, call.operation, call.arguments ?? {}) };
371
+ } catch (error) {
372
+ if (error instanceof Refused) return { error: error.error };
373
+ const family = call.operation.split(".")[0] ?? "db";
374
+ return { error: { code: "unavailable", family, retry_after_ms: null } };
375
+ }
376
+ }
377
+ prepare(db, sql) {
378
+ let found = this.cached.get(sql);
379
+ if (found === void 0) {
380
+ found = db.prepare(sql);
381
+ this.cached.set(sql, found);
382
+ }
383
+ return found;
384
+ }
385
+ run(db, sql, ...params) {
386
+ const done = this.prepare(db, sql).run(...params);
387
+ return { changes: Number(done.changes) };
388
+ }
389
+ one(db, sql, ...params) {
390
+ return this.prepare(db, sql).get(...params);
391
+ }
392
+ all(db, sql, ...params) {
393
+ return this.prepare(db, sql).all(...params);
394
+ }
395
+ current(db) {
396
+ const row = this.one(db, "SELECT version FROM ix_meta WHERE key = ?", META_STAMP_KEY);
397
+ return row === void 0 ? 0 : Number(row.version);
398
+ }
399
+ bump(db) {
400
+ const next = this.current(db) + 1;
401
+ this.run(db, "INSERT INTO ix_meta (key, version, schema) VALUES (?, ?, NULL) ON CONFLICT (key) DO UPDATE SET version = excluded.version", META_STAMP_KEY, next);
402
+ return next;
403
+ }
404
+ atomic(db, work) {
405
+ db.exec("BEGIN IMMEDIATE");
406
+ try {
407
+ const out = work();
408
+ db.exec("COMMIT");
409
+ return out;
410
+ } catch (error) {
411
+ db.exec("ROLLBACK");
412
+ throw error;
413
+ }
414
+ }
415
+ declared() {
416
+ return packageData(this.options.declarations()) ?? { schema: { version: 0, tables: [] }, statements: [] };
417
+ }
418
+ applied(db) {
419
+ const row = this.one(db, "SELECT schema FROM ix_meta WHERE key = ?", META_DB_KEY);
420
+ return row === void 0 ? { version: 0, tables: [] } : decode(row.schema);
421
+ }
422
+ dispatch(db, caller, operation, args) {
423
+ switch (operation) {
424
+ case "db.describe":
425
+ return this.describe(db);
426
+ case "db.migrate":
427
+ return this.migrate(db, args);
428
+ case "db.query":
429
+ return this.query(db, caller, args);
430
+ case "tx.begin":
431
+ return this.begin(db, args);
432
+ case "tx.stage":
433
+ return this.stage(args);
434
+ case "tx.commit":
435
+ return this.commit(db, caller, args);
436
+ case "tx.abort":
437
+ return this.abort(args);
438
+ case "kv.open":
439
+ return this.kvOpen(db, caller, args);
440
+ case "kv.get":
441
+ return this.kvGet(db, caller, args);
442
+ case "kv.set":
443
+ return this.atomic(db, () => this.kvSet(db, caller, args.namespace_id, args.key, args.value, args.ttl_ms));
444
+ case "kv.compare_and_set":
445
+ return this.atomic(db, () => this.kvSwap(db, caller, args));
446
+ case "kv.delete":
447
+ return this.atomic(db, () => this.kvDelete(db, caller, args.namespace_id, args.key));
448
+ case "kv.list":
449
+ return this.kvList(db, caller, args);
450
+ case "cache.open":
451
+ return this.cacheOpen(db, caller, args);
452
+ case "cache.get":
453
+ return this.cacheGet(db, caller, args, false);
454
+ case "cache.set":
455
+ return this.cacheSet(db, caller, args);
456
+ case "cache.delete":
457
+ return this.cacheGet(db, caller, args, true);
458
+ case "counter.open":
459
+ return this.counterOpen(caller, args);
460
+ case "counter.add":
461
+ return this.atomic(db, () => this.counterAdd(db, caller, args.counter_id, args.delta));
462
+ case "counter.read":
463
+ return this.counterRead(db, caller, args);
464
+ case "counter.rate_limit":
465
+ return this.atomic(db, () => this.rateLimit(db, caller, args));
466
+ case "counter.rank":
467
+ return this.atomic(db, () => this.rank(db, caller, args));
468
+ case "counter.rank_range":
469
+ return this.rankRange(db, caller, args);
470
+ case "counter.rank_of":
471
+ return this.rankOf(db, caller, args);
472
+ case "queue.enqueue":
473
+ return this.atomic(db, () => this.enqueue(db, caller, args));
474
+ case "queue.status":
475
+ return this.job(db, text(args.job_id, "job_id"));
476
+ case "queue.cancel":
477
+ return this.cancel(db, args);
478
+ case "queue.stats":
479
+ return this.queueStats(db, args);
480
+ case "search.index":
481
+ return this.atomic(db, () => this.searchIndex(db, caller, args));
482
+ case "search.query":
483
+ return this.searchQuery(db, caller, args);
484
+ case "search.remove":
485
+ return this.atomic(db, () => this.searchRemove(db, caller, args));
486
+ case "pubsub.publish":
487
+ return this.atomic(db, () => this.publish(db, args));
488
+ case "analytics.track":
489
+ return this.track(db, args);
490
+ case "analytics.record":
491
+ return this.record(db, args);
492
+ case "analytics.summary":
493
+ return this.summary(db, caller, args);
494
+ default:
495
+ return refuse({ code: "unavailable", family: operation.split(".")[0] ?? "db", retry_after_ms: null });
496
+ }
497
+ }
498
+ describe(db) {
499
+ const declared = this.declared().schema;
500
+ const applied = this.applied(db);
501
+ let rows = 0;
502
+ for (const table of applied.tables) rows += Number(this.one(db, `SELECT COUNT(*) AS n FROM ${table.name}`)?.n ?? 0);
503
+ const bytes = existsSync(this.file) ? statSync(this.file).size : 0;
504
+ return {
505
+ schema: applied,
506
+ pending_version: declared.version > applied.version ? declared.version : null,
507
+ rows: { used: rows, limit: DB_ROWS_LIMIT },
508
+ bytes: { used: bytes, limit: STORAGE_LIMIT }
509
+ };
510
+ }
511
+ migrate(db, args) {
512
+ const declared = this.declared().schema;
513
+ if (args.target_version !== declared.version) invalid("target_version", "schema_mismatch");
514
+ const applied = this.applied(db);
515
+ if (applied.version >= declared.version) return this.describe(db);
516
+ const plan = planMigration(applied, declared);
517
+ if (plan.kind === "destructive") refuse({ code: "conflict", resource: "table", current: null });
518
+ if (plan.kind !== "apply") invalid("schema", "schema_mismatch");
519
+ this.atomic(db, () => {
520
+ for (const statement of plan.script) db.exec(statement);
521
+ this.run(
522
+ db,
523
+ "INSERT INTO ix_meta (key, version, schema) VALUES (?, ?, ?) ON CONFLICT (key) DO UPDATE SET version = excluded.version, schema = excluded.schema",
524
+ META_DB_KEY,
525
+ declared.version,
526
+ encode(declared)
527
+ );
528
+ });
529
+ this.cached.clear();
530
+ return this.describe(db);
531
+ }
532
+ table(name2) {
533
+ const table = this.declared().schema.tables.find((t) => t.name === name2);
534
+ if (table === void 0) invalid("table", "not_declared");
535
+ return table;
536
+ }
537
+ column(table, name2, field) {
538
+ const column = table.columns.find((c) => c.name === name2);
539
+ if (column === void 0) invalid(field, "not_declared");
540
+ return column.column_type;
541
+ }
542
+ statement(name2, access) {
543
+ const found = this.declared().statements.find((s) => s.name === name2 && s.access === access);
544
+ if (found === void 0) denied("undeclared_statement");
545
+ return found;
546
+ }
547
+ bindStatement(caller, statement, params) {
548
+ if (params.length !== statement.params) invalid("params", "schema_mismatch");
549
+ if (statement.shared_write && !serverSide(caller)) denied("server_writer_required");
550
+ const out = [];
551
+ let next = 0;
552
+ const total = statement.params + (statement.owner_param === null ? 0 : 1);
553
+ for (let position = 1; position <= total; position++) {
554
+ if (position === statement.owner_param) out.push(caller.participant);
555
+ else out.push(sqlValue(params[next++], "params"));
556
+ }
557
+ if (out.length > MAX_DB_PARAMETERS) invalid("params", "too_long");
558
+ return out;
559
+ }
560
+ where(table, filter, owner, params) {
561
+ const clauses = [];
562
+ for (const predicate of filter) {
563
+ this.column(table, predicate.column, "filter");
564
+ const values = predicate.values;
565
+ if ((predicate.comparison === "eq" || predicate.comparison === "ne") && values.length === 1 && values[0].type === "null") {
566
+ clauses.push(`${predicate.column} IS ${predicate.comparison === "eq" ? "" : "NOT "}NULL`);
567
+ } else if (predicate.comparison === "prefix" && values.length === 1 && values[0].type === "text") {
568
+ clauses.push(`substr(${predicate.column}, 1, ?) = ?`);
569
+ const prefix = text(values[0].value, "filter");
570
+ params.push(prefix.length, prefix);
571
+ } else if (predicate.comparison === "in" && values.length > 0) {
572
+ clauses.push(`${predicate.column} IN (${values.map(() => "?").join(", ")})`);
573
+ for (const value of values) params.push(sqlValue(value, "filter"));
574
+ } else if (COMPARISONS[predicate.comparison] !== void 0 && values.length === 1) {
575
+ clauses.push(`${predicate.column}${COMPARISONS[predicate.comparison]}?`);
576
+ params.push(sqlValue(values[0], "filter"));
577
+ } else {
578
+ invalid("filter", "malformed");
579
+ }
580
+ }
581
+ if (owner !== null) {
582
+ clauses.push(`${OWNER_COLUMN} = ?`);
583
+ params.push(owner);
584
+ }
585
+ if (params.length > MAX_DB_PARAMETERS) invalid("params", "too_long");
586
+ return clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
587
+ }
588
+ query(db, caller, args) {
589
+ const query = args.query;
590
+ const limit = pageLimit(query.page);
591
+ const offset = pageOffset(query.page);
592
+ let sql;
593
+ let params;
594
+ let cells = null;
595
+ if (query.kind === "statement") {
596
+ const statement = this.statement(query.statement, "read");
597
+ params = this.bindStatement(caller, statement, query.params ?? []);
598
+ sql = `SELECT * FROM (${positional(statement.sql).trim().replace(/;$/, "")}) AS ix_page LIMIT ${limit + 1} OFFSET ${offset}`;
599
+ } else if (query.kind === "select") {
600
+ const table = this.table(query.table);
601
+ const names = query.columns ?? [];
602
+ cells = (names.length === 0 ? table.columns.map((c) => c.name) : names).map((n) => ({ name: n, type: this.column(table, n, "columns") }));
603
+ params = [];
604
+ sql = `SELECT ${cells.map((c) => c.name).join(", ")} FROM ${table.name}`;
605
+ sql += this.where(table, query.filter ?? [], table.row_scope === "owner" ? caller.participant : null, params);
606
+ const order = query.order ?? [];
607
+ order.forEach((key, at) => {
608
+ this.column(table, key.column, "order");
609
+ sql += `${at === 0 ? " ORDER BY " : ", "}${key.column} ${key.direction === "desc" ? "DESC" : "ASC"}`;
610
+ });
611
+ sql += ` LIMIT ${limit + 1} OFFSET ${offset}`;
612
+ } else {
613
+ return invalid("query.kind", "malformed");
614
+ }
615
+ let found;
616
+ try {
617
+ found = db.prepare(sql).all(...params);
618
+ } catch {
619
+ return invalid("query", "schema_mismatch");
620
+ }
621
+ const columns = cells ?? Object.keys(found[0] ?? {}).map((n) => ({ name: n, type: null }));
622
+ const rows = found.slice(0, limit).map((row) => columns.map((c) => cell(row[c.name], c.type)));
623
+ return {
624
+ columns: columns.map((c) => c.name),
625
+ rows,
626
+ consistency: consistency(this.current(db)),
627
+ next: found.length > limit ? String(offset + limit) : null
628
+ };
629
+ }
630
+ write(db, caller, write) {
631
+ if (write.kind === "statement") {
632
+ const statement = this.statement(write.statement, "write");
633
+ const params2 = this.bindStatement(caller, statement, write.params ?? []);
634
+ try {
635
+ return this.run(db, positional(statement.sql), ...params2).changes;
636
+ } catch {
637
+ return refuse({ code: "conflict", resource: "table", current: null });
638
+ }
639
+ }
640
+ const table = this.table(write.table);
641
+ const owner = table.row_scope === "owner" ? caller.participant : null;
642
+ if (table.row_scope === "shared" && !serverSide(caller)) denied("server_writer_required");
643
+ if (write.kind === "insert") {
644
+ const columns = write.columns ?? [];
645
+ const rows = write.rows ?? [];
646
+ for (const c of columns) this.column(table, c, "columns");
647
+ if (rows.length === 0 || rows.some((r) => r.length !== columns.length)) invalid("rows", "schema_mismatch");
648
+ if (table.primary_key.some((k) => !columns.includes(k))) invalid("columns", "missing");
649
+ const names = owner === null ? columns : [...columns, OWNER_COLUMN];
650
+ const sql = `INSERT INTO ${table.name} (${names.join(", ")}) VALUES (${names.map(() => "?").join(", ")})`;
651
+ let changed = 0;
652
+ for (const row of rows) {
653
+ const values = row.map((v) => sqlValue(v, "rows"));
654
+ if (owner !== null) values.push(owner);
655
+ try {
656
+ changed += this.run(db, sql, ...values).changes;
657
+ } catch {
658
+ return refuse({ code: "conflict", resource: "table", current: null });
659
+ }
660
+ }
661
+ return changed;
662
+ }
663
+ const params = [];
664
+ if (write.kind === "update") {
665
+ const set = write.set ?? [];
666
+ if (set.length === 0) invalid("set", "missing");
667
+ for (const assignment of set) {
668
+ this.column(table, assignment.column, "set");
669
+ if (table.primary_key.includes(assignment.column)) invalid("set", "malformed");
670
+ }
671
+ const assignments = set.map((a) => {
672
+ params.push(sqlValue(a.value, "set"));
673
+ return `${a.column} = ?`;
674
+ });
675
+ const where = this.where(table, write.filter ?? [], owner, params);
676
+ return this.run(db, `UPDATE ${table.name} SET ${assignments.join(", ")}${where}`, ...params).changes;
677
+ }
678
+ if (write.kind === "delete") {
679
+ const where = this.where(table, write.filter ?? [], owner, params);
680
+ return this.run(db, `DELETE FROM ${table.name}${where}`, ...params).changes;
681
+ }
682
+ return invalid("write.kind", "malformed");
683
+ }
684
+ begin(db, args) {
685
+ const transaction = `tx_local_${createHash("sha256").update(text(args.request_key, "request_key")).update(String(this.now())).digest("hex").slice(0, 24)}`;
686
+ const hold = typeof args.timeout_ms === "number" && args.timeout_ms > 0 ? Math.min(args.timeout_ms, TX_HOLD_MS) : TX_HOLD_MS;
687
+ const snapshot = this.current(db);
688
+ const expiresAt = this.now() + hold;
689
+ this.transactions.set(transaction, { steps: [], expiresAt, snapshot });
690
+ return { transaction, snapshot: stampOf(snapshot), expires_at: expiresAt };
691
+ }
692
+ held(id) {
693
+ const found = this.transactions.get(text(id, "transaction"));
694
+ if (found === void 0) notFound("operation");
695
+ if (found.expiresAt < this.now()) {
696
+ this.transactions.delete(id);
697
+ refuse({ code: "expired", resource: "operation" });
698
+ }
699
+ return found;
700
+ }
701
+ stage(args) {
702
+ const found = this.held(args.transaction);
703
+ const steps = args.steps ?? [];
704
+ found.steps.push(...steps);
705
+ return { transaction: args.transaction, staged: found.steps.length };
706
+ }
707
+ abort(args) {
708
+ this.transactions.delete(text(args.transaction, "transaction"));
709
+ return { transaction: args.transaction };
710
+ }
711
+ commit(db, caller, args) {
712
+ const staged = args.transaction === null || args.transaction === void 0 ? [] : this.held(args.transaction).steps;
713
+ const steps = [...staged, ...args.steps ?? []];
714
+ const declared = this.declared().schema.version;
715
+ if (typeof args.schema_version === "number" && args.schema_version !== declared) invalid("schema_version", "schema_mismatch");
716
+ const results = this.atomic(
717
+ db,
718
+ () => steps.map((step) => {
719
+ switch (step.kind) {
720
+ case "db":
721
+ return { kind: "db", changed_rows: this.write(db, caller, step.write) };
722
+ case "kv_set": {
723
+ const written = this.kvSet(db, caller, step.namespace_id, step.key, step.value, step.ttl_ms);
724
+ return { kind: "kv", key: step.key, stamp: written.stamp };
725
+ }
726
+ case "kv_delete": {
727
+ const written = this.kvDelete(db, caller, step.namespace_id, step.key);
728
+ return { kind: "kv", key: step.key, stamp: written.stamp };
729
+ }
730
+ case "kv_expect": {
731
+ const current = this.kvEntry(db, caller, step.namespace_id, step.key);
732
+ const expected = stampNumber(step.stamp);
733
+ const actual = current.value === null ? null : current.stamp;
734
+ if (expected !== actual) refuse({ code: "conflict", resource: "kv_key", current: null });
735
+ return { kind: "expectation_met" };
736
+ }
737
+ case "counter_add": {
738
+ const value = this.counterAdd(db, caller, step.counter_id, step.delta);
739
+ return { kind: "counter", counter_id: step.counter_id, value: value.value };
740
+ }
741
+ case "rank":
742
+ return { kind: "rank", entry: this.rank(db, caller, step.update).entry };
743
+ case "enqueue":
744
+ return { kind: "enqueued", job_id: this.enqueue(db, caller, step.job).job_id };
745
+ case "publish":
746
+ return { kind: "published", sequence: this.publish(db, step.message).sequence };
747
+ default:
748
+ return invalid("steps", "malformed");
749
+ }
750
+ })
751
+ );
752
+ if (typeof args.transaction === "string") this.transactions.delete(args.transaction);
753
+ return { results, consistency: consistency(this.current(db)) };
754
+ }
755
+ kvOpen(db, caller, args) {
756
+ const label = name(args.name, "name");
757
+ const scope = text(args.scope, "scope");
758
+ const id = namespaceId(scope, KV_KIND, label);
759
+ const part = partition(caller, scope, false);
760
+ const used = this.one(db, "SELECT COUNT(*) AS n, COALESCE(SUM(LENGTH(value)), 0) AS b FROM ix_kv WHERE ns = ? AND part = ?", id, part);
761
+ return {
762
+ namespace_id: id,
763
+ slot: slot(id),
764
+ scope,
765
+ classification: args.classification,
766
+ keys: { used: Number(used?.n ?? 0), limit: KV_KEYS_LIMIT },
767
+ bytes: { used: Number(used?.b ?? 0), limit: KV_BYTES_LIMIT }
768
+ };
769
+ }
770
+ kvEntry(db, caller, namespace, key) {
771
+ const ns = parseNamespace(namespace, KV_KIND, "namespace_id");
772
+ const k = name(key, "key");
773
+ const row = this.one(db, "SELECT value, stamp, expires_at FROM ix_kv WHERE ns = ? AND part = ? AND k = ?", ns.id, partition(caller, ns.scope, false), k);
774
+ if (row === void 0 || row.expires_at !== null && Number(row.expires_at) <= this.now()) return { value: null, stamp: 0, expiresAt: null };
775
+ return { value: row.value === null ? null : decode(row.value), stamp: Number(row.stamp), expiresAt: row.expires_at === null ? null : Number(row.expires_at) };
776
+ }
777
+ kvGet(db, caller, args) {
778
+ const entry = this.kvEntry(db, caller, args.namespace_id, args.key);
779
+ return {
780
+ entry: { key: args.key, value: entry.value, stamp: stampOf(entry.stamp), expires_at: entry.expiresAt },
781
+ consistency: consistency(this.current(db))
782
+ };
783
+ }
784
+ kvSet(db, caller, namespace, key, value, ttl) {
785
+ const ns = parseNamespace(namespace, KV_KIND, "namespace_id");
786
+ const k = name(key, "key");
787
+ const part = partition(caller, ns.scope, true);
788
+ const stamp = this.bump(db);
789
+ const expiresAt = typeof ttl === "number" && ttl > 0 ? this.now() + ttl : null;
790
+ this.run(
791
+ db,
792
+ "INSERT INTO ix_kv (ns, part, k, value, stamp, expires_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (ns, part, k) DO UPDATE SET value = excluded.value, stamp = excluded.stamp, expires_at = excluded.expires_at",
793
+ ns.id,
794
+ part,
795
+ k,
796
+ encode(value),
797
+ stamp,
798
+ expiresAt
799
+ );
800
+ return { key, stamp: stampOf(stamp), consistency: consistency(stamp) };
801
+ }
802
+ kvDelete(db, caller, namespace, key) {
803
+ const ns = parseNamespace(namespace, KV_KIND, "namespace_id");
804
+ const k = name(key, "key");
805
+ const part = partition(caller, ns.scope, true);
806
+ const stamp = this.bump(db);
807
+ this.run(db, "DELETE FROM ix_kv WHERE ns = ? AND part = ? AND k = ?", ns.id, part, k);
808
+ return { key, stamp: stampOf(stamp), consistency: consistency(stamp) };
809
+ }
810
+ kvSwap(db, caller, args) {
811
+ const current = this.kvEntry(db, caller, args.namespace_id, args.key);
812
+ const expected = stampNumber(args.expected);
813
+ const actual = current.value === null ? null : current.stamp;
814
+ const entry = (stamp2, value, expiresAt) => ({ key: args.key, value, stamp: stampOf(stamp2), expires_at: expiresAt });
815
+ if (expected !== actual) {
816
+ return { applied: false, current: entry(current.stamp, current.value, current.expiresAt), consistency: consistency(this.current(db)) };
817
+ }
818
+ if (args.value === null || args.value === void 0) {
819
+ const deleted = this.kvDelete(db, caller, args.namespace_id, args.key);
820
+ const stamp2 = stampNumber(deleted.stamp) ?? 0;
821
+ return { applied: true, current: entry(stamp2, null, null), consistency: consistency(stamp2) };
822
+ }
823
+ const written = this.kvSet(db, caller, args.namespace_id, args.key, args.value, args.ttl_ms);
824
+ const stamp = stampNumber(written.stamp) ?? 0;
825
+ const ttl = typeof args.ttl_ms === "number" && args.ttl_ms > 0 ? this.now() + args.ttl_ms : null;
826
+ return { applied: true, current: entry(stamp, args.value, ttl), consistency: consistency(stamp) };
827
+ }
828
+ kvList(db, caller, args) {
829
+ const ns = parseNamespace(args.namespace_id, KV_KIND, "namespace_id");
830
+ const limit = pageLimit(args.page);
831
+ const after = args.page?.cursor ?? "";
832
+ const prefix = typeof args.prefix === "string" ? args.prefix : "";
833
+ const rows = this.all(
834
+ db,
835
+ "SELECT k, value, stamp, expires_at FROM ix_kv WHERE ns = ? AND part = ? AND k > ? AND substr(k, 1, ?) = ? AND (expires_at IS NULL OR expires_at > ?) ORDER BY k LIMIT ?",
836
+ ns.id,
837
+ partition(caller, ns.scope, false),
838
+ after,
839
+ prefix.length,
840
+ prefix,
841
+ this.now(),
842
+ limit + 1
843
+ );
844
+ const entries = rows.slice(0, limit).map((row) => ({
845
+ key: row.k,
846
+ value: decode(row.value),
847
+ stamp: stampOf(Number(row.stamp)),
848
+ expires_at: row.expires_at === null ? null : Number(row.expires_at)
849
+ }));
850
+ return { entries, next: rows.length > limit ? String(rows[limit - 1].k) : null, consistency: consistency(this.current(db)) };
851
+ }
852
+ cacheNamespace(id) {
853
+ const raw = text(id, "namespace_id");
854
+ const at = raw.indexOf(TTL_SEPARATOR);
855
+ if (at < 0) invalid("namespace_id", "malformed");
856
+ const ns = parseNamespace(raw.slice(0, at), CACHE_KIND, "namespace_id");
857
+ const ttl = Number(raw.slice(at + 1));
858
+ if (!Number.isInteger(ttl) || ttl <= 0) invalid("namespace_id", "malformed");
859
+ return { id: raw, scope: ns.scope, ttl };
860
+ }
861
+ cacheOpen(db, caller, args) {
862
+ const label = name(args.name, "name");
863
+ const ttl = args.default_ttl_ms;
864
+ if (typeof ttl !== "number" || ttl <= 0 || ttl > MAX_CACHE_TTL_MS) invalid("default_ttl_ms", "out_of_range");
865
+ const scope = text(args.scope, "scope");
866
+ const id = `${namespaceId(scope, CACHE_KIND, label)}${TTL_SEPARATOR}${ttl}`;
867
+ const used = this.one(db, "SELECT COALESCE(SUM(LENGTH(value)), 0) AS b FROM ix_cache WHERE ns = ? AND part = ?", id, partition(caller, scope, false));
868
+ return {
869
+ namespace_id: id,
870
+ slot: slot(id),
871
+ scope,
872
+ classification: args.classification,
873
+ max_entry_bytes: CACHE_MAX_ENTRY_BYTES,
874
+ bytes: { used: Number(used?.b ?? 0), limit: KV_BYTES_LIMIT }
875
+ };
876
+ }
877
+ cacheGet(db, caller, args, take) {
878
+ const ns = this.cacheNamespace(args.namespace_id);
879
+ const k = name(args.key, "key");
880
+ const part = partition(caller, ns.scope, take);
881
+ const row = this.one(db, "SELECT value, expires_at FROM ix_cache WHERE ns = ? AND part = ? AND k = ? AND expires_at > ?", ns.id, part, k, this.now());
882
+ if (take) this.run(db, "DELETE FROM ix_cache WHERE ns = ? AND part = ? AND k = ?", ns.id, part, k);
883
+ return { key: k, value: row === void 0 ? null : decode(row.value), expires_at: row === void 0 ? null : Number(row.expires_at) };
884
+ }
885
+ cacheSet(db, caller, args) {
886
+ const ns = this.cacheNamespace(args.namespace_id);
887
+ const k = name(args.key, "key");
888
+ const value = encode(args.value);
889
+ if (Buffer.byteLength(value) > CACHE_MAX_ENTRY_BYTES) refuse({ code: "limit_exceeded", limit: "payload_bytes" });
890
+ const ttl = typeof args.ttl_ms === "number" && args.ttl_ms > 0 ? Math.min(args.ttl_ms, MAX_CACHE_TTL_MS) : ns.ttl;
891
+ const expiresAt = this.now() + ttl;
892
+ this.run(
893
+ db,
894
+ "INSERT INTO ix_cache (ns, part, k, value, expires_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (ns, part, k) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
895
+ ns.id,
896
+ partition(caller, ns.scope, true),
897
+ k,
898
+ value,
899
+ expiresAt
900
+ );
901
+ return { key: k, expires_at: expiresAt };
902
+ }
903
+ counter(id) {
904
+ const raw = text(id, "counter_id");
905
+ const kind = Object.entries(COUNTER_KINDS).find(([, c]) => c === raw[1])?.[0];
906
+ if (kind === void 0) invalid("counter_id", "malformed");
907
+ const ns = parseNamespace(raw, raw[1], "counter_id");
908
+ return { id: ns.id, scope: ns.scope, kind };
909
+ }
910
+ counterOpen(caller, args) {
911
+ const label = name(args.name, "name");
912
+ const kind = text(args.kind, "kind");
913
+ const code = COUNTER_KINDS[kind];
914
+ if (code === void 0) invalid("kind", "malformed");
915
+ const scope = text(args.scope, "scope");
916
+ partition(caller, scope, false);
917
+ const id = namespaceId(scope, code, label);
918
+ return { counter_id: id, kind, scope, slot: slot(id) };
919
+ }
920
+ counterAdd(db, caller, id, delta) {
921
+ const counter = this.counter(id);
922
+ if (counter.kind !== "sum") invalid("counter_id", "schema_mismatch");
923
+ if (typeof delta !== "number" || !Number.isInteger(delta)) invalid("delta", "malformed");
924
+ const part = partition(caller, counter.scope, true);
925
+ this.run(db, "INSERT INTO ix_counters (id, part, value) VALUES (?, ?, ?) ON CONFLICT (id, part) DO UPDATE SET value = value + excluded.value", counter.id, part, delta);
926
+ const stamp = this.bump(db);
927
+ const value = Number(this.one(db, "SELECT value FROM ix_counters WHERE id = ? AND part = ?", counter.id, part)?.value ?? 0);
928
+ return { counter_id: counter.id, value, consistency: consistency(stamp) };
929
+ }
930
+ counterRead(db, caller, args) {
931
+ const counter = this.counter(args.counter_id);
932
+ const value = Number(this.one(db, "SELECT value FROM ix_counters WHERE id = ? AND part = ?", counter.id, partition(caller, counter.scope, false))?.value ?? 0);
933
+ return { counter_id: counter.id, value, consistency: consistency(this.current(db)) };
934
+ }
935
+ rateLimit(db, caller, args) {
936
+ const counter = this.counter(args.counter_id);
937
+ if (counter.kind !== "rate_limit") invalid("counter_id", "schema_mismatch");
938
+ const key = name(args.key, "key");
939
+ const cost = Number(args.cost);
940
+ const limit = Number(args.limit);
941
+ const windowMs = Number(args.window_ms);
942
+ if (!(cost > 0 && limit > 0 && windowMs > 0)) invalid("window_ms", "out_of_range");
943
+ const part = partition(caller, counter.scope, false);
944
+ const now = this.now();
945
+ const started = now - now % windowMs;
946
+ const row = this.one(db, "SELECT started, used FROM ix_windows WHERE id = ? AND part = ? AND k = ?", counter.id, part, key);
947
+ const used = row !== void 0 && Number(row.started) === started ? Number(row.used) : 0;
948
+ const allowed = used + cost <= limit;
949
+ const next = allowed ? used + cost : used;
950
+ this.run(
951
+ db,
952
+ "INSERT INTO ix_windows (id, part, k, started, used) VALUES (?, ?, ?, ?, ?) ON CONFLICT (id, part, k) DO UPDATE SET started = excluded.started, used = excluded.used",
953
+ counter.id,
954
+ part,
955
+ key,
956
+ started,
957
+ next
958
+ );
959
+ return { allowed, remaining: Math.max(0, limit - next), resets_at: started + windowMs };
960
+ }
961
+ ranked(db, id, part, member) {
962
+ const row = this.one(db, "SELECT score FROM ix_ranks WHERE id = ? AND part = ? AND member = ?", id, part, member);
963
+ if (row === void 0) return null;
964
+ const score = Number(row.score);
965
+ const above = Number(this.one(db, "SELECT COUNT(*) AS n FROM ix_ranks WHERE id = ? AND part = ? AND (score > ? OR (score = ? AND member < ?))", id, part, score, score, member)?.n ?? 0);
966
+ return { member, score, rank: above + 1 };
967
+ }
968
+ rank(db, caller, args) {
969
+ const counter = this.counter(args.counter_id);
970
+ if (counter.kind !== "ranked_set") invalid("counter_id", "schema_mismatch");
971
+ const member = name(args.member, "member");
972
+ const score = Number(args.score);
973
+ if (!Number.isFinite(score)) invalid("score", "malformed");
974
+ const part = partition(caller, counter.scope, true);
975
+ const old = this.one(db, "SELECT score FROM ix_ranks WHERE id = ? AND part = ? AND member = ?", counter.id, part, member);
976
+ const previous = old === void 0 ? null : Number(old.score);
977
+ let next = score;
978
+ if (previous !== null) {
979
+ if (args.mode === "keep_highest") next = Math.max(previous, score);
980
+ else if (args.mode === "keep_lowest") next = Math.min(previous, score);
981
+ else if (args.mode === "increment") next = previous + score;
982
+ }
983
+ this.run(db, "INSERT INTO ix_ranks (id, part, member, score) VALUES (?, ?, ?, ?) ON CONFLICT (id, part, member) DO UPDATE SET score = excluded.score", counter.id, part, member, next);
984
+ const stamp = this.bump(db);
985
+ return { entry: this.ranked(db, counter.id, part, member), consistency: consistency(stamp) };
986
+ }
987
+ rankRange(db, caller, args) {
988
+ const counter = this.counter(args.counter_id);
989
+ const part = partition(caller, counter.scope, false);
990
+ const limit = Number(args.limit);
991
+ const offset = Number(args.offset ?? 0);
992
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT || !Number.isInteger(offset) || offset < 0) invalid("limit", "out_of_range");
993
+ const descending = args.direction !== "asc";
994
+ const rows = this.all(
995
+ db,
996
+ `SELECT member, score FROM ix_ranks WHERE id = ? AND part = ? ORDER BY score ${descending ? "DESC" : "ASC"}, member ASC LIMIT ? OFFSET ?`,
997
+ counter.id,
998
+ part,
999
+ limit,
1000
+ offset
1001
+ );
1002
+ const total = Number(this.one(db, "SELECT COUNT(*) AS n FROM ix_ranks WHERE id = ? AND part = ?", counter.id, part)?.n ?? 0);
1003
+ return {
1004
+ counter_id: counter.id,
1005
+ entries: rows.map((row, at) => ({ member: row.member, score: Number(row.score), rank: offset + at + 1 })),
1006
+ total,
1007
+ consistency: consistency(this.current(db))
1008
+ };
1009
+ }
1010
+ rankOf(db, caller, args) {
1011
+ const counter = this.counter(args.counter_id);
1012
+ const entry = this.ranked(db, counter.id, partition(caller, counter.scope, false), name(args.member, "member"));
1013
+ return { entry, consistency: consistency(this.current(db)) };
1014
+ }
1015
+ enqueue(db, caller, args) {
1016
+ if (args.classification === "personal" && !serverSide(caller)) denied("personal_data_forbidden");
1017
+ const queue = name(args.queue, "queue");
1018
+ const entry = name(args.entry, "entry");
1019
+ const key = text(args.request_key, "request_key");
1020
+ const jobId = `job_${createHash("sha256").update(queue).update("\0").update(key).digest("hex").slice(0, 24)}`;
1021
+ const delay = typeof args.delay_ms === "number" && args.delay_ms > 0 ? args.delay_ms : 0;
1022
+ this.run(
1023
+ db,
1024
+ "INSERT INTO ix_jobs (job_id, queue, entry, payload, status, attempt, next_run_at) VALUES (?, ?, ?, ?, 'queued', 0, ?) ON CONFLICT (job_id) DO NOTHING",
1025
+ jobId,
1026
+ queue,
1027
+ entry,
1028
+ encode(args.payload),
1029
+ this.now() + delay
1030
+ );
1031
+ return this.job(db, jobId);
1032
+ }
1033
+ job(db, jobId) {
1034
+ const row = this.one(db, "SELECT job_id, queue, status, attempt, next_run_at FROM ix_jobs WHERE job_id = ?", jobId);
1035
+ if (row === void 0) notFound("job");
1036
+ return { job_id: row.job_id, queue: row.queue, status: row.status, attempt: Number(row.attempt), next_run_at: row.next_run_at === null ? null : Number(row.next_run_at) };
1037
+ }
1038
+ cancel(db, args) {
1039
+ const jobId = text(args.job_id, "job_id");
1040
+ this.run(db, "UPDATE ix_jobs SET status = 'cancelled', next_run_at = NULL WHERE job_id = ? AND status IN ('queued', 'retrying')", jobId);
1041
+ return this.job(db, jobId);
1042
+ }
1043
+ queueStats(db, args) {
1044
+ const queue = name(args.queue, "queue");
1045
+ const count = (status) => Number(this.one(db, "SELECT COUNT(*) AS n FROM ix_jobs WHERE queue = ? AND status = ?", queue, status)?.n ?? 0);
1046
+ const queued = count("queued") + count("retrying");
1047
+ return { queue, queued, running: count("running"), dead_lettered: count("dead_lettered"), depth: { used: queued, limit: QUEUE_DEPTH_LIMIT } };
1048
+ }
1049
+ searchIndex(db, caller, args) {
1050
+ const index = text(args.index, "index");
1051
+ if (!isIdentifier(index)) invalid("index", "malformed");
1052
+ const documents = args.documents ?? [];
1053
+ for (const document of documents) {
1054
+ const scope = document.classification === "personal" ? "personal" : document.classification === "public" ? "shared" : "interactive";
1055
+ this.run(
1056
+ db,
1057
+ "INSERT INTO ix_search (idx, part, doc, classification, body, fields) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (idx, part, doc) DO UPDATE SET classification = excluded.classification, body = excluded.body, fields = excluded.fields",
1058
+ index,
1059
+ partition(caller, scope, true),
1060
+ name(document.document_id, "documents.document_id"),
1061
+ document.classification,
1062
+ document.text ?? "",
1063
+ encode(document.fields)
1064
+ );
1065
+ }
1066
+ const used = Number(this.one(db, "SELECT COUNT(*) AS n FROM ix_search WHERE idx = ?", index)?.n ?? 0);
1067
+ return { index, indexed: documents.length, documents: { used, limit: SEARCH_DOCUMENTS_LIMIT } };
1068
+ }
1069
+ searchQuery(db, caller, args) {
1070
+ const index = text(args.index, "index");
1071
+ const limit = Number(args.limit);
1072
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_PAGE_LIMIT) invalid("limit", "out_of_range");
1073
+ const terms = (typeof args.text === "string" ? args.text : "").toLowerCase().split(/\s+/).filter((t) => t.length > 0);
1074
+ const filter = args.filter ?? [];
1075
+ const rows = this.all(db, "SELECT doc, body, fields FROM ix_search WHERE idx = ? AND part IN ('', ?)", index, `p:${caller.participant}`);
1076
+ const hits = [];
1077
+ for (const row of rows) {
1078
+ const fields = decode(row.fields);
1079
+ const keep = filter.every((p) => {
1080
+ const actual = fields?.[p.column];
1081
+ const wanted = p.values.map((v) => v.value);
1082
+ if (p.comparison === "in") return wanted.includes(actual);
1083
+ if (p.comparison === "eq") return actual === wanted[0];
1084
+ if (p.comparison === "ne") return actual !== wanted[0];
1085
+ return true;
1086
+ });
1087
+ if (!keep) continue;
1088
+ const body = String(row.body ?? "").toLowerCase();
1089
+ const score = terms.reduce((sum, term) => sum + (body.split(term).length - 1), 0);
1090
+ if (terms.length > 0 && score === 0) continue;
1091
+ hits.push({ document_id: String(row.doc), fields, score });
1092
+ }
1093
+ hits.sort((a, b) => b.score - a.score || a.document_id.localeCompare(b.document_id));
1094
+ return { index, hits: hits.slice(0, limit), consistency: consistency(this.current(db)) };
1095
+ }
1096
+ searchRemove(db, caller, args) {
1097
+ const index = text(args.index, "index");
1098
+ const parts = serverSide(caller) ? ["", `p:${caller.participant}`] : [`p:${caller.participant}`, ""];
1099
+ let removed = 0;
1100
+ for (const document of args.documents ?? []) {
1101
+ for (const part of parts) {
1102
+ const classification = this.one(db, "SELECT classification FROM ix_search WHERE idx = ? AND part = ? AND doc = ?", index, part, document)?.classification;
1103
+ if (classification === void 0) continue;
1104
+ if (classification === "public" && !serverSide(caller)) continue;
1105
+ removed += this.run(db, "DELETE FROM ix_search WHERE idx = ? AND part = ? AND doc = ?", index, part, document).changes;
1106
+ }
1107
+ }
1108
+ return { index, removed };
1109
+ }
1110
+ publish(db, args) {
1111
+ const topic = name(args.topic, "topic");
1112
+ const sequence = Number(this.one(db, "SELECT COALESCE(MAX(sequence), 0) AS n FROM ix_topics WHERE topic = ?", topic)?.n ?? 0) + 1;
1113
+ this.run(db, "INSERT INTO ix_topics (topic, sequence, message) VALUES (?, ?, ?)", topic, sequence, encode(args.message));
1114
+ return { topic, sequence };
1115
+ }
1116
+ track(db, args) {
1117
+ const events = args.events ?? [];
1118
+ let accepted = 0;
1119
+ for (const event of events) {
1120
+ if (typeof event.name !== "string" || event.name.length === 0) continue;
1121
+ this.run(db, "INSERT INTO ix_events (name, at, properties) VALUES (?, ?, ?)", event.name, Number(event.at ?? this.now()), encode(event.properties));
1122
+ accepted++;
1123
+ }
1124
+ return { accepted, dropped: events.length - accepted };
1125
+ }
1126
+ record(db, args) {
1127
+ this.run(db, "INSERT INTO ix_metrics (name, kind, at, value) VALUES (?, ?, ?, ?)", name(args.name, "name"), text(args.kind, "kind"), this.now(), Number(args.value));
1128
+ return { accepted: 1, dropped: 0 };
1129
+ }
1130
+ summary(db, caller, args) {
1131
+ if (!serverSide(caller)) denied("location_not_allowed");
1132
+ const metric = name(args.name, "name");
1133
+ const windows = { hour: 36e5, day: 864e5, week: 6048e5 };
1134
+ const span = windows[text(args.window, "window")] ?? invalid("window", "malformed");
1135
+ const values = this.all(db, "SELECT kind, value FROM ix_metrics WHERE name = ? AND at >= ? ORDER BY value", metric, this.now() - span);
1136
+ const numbers = values.map((v) => Number(v.value));
1137
+ const at = (q) => numbers.length === 0 ? null : numbers[Math.min(numbers.length - 1, Math.floor(q * numbers.length))];
1138
+ return {
1139
+ name: metric,
1140
+ kind: values[0]?.kind ?? "count",
1141
+ window: args.window,
1142
+ count: numbers.length,
1143
+ sum: numbers.reduce((s, v) => s + v, 0),
1144
+ minimum: numbers.length === 0 ? null : numbers[0],
1145
+ maximum: numbers.length === 0 ? null : numbers[numbers.length - 1],
1146
+ p50: at(0.5),
1147
+ p99: at(0.99)
1148
+ };
1149
+ }
1150
+ };
1151
+
1152
+ export { DATA_FAMILIES, DEV_DATA_ROUTE, LOCAL_ENVIRONMENT, LocalData, localDataDir, localEnvironments, resetLocalData };