@silverbulletmd/silverbullet 2.9.0 → 2.11.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 (123) hide show
  1. package/README.md +38 -25
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +24 -14
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/asset.ts +61 -23
  15. package/client/plugos/syscalls/clientStore.ts +32 -6
  16. package/client/plugos/syscalls/client_code_widget.ts +9 -4
  17. package/client/plugos/syscalls/code_widget.ts +26 -11
  18. package/client/plugos/syscalls/config.ts +146 -30
  19. package/client/plugos/syscalls/datastore.ts +146 -40
  20. package/client/plugos/syscalls/editor.ts +1518 -722
  21. package/client/plugos/syscalls/event.ts +44 -13
  22. package/client/plugos/syscalls/fetch.ts +120 -83
  23. package/client/plugos/syscalls/icon.ts +46 -0
  24. package/client/plugos/syscalls/index.ts +267 -121
  25. package/client/plugos/syscalls/jsonschema.ts +107 -9
  26. package/client/plugos/syscalls/language.ts +38 -12
  27. package/client/plugos/syscalls/markdown.ts +166 -50
  28. package/client/plugos/syscalls/mq.ts +92 -20
  29. package/client/plugos/syscalls/navigator.ts +143 -0
  30. package/client/plugos/syscalls/schema_introspection.ts +33 -0
  31. package/client/plugos/syscalls/search.ts +44 -0
  32. package/client/plugos/syscalls/service_registry.ts +74 -20
  33. package/client/plugos/syscalls/shell.ts +51 -21
  34. package/client/plugos/syscalls/space.ts +412 -113
  35. package/client/plugos/syscalls/sync.ts +103 -20
  36. package/client/plugos/syscalls/system.ts +274 -139
  37. package/client/plugos/system.ts +28 -10
  38. package/client/plugos/worker_runtime.ts +0 -2
  39. package/client/space_lua/aggregates.ts +0 -16
  40. package/client/space_lua/api_documentation.ts +107 -0
  41. package/client/space_lua/ast.ts +13 -5
  42. package/client/space_lua/ast_narrow.ts +0 -5
  43. package/client/space_lua/budget.ts +113 -0
  44. package/client/space_lua/budget_ui.ts +49 -0
  45. package/client/space_lua/eval.ts +125 -92
  46. package/client/space_lua/labels.ts +0 -4
  47. package/client/space_lua/numeric.ts +0 -5
  48. package/client/space_lua/parse.ts +243 -125
  49. package/client/space_lua/pretty_print.ts +128 -54
  50. package/client/space_lua/quarantine.ts +127 -0
  51. package/client/space_lua/query_collection.ts +7 -51
  52. package/client/space_lua/query_env.ts +0 -2
  53. package/client/space_lua/render_lua_markdown.ts +2 -7
  54. package/client/space_lua/render_widget.ts +173 -0
  55. package/client/space_lua/runtime.ts +133 -92
  56. package/client/space_lua/stdlib/crypto.ts +8 -3
  57. package/client/space_lua/stdlib/encoding.ts +27 -9
  58. package/client/space_lua/stdlib/format.ts +0 -12
  59. package/client/space_lua/stdlib/js.ts +136 -23
  60. package/client/space_lua/stdlib/load.ts +24 -16
  61. package/client/space_lua/stdlib/math.ts +331 -133
  62. package/client/space_lua/stdlib/net.ts +57 -13
  63. package/client/space_lua/stdlib/os.ts +111 -54
  64. package/client/space_lua/stdlib/pattern.ts +10 -15
  65. package/client/space_lua/stdlib/space_lua.ts +344 -21
  66. package/client/space_lua/stdlib/string.ts +282 -104
  67. package/client/space_lua/stdlib/string_pack.ts +58 -22
  68. package/client/space_lua/stdlib/table.ts +195 -45
  69. package/client/space_lua/stdlib.ts +354 -125
  70. package/client/space_lua/syscalls.ts +270 -0
  71. package/client/space_lua/tonumber.ts +0 -9
  72. package/dist/plug-compile.js +2 -3
  73. package/package.json +15 -5
  74. package/plug-api/lib/async.ts +1 -6
  75. package/plug-api/lib/collation.ts +22 -0
  76. package/plug-api/lib/crypto.ts +16 -15
  77. package/plug-api/lib/dates.ts +33 -0
  78. package/plug-api/lib/fuzzy.ts +282 -0
  79. package/plug-api/lib/json.ts +0 -6
  80. package/plug-api/lib/limited_map.ts +0 -2
  81. package/plug-api/lib/link_write.ts +27 -0
  82. package/plug-api/{ui → lib}/panel_styles.ts +4 -3
  83. package/plug-api/lib/ref.ts +124 -5
  84. package/plug-api/lib/resolve.ts +0 -1
  85. package/plug-api/lib/resolve_path.ts +290 -0
  86. package/plug-api/lib/shortcut.ts +17 -1
  87. package/plug-api/lib/tags.ts +0 -3
  88. package/plug-api/lib/transclusion.ts +28 -1
  89. package/plug-api/lib/tree.ts +0 -5
  90. package/plug-api/lib/yaml.ts +3 -42
  91. package/plug-api/syscall.ts +1 -5
  92. package/plug-api/syscalls/config.ts +7 -5
  93. package/plug-api/syscalls/editor.ts +70 -208
  94. package/plug-api/syscalls/icon.ts +18 -0
  95. package/plug-api/syscalls/index.ts +39 -5
  96. package/plug-api/syscalls/jsonschema.ts +9 -0
  97. package/plug-api/syscalls/lua.ts +7 -0
  98. package/plug-api/syscalls/search.ts +11 -0
  99. package/plug-api/syscalls/shell.ts +0 -1
  100. package/plug-api/syscalls/space.ts +122 -5
  101. package/plug-api/syscalls/sync.ts +23 -2
  102. package/plug-api/syscalls/system.ts +53 -0
  103. package/plug-api/syscalls.ts +2 -0
  104. package/plug-api/system_mock.ts +15 -2
  105. package/plug-api/types/client.ts +4 -3
  106. package/plug-api/types/datastore.ts +11 -2
  107. package/plug-api/types/index.ts +62 -1
  108. package/plug-api/types/manifest.ts +22 -4
  109. package/plug-api/types/profile.ts +13 -0
  110. package/plug-api/types/revisions.ts +67 -0
  111. package/plug-api/ui/cx.ts +1 -3
  112. package/plug-api/ui/description.ts +64 -0
  113. package/plug-api/ui/hover.ts +69 -0
  114. package/plug-api/ui/index.ts +61 -2
  115. package/plug-api/ui/scroll.ts +30 -0
  116. package/plug-api/ui/slugify.ts +44 -0
  117. package/plug-api/ui/tree_model.ts +263 -0
  118. package/plug-api/ui/tree_types.ts +66 -0
  119. package/plug-api/ui/use_fit_collapse.ts +67 -0
  120. package/plugs/builtin_plugs.ts +0 -1
  121. package/plugs/index/types.ts +9 -0
  122. package/client/plugos/syscalls/lua.ts +0 -81
  123. package/plug-api/lib/memory_cache.ts +0 -21
@@ -29,9 +29,28 @@ function resolveOptions(opts?: PrintOptions): ResolvedOptions {
29
29
  }
30
30
 
31
31
  const RESERVED = new Set([
32
- "and", "break", "do", "else", "elseif", "end", "false", "for", "function",
33
- "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then",
34
- "true", "until", "while",
32
+ "and",
33
+ "break",
34
+ "do",
35
+ "else",
36
+ "elseif",
37
+ "end",
38
+ "false",
39
+ "for",
40
+ "function",
41
+ "goto",
42
+ "if",
43
+ "in",
44
+ "local",
45
+ "nil",
46
+ "not",
47
+ "or",
48
+ "repeat",
49
+ "return",
50
+ "then",
51
+ "true",
52
+ "until",
53
+ "while",
35
54
  ]);
36
55
 
37
56
  function isLuaIdentifier(s: string): boolean {
@@ -39,16 +58,26 @@ function isLuaIdentifier(s: string): boolean {
39
58
  }
40
59
 
41
60
  const BINARY_PREC: Record<string, number> = {
42
- "or": 1,
43
- "and": 2,
44
- "<": 3, ">": 3, "<=": 3, ">=": 3, "~=": 3, "==": 3,
61
+ or: 1,
62
+ and: 2,
63
+ "<": 3,
64
+ ">": 3,
65
+ "<=": 3,
66
+ ">=": 3,
67
+ "~=": 3,
68
+ "==": 3,
45
69
  "|": 4,
46
70
  "~": 5, // binary bitwise xor
47
71
  "&": 6,
48
- "<<": 7, ">>": 7,
72
+ "<<": 7,
73
+ ">>": 7,
49
74
  "..": 8,
50
- "+": 9, "-": 9,
51
- "*": 10, "/": 10, "//": 10, "%": 10,
75
+ "+": 9,
76
+ "-": 9,
77
+ "*": 10,
78
+ "/": 10,
79
+ "//": 10,
80
+ "%": 10,
52
81
  "^": 12,
53
82
  };
54
83
  const UNARY_PREC = 11;
@@ -62,7 +91,7 @@ function exprPrec(e: LuaExpression): number {
62
91
  }
63
92
 
64
93
  class Printer {
65
- constructor(private opts: ResolvedOptions) { }
94
+ constructor(private opts: ResolvedOptions) {}
66
95
 
67
96
  private indent(depth: number): string {
68
97
  return " ".repeat(depth * this.opts.indentWidth);
@@ -129,22 +158,29 @@ class Printer {
129
158
  case "TableConstructor":
130
159
  return this.tableConstructor(e.fields, depth);
131
160
  case "FunctionDefinition":
132
- return "function" +
133
- this.functionRest(e.body.parameters, e.body.block, depth);
161
+ return `function${this.functionRest(
162
+ e.body.parameters,
163
+ e.body.block,
164
+ depth,
165
+ )}`;
134
166
  case "FunctionCall":
135
167
  return this.functionCall(e, depth);
136
168
  case "Query": {
137
- const lines = e.clauses.map((c) =>
138
- this.indent(depth + 1) + this.queryClause(c, depth + 1)
169
+ const lines = e.clauses.map(
170
+ (c) => this.indent(depth + 1) + this.queryClause(c, depth + 1),
139
171
  );
140
172
  return `query[[\n${lines.join("\n")}\n${this.indent(depth)}]]`;
141
173
  }
142
174
  case "FilteredCall":
143
- return `${this.expression(e.call, depth)} filter ${this.expression(e.filter, depth)
144
- }`;
175
+ return `${this.expression(e.call, depth)} filter ${this.expression(
176
+ e.filter,
177
+ depth,
178
+ )}`;
145
179
  case "AggregateCall":
146
- return `${this.expression(e.call, depth)} order by ${this.orderByList(e.orderBy, depth)
147
- }`;
180
+ return `${this.expression(e.call, depth)} order by ${this.orderByList(
181
+ e.orderBy,
182
+ depth,
183
+ )}`;
148
184
  default:
149
185
  throw new Error(
150
186
  `pretty_print: unsupported expression type: ${(e as any).type}`,
@@ -193,8 +229,8 @@ class Printer {
193
229
  private tableConstructor(fields: LuaTableField[], depth: number): string {
194
230
  if (fields.length === 0) return "{}";
195
231
  if (fields.length === 1) return `{${this.field(fields[0], depth)}}`;
196
- const lines = fields.map((f) =>
197
- this.indent(depth + 1) + this.field(f, depth + 1)
232
+ const lines = fields.map(
233
+ (f) => this.indent(depth + 1) + this.field(f, depth + 1),
198
234
  );
199
235
  const tail = this.opts.trailingComma ? "," : "";
200
236
  return `{\n${lines.join(",\n")}${tail}\n${this.indent(depth)}}`;
@@ -224,7 +260,8 @@ class Printer {
224
260
  ? ` order by ${this.orderByList(e.orderBy, depth)}`
225
261
  : "";
226
262
  if (
227
- !e.orderBy && args.length === 1 &&
263
+ !e.orderBy &&
264
+ args.length === 1 &&
228
265
  (args[0].type === "TableConstructor" || args[0].type === "String")
229
266
  ) {
230
267
  return `${base} ${this.expression(args[0], depth)}`;
@@ -254,8 +291,10 @@ class Printer {
254
291
  case "Where":
255
292
  return `where ${this.expression(c.expression, depth)}`;
256
293
  case "Limit":
257
- return `limit ${this.expression(c.limit, depth)}` +
258
- (c.offset ? ` offset ${this.expression(c.offset, depth)}` : "");
294
+ return (
295
+ `limit ${this.expression(c.limit, depth)}` +
296
+ (c.offset ? ` offset ${this.expression(c.offset, depth)}` : "")
297
+ );
259
298
  case "Offset":
260
299
  return `offset ${this.expression(c.offset, depth)}`;
261
300
  case "OrderBy":
@@ -271,16 +310,32 @@ class Printer {
271
310
 
272
311
  block(b: LuaBlock, depth: number): string {
273
312
  const stmts = b.statements.filter((s) => s.type !== "Semicolon");
274
- const isDef = (s: LuaStatement) =>
313
+ const items: (LuaStatement | NonNullable<LuaBlock["comments"]>[number])[] =
314
+ [...stmts, ...(b.comments ?? [])].sort(
315
+ (a, z) => (a.ctx.from ?? 0) - (z.ctx.from ?? 0),
316
+ );
317
+ const isDef = (s: (typeof items)[number]) =>
275
318
  s.type === "Function" || s.type === "LocalFunction";
276
319
  let out = "";
277
- for (let i = 0; i < stmts.length; i++) {
320
+ for (let i = 0; i < items.length; i++) {
278
321
  if (i > 0) {
279
322
  out += "\n";
280
323
  // blank line separating function definitions from neighbours
281
- if (isDef(stmts[i - 1]) || isDef(stmts[i])) out += "\n";
324
+ const previous = items[i - 1];
325
+ const isDocumentationPair =
326
+ previous.type === "Comment" &&
327
+ previous.text.startsWith("---") &&
328
+ isDef(items[i]);
329
+ if (!isDocumentationPair && (isDef(previous) || isDef(items[i]))) {
330
+ out += "\n";
331
+ }
282
332
  }
283
- out += this.statement(stmts[i], depth);
333
+ const item = items[i];
334
+ out +=
335
+ item.type === "Comment"
336
+ ? this.indent(depth) +
337
+ item.text.replaceAll("\n", `\n${this.indent(depth)}`)
338
+ : this.statement(item, depth);
284
339
  }
285
340
  return out;
286
341
  }
@@ -290,11 +345,14 @@ class Printer {
290
345
  switch (s.type) {
291
346
  case "Local": {
292
347
  const names = s.names
293
- .map((n) =>
294
- n.name +
295
- (n.attribute
296
- ? ` <${n.attribute}>`
297
- : (n.attributes?.length ? ` <${n.attributes[0]}>` : ""))
348
+ .map(
349
+ (n) =>
350
+ n.name +
351
+ (n.attribute
352
+ ? ` <${n.attribute}>`
353
+ : n.attributes?.length
354
+ ? ` <${n.attributes[0]}>`
355
+ : ""),
298
356
  )
299
357
  .join(", ");
300
358
  const exprs = s.expressions?.length
@@ -303,12 +361,16 @@ class Printer {
303
361
  return `${ind}local ${names}${exprs}`;
304
362
  }
305
363
  case "Assignment":
306
- return `${ind}${s.variables.map((v) => this.expression(v, depth)).join(", ")
307
- } = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`;
364
+ return `${ind}${s.variables
365
+ .map((v) => this.expression(v, depth))
366
+ .join(
367
+ ", ",
368
+ )} = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`;
308
369
  case "Return":
309
370
  return s.expressions.length
310
- ? `${ind}return ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
311
- }`
371
+ ? `${ind}return ${s.expressions
372
+ .map((e) => this.expression(e, depth))
373
+ .join(", ")}`
312
374
  : `${ind}return`;
313
375
  case "FunctionCallStatement":
314
376
  return ind + this.expression(s.call, depth);
@@ -328,7 +390,8 @@ class Printer {
328
390
  const kw = i === 0 ? "if" : "elseif";
329
391
  const head = i === 0 ? `${ind}${kw} ` : `\n${ind}${kw} `;
330
392
  const body = this.block(c.block, depth + 1);
331
- out += `${head}${this.expression(c.condition, depth)} then` +
393
+ out +=
394
+ `${head}${this.expression(c.condition, depth)} then` +
332
395
  (body ? `\n${body}` : "");
333
396
  });
334
397
  if (s.elseBlock) {
@@ -340,34 +403,48 @@ class Printer {
340
403
  }
341
404
  case "While": {
342
405
  const body = this.block(s.block, depth + 1);
343
- return `${ind}while ${this.expression(s.condition, depth)} do` +
344
- (body ? `\n${body}` : "") + `\n${ind}end`;
406
+ return (
407
+ `${ind}while ${this.expression(s.condition, depth)} do` +
408
+ (body ? `\n${body}` : "") +
409
+ `\n${ind}end`
410
+ );
345
411
  }
346
412
  case "Repeat": {
347
413
  const body = this.block(s.block, depth + 1);
348
- return `${ind}repeat` + (body ? `\n${body}` : "") +
349
- `\n${ind}until ${this.expression(s.condition, depth)}`;
414
+ return (
415
+ `${ind}repeat` +
416
+ (body ? `\n${body}` : "") +
417
+ `\n${ind}until ${this.expression(s.condition, depth)}`
418
+ );
350
419
  }
351
420
  case "For": {
352
421
  const step = s.step ? `, ${this.expression(s.step, depth)}` : "";
353
422
  const body = this.block(s.block, depth + 1);
354
- return `${ind}for ${s.name} = ${this.expression(s.start, depth)}, ${this.expression(s.end, depth)
355
- }${step} do${body ? `\n${body}` : ""}\n${ind}end`;
423
+ return `${ind}for ${s.name} = ${this.expression(s.start, depth)}, ${this.expression(
424
+ s.end,
425
+ depth,
426
+ )}${step} do${body ? `\n${body}` : ""}\n${ind}end`;
356
427
  }
357
428
  case "ForIn": {
358
429
  const body = this.block(s.block, depth + 1);
359
- return `${ind}for ${s.names.join(", ")} in ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
360
- } do${body ? `\n${body}` : ""}\n${ind}end`;
430
+ return `${ind}for ${s.names.join(", ")} in ${s.expressions
431
+ .map((e) => this.expression(e, depth))
432
+ .join(", ")} do${body ? `\n${body}` : ""}\n${ind}end`;
361
433
  }
362
434
  case "Function": {
363
- const name = s.name.propNames.join(".") +
435
+ const name =
436
+ s.name.propNames.join(".") +
364
437
  (s.name.colonName ? `:${s.name.colonName}` : "");
365
- return `${ind}function ${name}` +
366
- this.functionRest(s.body.parameters, s.body.block, depth);
438
+ return (
439
+ `${ind}function ${name}` +
440
+ this.functionRest(s.body.parameters, s.body.block, depth)
441
+ );
367
442
  }
368
443
  case "LocalFunction":
369
- return `${ind}local function ${s.name}` +
370
- this.functionRest(s.body.parameters, s.body.block, depth);
444
+ return (
445
+ `${ind}local function ${s.name}` +
446
+ this.functionRest(s.body.parameters, s.body.block, depth)
447
+ );
371
448
  default:
372
449
  throw new Error(
373
450
  `pretty_print: unsupported statement type: ${(s as any).type}`,
@@ -383,9 +460,6 @@ export function prettyPrintExpression(
383
460
  return new Printer(resolveOptions(opts)).expression(expr, 0);
384
461
  }
385
462
 
386
- export function prettyPrintBlock(
387
- block: LuaBlock,
388
- opts?: PrintOptions,
389
- ): string {
463
+ export function prettyPrintBlock(block: LuaBlock, opts?: PrintOptions): string {
390
464
  return new Printer(resolveOptions(opts)).block(block, 0);
391
465
  }
@@ -0,0 +1,127 @@
1
+ export const QUARANTINE_KEY = "spaceLuaQuarantine";
2
+
3
+ export type QuarantineStore = {
4
+ getItem(k: string): string | null;
5
+ setItem(k: string, v: string): void;
6
+ };
7
+
8
+ function defaultStore(): QuarantineStore {
9
+ return localStorage;
10
+ }
11
+
12
+ /**
13
+ * Dashboard can host multiple spaces on the same origin, sharing
14
+ * localStorage. Namespace by document.baseURI the same way boot.ts's
15
+ * cachedFetch does (and client.ts's deriveDbName), so one space's
16
+ * quarantine entries never collide with or get pruned by another's.
17
+ */
18
+ export function quarantineStorageKey(): string {
19
+ const namespace = typeof document !== "undefined" ? document.baseURI : "";
20
+ return `silverbullet.${namespace}.${QUARANTINE_KEY}`;
21
+ }
22
+
23
+ export function hashScript(source: string): string {
24
+ let h = 2166136261;
25
+ for (let i = 0; i < source.length; i++) {
26
+ h ^= source.charCodeAt(i);
27
+ h = Math.imul(h, 16777619);
28
+ }
29
+ return (h >>> 0).toString(36);
30
+ }
31
+
32
+ function read(store: QuarantineStore): Record<string, string> {
33
+ let raw: string | null;
34
+ try {
35
+ raw = store.getItem(quarantineStorageKey());
36
+ } catch {
37
+ return {};
38
+ }
39
+ if (!raw) {
40
+ return {};
41
+ }
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
45
+ ? parsed
46
+ : {};
47
+ } catch {
48
+ return {};
49
+ }
50
+ }
51
+
52
+ function write(store: QuarantineStore, v: Record<string, string>): void {
53
+ try {
54
+ store.setItem(quarantineStorageKey(), JSON.stringify(v));
55
+ } catch {
56
+ // Quarantine is a convenience: a store that refuses to persist must never
57
+ // prevent scripts from loading.
58
+ }
59
+ }
60
+
61
+ export function isQuarantined(
62
+ ref: string,
63
+ source: string,
64
+ store: QuarantineStore = defaultStore(),
65
+ ): boolean {
66
+ const entries = read(store);
67
+ const stored = entries[ref];
68
+ if (stored === undefined) {
69
+ return false;
70
+ }
71
+ if (stored === hashScript(source)) {
72
+ return true;
73
+ }
74
+ delete entries[ref];
75
+ write(store, entries);
76
+ return false;
77
+ }
78
+
79
+ export function quarantine(
80
+ ref: string,
81
+ source: string,
82
+ store: QuarantineStore = defaultStore(),
83
+ ): void {
84
+ const entries = read(store);
85
+ entries[ref] = hashScript(source);
86
+ write(store, entries);
87
+ }
88
+
89
+ export function unquarantine(
90
+ ref: string,
91
+ store: QuarantineStore = defaultStore(),
92
+ ): void {
93
+ const entries = read(store);
94
+ delete entries[ref];
95
+ write(store, entries);
96
+ }
97
+
98
+ export function listQuarantined(
99
+ store: QuarantineStore = defaultStore(),
100
+ ): string[] {
101
+ return Object.keys(read(store));
102
+ }
103
+
104
+ /**
105
+ * Drops quarantine entries whose ref no longer appears among the live
106
+ * scripts. A ref changes whenever the script it belongs to moves (e.g. an
107
+ * edit shifts its page@charOffset), so the hash-mismatch self-heal in
108
+ * isQuarantined never revisits the old ref to prune it. Call this with the
109
+ * full live ref list before quarantine state is read for display.
110
+ */
111
+ export function reconcileQuarantine(
112
+ liveRefs: string[],
113
+ store: QuarantineStore = defaultStore(),
114
+ ): void {
115
+ const entries = read(store);
116
+ const live = new Set(liveRefs);
117
+ let changed = false;
118
+ for (const ref of Object.keys(entries)) {
119
+ if (!live.has(ref)) {
120
+ delete entries[ref];
121
+ changed = true;
122
+ }
123
+ }
124
+ if (changed) {
125
+ write(store, entries);
126
+ }
127
+ }
@@ -33,6 +33,7 @@ import type { DataStore } from "../data/datastore.ts";
33
33
  import type { KvPrimitives } from "../data/kv_primitives.ts";
34
34
 
35
35
  import type { QueryCollationConfig } from "../../plug-api/types/config.ts";
36
+ import { compareCollated } from "../../plug-api/lib/collation.ts";
36
37
 
37
38
  import type { KvKey } from "../../plug-api/types/datastore.ts";
38
39
 
@@ -77,7 +78,6 @@ function buildGroupItemEnv(
77
78
  itemEnv.setLocal("group", groupVal);
78
79
  }
79
80
 
80
- // Unpack named fields from multi-key LuaTable keys
81
81
  if (keyVal instanceof LuaTable) {
82
82
  for (const k of luaKeys(keyVal)) {
83
83
  if (typeof k !== "string") continue;
@@ -85,12 +85,8 @@ function buildGroupItemEnv(
85
85
  }
86
86
  }
87
87
 
88
- // Bind all `group by` aliases/names to their key values. For
89
- // single key bind the name to the scalar `keyVal`. For multi-key
90
- // bind each name to the field from the key table.
91
88
  if (groupByNames && groupByNames.length > 0) {
92
89
  if (!(keyVal instanceof LuaTable)) {
93
- // Bind all names to scalar
94
90
  for (const gbn of groupByNames) {
95
91
  itemEnv.setLocal(gbn, keyVal);
96
92
  }
@@ -109,8 +105,6 @@ function buildGroupItemEnv(
109
105
  }
110
106
 
111
107
  /**
112
- * Build an environment for evaluating per-item expressions in queries.
113
- *
114
108
  * When `objectVariable` is NOT set: item fields are unpacked as locals
115
109
  * and shadow any globals. The item is also bound to `_`.
116
110
  *
@@ -128,7 +122,6 @@ function buildItemEnvLocal(
128
122
  if (objectVariable) {
129
123
  itemEnv.setLocal(objectVariable, item);
130
124
  } else {
131
- // Unpack item fields as locals so unqualified access works
132
125
  itemEnv.setLocal("_", item);
133
126
  if (item instanceof LuaTable) {
134
127
  for (const key of luaKeys(item)) {
@@ -157,26 +150,15 @@ export type LuaGroupByEntry = {
157
150
  alias?: string;
158
151
  };
159
152
 
160
- /**
161
- * Represents a query for a collection
162
- */
163
153
  export type LuaCollectionQuery = {
164
154
  objectVariable?: string;
165
- // The filter expression evaluated with Lua
166
155
  where?: LuaExpression;
167
- // The order by expression evaluated with Lua
168
156
  orderBy?: LuaOrderBy[];
169
- // The select expression evaluated with Lua
170
157
  select?: LuaExpression;
171
- // The limit of the query
172
158
  limit?: number;
173
- // The offset of the query
174
159
  offset?: number;
175
- // Whether to return only distinct values
176
160
  distinct?: boolean;
177
- // The group by entries evaluated with Lua
178
161
  groupBy?: LuaGroupByEntry[];
179
- // The having expression evaluated with Lua
180
162
  having?: LuaExpression;
181
163
  };
182
164
 
@@ -189,11 +171,8 @@ export interface LuaQueryCollection {
189
171
  ): Promise<any[]>;
190
172
  }
191
173
 
192
- /**
193
- * Implements a query collection for a regular JavaScript array
194
- */
195
174
  export class ArrayQueryCollection<T> implements LuaQueryCollection {
196
- constructor(private readonly array: T[]) { }
175
+ constructor(private readonly array: T[]) {}
197
176
  query(
198
177
  query: LuaCollectionQuery,
199
178
  env: LuaEnv,
@@ -204,7 +183,6 @@ export class ArrayQueryCollection<T> implements LuaQueryCollection {
204
183
  }
205
184
  }
206
185
 
207
- // Wrap any object, array, or LuaQueryCollection as a queryable collection
208
186
  export function toCollection(obj: any): LuaQueryCollection {
209
187
  if (
210
188
  obj instanceof ArrayQueryCollection ||
@@ -515,7 +493,6 @@ function resolveUsing(
515
493
  return new LuaFunction(using, env);
516
494
  }
517
495
 
518
- // Compare values using a custom comparator with SWO violation detection
519
496
  async function usingCompare(
520
497
  luaCmp: LuaValue,
521
498
  aVal: any,
@@ -621,7 +598,6 @@ async function sortKeyCompare(
621
598
  const aVal = aKeys[idx];
622
599
  const bVal = bKeys[idx];
623
600
 
624
- // Handle nulls positioning
625
601
  const aIsNull = aVal === null || aVal === undefined || isSqlNull(aVal);
626
602
  const bIsNull = bVal === null || bVal === undefined || isSqlNull(bVal);
627
603
  if (aIsNull || bIsNull) {
@@ -646,25 +622,16 @@ async function sortKeyCompare(
646
622
  idx,
647
623
  );
648
624
  if (cmp !== 0) return cmp;
649
- } else if (
650
- collation?.enabled &&
651
- typeof aVal === "string" &&
652
- typeof bVal === "string"
653
- ) {
654
- const order = collator.compare(aVal, bVal);
625
+ } else {
626
+ const order = compareCollated(aVal, bVal, collation, collator);
655
627
  if (order !== 0) {
656
628
  return desc ? -order : order;
657
629
  }
658
- } else if (aVal < bVal) {
659
- return desc ? 1 : -1;
660
- } else if (aVal > bVal) {
661
- return desc ? -1 : 1;
662
630
  }
663
631
  }
664
632
  return 0;
665
633
  }
666
634
 
667
- // Build a select-result table from a non-aggregate select expression
668
635
  async function evalSelectExpression(
669
636
  selectExpr: LuaExpression,
670
637
  itemEnv: LuaEnv,
@@ -711,14 +678,11 @@ export async function applyQuery(
711
678
 
712
679
  const grouped = !!query.groupBy;
713
680
 
714
- // Collect group-by key names for unpacking into the post-group environment.
715
681
  let groupByNames: string[] | undefined;
716
682
 
717
683
  if (query.groupBy) {
718
- // Extract expressions and names from `group by` entries
719
684
  const groupByEntries = query.groupBy;
720
685
 
721
- // Derive canonical name (explicit alias first, or from expression)
722
686
  groupByNames = groupByEntries
723
687
  .map((entry) => {
724
688
  if (entry.alias) return entry.alias;
@@ -740,7 +704,6 @@ export async function applyQuery(
740
704
  const entry = groupByEntries[ei];
741
705
  const v = await evalExpression(entry.expr, itemEnv, sf);
742
706
  keyParts.push(v);
743
- // Use alias if provided, or from expression
744
707
  const name =
745
708
  entry.alias ??
746
709
  (entry.expr.type === "Variable" ? entry.expr.name : undefined) ??
@@ -769,11 +732,9 @@ export async function applyQuery(
769
732
  keyVal = keyParts[0];
770
733
  } else {
771
734
  const kt = new LuaTable();
772
- // Always populate array indices from keyParts
773
735
  for (let i = 0; i < keyParts.length; i++) {
774
736
  kt.rawSetArrayIndex(i + 1, keyParts[i]);
775
737
  }
776
- // Additionally set named fields for Variable/PropertyAccess exprs
777
738
  for (const name in keyRecord) {
778
739
  void kt.rawSet(name, keyRecord[name]);
779
740
  }
@@ -842,12 +803,11 @@ export async function applyQuery(
842
803
 
843
804
  const mkEnv = grouped
844
805
  ? (ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame) =>
845
- buildGroupItemEnv(ov, groupByNames, item, e, s)
806
+ buildGroupItemEnv(ov, groupByNames, item, e, s)
846
807
  : buildItemEnvLocal;
847
808
 
848
809
  let selectResults: any[] | undefined;
849
810
 
850
- // Pre-compute select for grouped + ordered queries
851
811
  if (grouped && query.select && query.orderBy) {
852
812
  const selectExpr = query.select;
853
813
  selectResults = [];
@@ -879,7 +839,6 @@ export async function applyQuery(
879
839
  violated.push(false);
880
840
  }
881
841
 
882
- // Decorate: pre-compute all sort keys once (Schwartzian transform)
883
842
  const sortKeys = await precomputeSortKeys(
884
843
  results,
885
844
  query.orderBy,
@@ -892,10 +851,8 @@ export async function applyQuery(
892
851
  config,
893
852
  );
894
853
 
895
- // Tag each result with its original index for stable sorting
896
854
  const tagged = results.map((val, idx) => ({ val, idx }));
897
855
 
898
- // Sort: compare cached keys only, no Lua eval in comparator
899
856
  await asyncMergeSort(tagged, (a, b) =>
900
857
  sortKeyCompare(
901
858
  a,
@@ -911,7 +868,6 @@ export async function applyQuery(
911
868
  ),
912
869
  );
913
870
 
914
- // Check for SWO violations in comparators
915
871
  for (let i = 0; i < violated.length; i++) {
916
872
  if (violated[i]) {
917
873
  throw new LuaRuntimeError(
@@ -1002,7 +958,7 @@ export async function queryLua<T = any>(
1002
958
  for await (let { key, value } of kv.query({ prefix })) {
1003
959
  if (enricher) {
1004
960
  value = enricher(key, value);
1005
- // Enrichers may return undefined to filter a row out
961
+ // Enrichers may return undefined to filter a row out
1006
962
  if (value === undefined) continue;
1007
963
  }
1008
964
  results.push(value);
@@ -1056,7 +1012,7 @@ export class DataStoreQueryCollection implements LuaQueryCollection {
1056
1012
  constructor(
1057
1013
  private readonly dataStore: DataStore,
1058
1014
  readonly prefix: string[],
1059
- ) { }
1015
+ ) {}
1060
1016
  query(
1061
1017
  query: LuaCollectionQuery,
1062
1018
  env: LuaEnv,
@@ -13,11 +13,9 @@ export function buildItemEnv(
13
13
  ): LuaEnv {
14
14
  const itemEnv = new LuaEnv(env);
15
15
  if (!objectVariable) {
16
- // Inject all item keys as variables
17
16
  for (const key of luaKeys(item)) {
18
17
  itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
19
18
  }
20
- // As well as _
21
19
  itemEnv.setLocal("_", item);
22
20
  } else {
23
21
  itemEnv.setLocal(objectVariable, item);