@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
@@ -2,14 +2,15 @@ import type { Hook } from "./types.ts";
2
2
  import { EventEmitter } from "./event.ts";
3
3
  import type { SandboxFactory } from "./sandboxes/sandbox.ts";
4
4
  import { Plug } from "./plug.ts";
5
- import { InMemoryManifestCache, type ManifestCache } from "./manifest_cache.ts";
5
+ import type { ManifestCache } from "./manifest_cache.ts";
6
6
  import {
7
7
  builtinPlugNames,
8
8
  builtinPlugPaths,
9
9
  } from "../../plugs/builtin_plugs.ts";
10
+ import type { LuaFunctionDocumentation } from "../../plug-api/types/index.ts";
10
11
 
11
12
  export interface SysCallMapping {
12
- [key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
13
+ [key: string]: SyscallSignature | SyscallDefinition;
13
14
  }
14
15
 
15
16
  export type SystemEvents<HookT> = {
@@ -26,10 +27,14 @@ export type SyscallContext = {
26
27
 
27
28
  type SyscallSignature = (...args: any[]) => Promise<any> | any;
28
29
 
29
- type Syscall = {
30
+ export type SyscallDefinition = {
31
+ callback: SyscallSignature;
32
+ } & LuaFunctionDocumentation;
33
+
34
+ type RegisteredSyscall = {
30
35
  requiredPermissions: string[];
31
36
  callback: SyscallSignature;
32
- };
37
+ } & LuaFunctionDocumentation;
33
38
 
34
39
  export type SystemOptions = {
35
40
  manifestCache?: ManifestCache<any>;
@@ -37,7 +42,7 @@ export type SystemOptions = {
37
42
  };
38
43
 
39
44
  export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
40
- registeredSyscalls = new Map<string, Syscall>();
45
+ registeredSyscalls = new Map<string, RegisteredSyscall>();
41
46
  protected plugs = new Map<string, Plug<HookT>>();
42
47
  protected enabledHooks = new Set<Hook<HookT>>();
43
48
 
@@ -50,7 +55,12 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
50
55
  ) {
51
56
  super();
52
57
  if (!options.manifestCache) {
53
- options.manifestCache = new InMemoryManifestCache();
58
+ options.manifestCache = {
59
+ async getManifest(plug) {
60
+ await plug.sandbox.init();
61
+ return plug.sandbox.manifest!;
62
+ },
63
+ };
54
64
  }
55
65
  }
56
66
 
@@ -68,10 +78,21 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
68
78
  ...registrationObjects: SysCallMapping[]
69
79
  ) {
70
80
  for (const registrationObject of registrationObjects) {
71
- for (const [name, callback] of Object.entries(registrationObject)) {
81
+ for (const [name, registration] of Object.entries(registrationObject)) {
82
+ const definition =
83
+ typeof registration === "function"
84
+ ? { callback: registration }
85
+ : registration;
86
+ const cleanName = name.startsWith("lua:")
87
+ ? name.slice("lua:".length)
88
+ : name;
89
+ const namespace = cleanName.split(".")[0];
90
+ const { callback, ...metadata } = definition;
72
91
  this.registeredSyscalls.set(name, {
73
92
  requiredPermissions: requiredCapabilities,
74
93
  callback,
94
+ see: `API/${namespace}`,
95
+ ...metadata,
75
96
  });
76
97
  }
77
98
  }
@@ -83,7 +104,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
83
104
  * @param args an array of arguments to pass to the function
84
105
  */
85
106
  invokeFunction(name: string, args: any[]): Promise<any> {
86
- // Some sanity type checks
87
107
  if (typeof name !== "string") {
88
108
  throw new Error(
89
109
  `invokeFunction: function name should be a string, got ${typeof name}`,
@@ -96,7 +116,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
96
116
  }
97
117
  const [plugName, functionName] = name.split(".");
98
118
  if (!functionName) {
99
- // Sanity check
100
119
  throw new Error(`Missing function name: ${name}`);
101
120
  }
102
121
  const plug = this.loadedPlugs.get(plugName);
@@ -153,7 +172,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
153
172
  throw new Error("Plug tried to overwrite internal plug");
154
173
  }
155
174
 
156
- // Validate the manifest
157
175
  let errors: string[] = [];
158
176
  for (const feature of this.enabledHooks) {
159
177
  errors = [...errors, ...feature.validateManifest(manifest)];
@@ -112,7 +112,6 @@ export function setupMessageListener(
112
112
  }
113
113
  })().catch(console.error);
114
114
  });
115
- // Signal initialization with manifest
116
115
  workerPostMessage({
117
116
  type: "manifest",
118
117
  manifest,
@@ -139,7 +138,6 @@ export async function sandboxFetch(
139
138
 
140
139
  globalThis.nativeFetch = globalThis.fetch;
141
140
 
142
- // Monkey patch fetch()
143
141
  export function monkeyPatchFetch() {
144
142
  // @ts-expect-error: monkey patching fetch
145
143
  globalThis.fetch = async (
@@ -112,7 +112,6 @@ function covarIterate(state: CovarState, x: any, y: any): CovarState {
112
112
  return state;
113
113
  }
114
114
 
115
- // Quantile interpolation methods
116
115
  type QuantileMethod =
117
116
  | "linear" // percentile_cont
118
117
  | "lower" // percentile_disc
@@ -126,7 +125,6 @@ interface QuantileState {
126
125
  method: QuantileMethod;
127
126
  }
128
127
 
129
- // Default method based on aggregate invocation name
130
128
  const quantileNameDefaults: Record<string, QuantileMethod> = {
131
129
  percentile_cont: "linear",
132
130
  percentile_disc: "lower",
@@ -159,7 +157,6 @@ function quantileFinish(state: QuantileState): number | null {
159
157
  }
160
158
  }
161
159
 
162
- // Shared spec — branching on `ctx.name` for the default method
163
160
  function makeQuantileSpec(name: string, description: string): AggregateSpec {
164
161
  return {
165
162
  name,
@@ -187,9 +184,7 @@ function makeQuantileSpec(name: string, description: string): AggregateSpec {
187
184
  };
188
185
  }
189
186
 
190
- // Built-in aggregate specs
191
187
  const builtinAggregates: Record<string, AggregateSpec> = {
192
- // General purpose
193
188
  count: {
194
189
  name: "count",
195
190
  description:
@@ -299,7 +294,6 @@ const builtinAggregates: Record<string, AggregateSpec> = {
299
294
  return value;
300
295
  }),
301
296
  },
302
- // Collection and format
303
297
  array_agg: {
304
298
  name: "array_agg",
305
299
  description: "Input values concatenated into an array",
@@ -365,7 +359,6 @@ const builtinAggregates: Record<string, AggregateSpec> = {
365
359
  return JSON.stringify(state);
366
360
  }),
367
361
  },
368
- // Bitwise and boolean
369
362
  bit_and: {
370
363
  name: "bit_and",
371
364
  description: "Bitwise AND of all non-null input values",
@@ -447,7 +440,6 @@ const builtinAggregates: Record<string, AggregateSpec> = {
447
440
  return state.hasValue ? state.result : null;
448
441
  }),
449
442
  },
450
- // Statistical
451
443
  stddev_pop: {
452
444
  name: "stddev_pop",
453
445
  description: "Population standard deviation of non-null inputs",
@@ -558,7 +550,6 @@ const builtinAggregates: Record<string, AggregateSpec> = {
558
550
  return state.bestCount > 0 ? state.best : null;
559
551
  }),
560
552
  },
561
- // Quantile and percentile
562
553
  quantile: makeQuantileSpec(
563
554
  "quantile",
564
555
  "Quantile of ordered set of non-null inputs; arguments: value, quantile (0-1), interpolation ('lower', 'higher', 'nearest', 'midpoint' and default: 'linear')",
@@ -617,14 +608,12 @@ export function getAggregateSpec(
617
608
  const spec: any = config.get(`aggregates.${current}`, null);
618
609
  if (!spec) break;
619
610
 
620
- // Check for alias redirect
621
611
  const alias = spec instanceof LuaTable ? spec.rawGet("alias") : spec.alias;
622
612
  if (typeof alias === "string") {
623
613
  current = alias;
624
614
  continue;
625
615
  }
626
616
 
627
- // Full definition in config
628
617
  let candidate: AggregateSpec | null = null;
629
618
  if (spec instanceof LuaTable) {
630
619
  const init = spec.rawGet("initialize");
@@ -700,16 +689,13 @@ export async function executeAggregate(
700
689
  }
701
690
  }
702
691
 
703
- // Initialize
704
692
  let state = await luaCall(spec.initialize, [ctx, ...extraArgs], noCtx, sf);
705
693
 
706
- // Collect filtered items
707
694
  const filteredItems: LuaValue[] = [];
708
695
  const len = items.length;
709
696
  for (let i = 1; i <= len; i++) {
710
697
  const item = items.rawGet(i);
711
698
 
712
- // Filter
713
699
  if (filterExpr) {
714
700
  const filterEnv = buildItemEnv(objectVariable, item, env, sf);
715
701
  const filterResult = await evalExprFn(filterExpr, filterEnv, sf);
@@ -747,7 +733,6 @@ export async function executeAggregate(
747
733
  });
748
734
  }
749
735
 
750
- // Iterate
751
736
  for (const item of filteredItems) {
752
737
  const itemEnv = buildItemEnv(objectVariable, item, env, sf);
753
738
  let value: LuaValue;
@@ -773,7 +758,6 @@ export async function executeAggregate(
773
758
  );
774
759
  }
775
760
 
776
- // Finish
777
761
  if (spec.finish) {
778
762
  state = await luaCall(spec.finish, [state, ctx, ...extraArgs], noCtx, sf);
779
763
  }
@@ -0,0 +1,107 @@
1
+ import type {
2
+ LuaFunctionExampleDocumentation,
3
+ LuaFunctionInfo,
4
+ LuaFunctionParameterDocumentation,
5
+ LuaFunctionReturnDocumentation,
6
+ } from "../../plug-api/types/index.ts";
7
+
8
+ function compareNames(a: LuaFunctionInfo, b: LuaFunctionInfo): number {
9
+ const aName = a.name ?? "";
10
+ const bName = b.name ?? "";
11
+ return aName < bName ? -1 : aName > bName ? 1 : 0;
12
+ }
13
+
14
+ function signature(info: LuaFunctionInfo): string {
15
+ const parameters = (info.parameters ?? [])
16
+ .map((parameter) => `${parameter.name}${parameter.optional ? "?" : ""}`)
17
+ .join(", ");
18
+ return `${info.name ?? "<anonymous>"}(${parameters})`;
19
+ }
20
+
21
+ function renderParameter(parameter: LuaFunctionParameterDocumentation): string {
22
+ const type = parameter.type ? ` (\`${parameter.type}\`)` : "";
23
+ const description = parameter.description
24
+ ? ` — ${parameter.description}`
25
+ : "";
26
+ return `- \`${parameter.name}${parameter.optional ? "?" : ""}\`${type}${description}`;
27
+ }
28
+
29
+ function renderReturn(value: LuaFunctionReturnDocumentation): string {
30
+ const type = value.type ? `\`${value.type}\`` : "Value";
31
+ return `- ${type}${value.description ? ` — ${value.description}` : ""}`;
32
+ }
33
+
34
+ function codeFence(example: LuaFunctionExampleDocumentation): string[] {
35
+ const longestFence = Math.max(
36
+ 2,
37
+ ...[...example.code.matchAll(/`+/g)].map((match) => match[0].length),
38
+ );
39
+ const fence = "`".repeat(longestFence + 1);
40
+ return [`${fence}${example.language ?? "lua"}`, example.code, fence];
41
+ }
42
+
43
+ function renderFunction(
44
+ info: LuaFunctionInfo,
45
+ apiNamespace?: string,
46
+ ): string[] {
47
+ const lines = [`## ${info.name ?? "<anonymous>"}`, ""];
48
+ const signatures = info.signatures?.length
49
+ ? info.signatures
50
+ : [signature(info)];
51
+ for (const value of signatures) lines.push(`\`${value}\``);
52
+
53
+ if (info.deprecated) {
54
+ lines.push(
55
+ "",
56
+ `> **Deprecated:** ${
57
+ typeof info.deprecated === "string"
58
+ ? info.deprecated
59
+ : "This function is deprecated."
60
+ }`,
61
+ );
62
+ }
63
+ if (info.description) lines.push("", info.description);
64
+ if (info.parameters?.length) {
65
+ lines.push("", "**Parameters:**", "");
66
+ lines.push(...info.parameters.map(renderParameter));
67
+ }
68
+ if (info.returns?.length) {
69
+ lines.push("", "**Returns:**", "");
70
+ lines.push(...info.returns.map(renderReturn));
71
+ }
72
+ if (info.examples?.length) {
73
+ lines.push(
74
+ "",
75
+ info.examples.length === 1 ? "**Example:**" : "**Examples:**",
76
+ );
77
+ for (const example of info.examples) {
78
+ if (example.description) lines.push("", example.description);
79
+ lines.push("", ...codeFence(example));
80
+ }
81
+ }
82
+ const namespacePage = apiNamespace ? `API/${apiNamespace}` : undefined;
83
+ if (info.see && info.see !== namespacePage) {
84
+ lines.push("", `**See:** [[${info.see}]]`);
85
+ }
86
+ return lines;
87
+ }
88
+
89
+ /** Render function metadata as deterministic Markdown for live `${...}` directives. */
90
+ export function renderApiDocumentationMarkdown(
91
+ functions: LuaFunctionInfo[],
92
+ context?: string,
93
+ ): string {
94
+ const documented = functions.filter((info) => info.name).sort(compareNames);
95
+ if (documented.length === 0) {
96
+ return context
97
+ ? `_No documented API functions found for \`${context}\`._`
98
+ : "_No functions found._";
99
+ }
100
+ return documented
101
+ .flatMap((info, index) => [
102
+ ...(index > 0 ? [""] : []),
103
+ ...renderFunction(info, context),
104
+ ])
105
+ .join("\n")
106
+ .trim();
107
+ }
@@ -1,3 +1,5 @@
1
+ import type { LuaFunctionDocumentation } from "../../plug-api/types/index.ts";
2
+
1
3
  type ASTContext = {
2
4
  ctx: ASTCtx;
3
5
  };
@@ -12,6 +14,8 @@ export type ASTCtx = {
12
14
  export type LuaBlock = {
13
15
  type: "Block";
14
16
  statements: LuaStatement[];
17
+ // Comments lexically owned by this block, in source order.
18
+ comments?: LuaComment[];
15
19
  hasLabel?: boolean;
16
20
  hasGoto?: boolean;
17
21
  // the first duplicated label is recorded here for later eval
@@ -26,7 +30,14 @@ export type LuaBlock = {
26
30
  hasFunctionDef?: boolean;
27
31
  } & ASTContext;
28
32
 
29
- // STATEMENTS
33
+ export type LuaComment = {
34
+ type: "Comment";
35
+ /** Exact source text, including delimiters. */
36
+ text: string;
37
+ kind: "line" | "long";
38
+ ctx: ASTCtx;
39
+ };
40
+
30
41
  export type LuaReturnStatement = {
31
42
  type: "Return";
32
43
  expressions: LuaExpression[];
@@ -128,6 +139,7 @@ export type LuaFunctionBody = {
128
139
  type: "FunctionBody";
129
140
  parameters: string[];
130
141
  block: LuaBlock;
142
+ documentation?: LuaFunctionDocumentation;
131
143
  } & ASTContext;
132
144
 
133
145
  export type LuaAssignmentStatement = {
@@ -164,7 +176,6 @@ export type LuaFunctionCallStatement = {
164
176
  call: LuaFunctionCallExpression;
165
177
  } & ASTContext;
166
178
 
167
- // EXPRESSIONS
168
179
  export type LuaExpression =
169
180
  | LuaNilLiteral
170
181
  | LuaBooleanLiteral
@@ -281,21 +292,18 @@ export type LuaFunctionDefinition = {
281
292
  body: LuaFunctionBody;
282
293
  } & ASTContext;
283
294
 
284
- // Aggregate with per-row filter
285
295
  export type LuaFilteredCallExpression = {
286
296
  type: "FilteredCall";
287
297
  call: LuaFunctionCallExpression;
288
298
  filter: LuaExpression;
289
299
  } & ASTContext;
290
300
 
291
- // Aggregate call with intra-aggregate order by
292
301
  export type LuaAggregateCallExpression = {
293
302
  type: "AggregateCall";
294
303
  call: LuaFunctionCallExpression;
295
304
  orderBy: LuaOrderBy[];
296
305
  } & ASTContext;
297
306
 
298
- // Query stuff
299
307
  export type LuaQueryExpression = {
300
308
  type: "Query";
301
309
  clauses: LuaQueryClause[];
@@ -2,13 +2,11 @@
2
2
 
3
3
  import type { ASTCtx, LuaExpression, LuaLValue, LuaStatement } from "./ast.ts";
4
4
 
5
- // Extract by `type` discriminant
6
5
  type NarrowByType<
7
6
  U,
8
7
  K extends U extends { type: infer T } ? T : never,
9
8
  > = Extract<U, { type: K }>;
10
9
 
11
- // Expressions
12
10
  export const asStringExpr = (e: LuaExpression) =>
13
11
  e as NarrowByType<LuaExpression, "String">;
14
12
  export const asNumberExpr = (e: LuaExpression) =>
@@ -38,7 +36,6 @@ export const asFunctionDef = (e: LuaExpression) =>
38
36
  export const asQueryExpr = (e: LuaExpression) =>
39
37
  e as NarrowByType<LuaExpression, "Query">;
40
38
 
41
- // L-values
42
39
  export const asLValueVariable = (l: LuaLValue) =>
43
40
  l as Extract<LuaLValue, { type: "Variable" }>;
44
41
  export const asLValueTableAccess = (l: LuaLValue) =>
@@ -46,7 +43,6 @@ export const asLValueTableAccess = (l: LuaLValue) =>
46
43
  export const asLValuePropertyAccess = (l: LuaLValue) =>
47
44
  l as Extract<LuaLValue, { type: "PropertyAccess" }>;
48
45
 
49
- // Statements
50
46
  export const asAssignment = (s: LuaStatement) =>
51
47
  s as NarrowByType<LuaStatement, "Assignment">;
52
48
  export const asLocal = (s: LuaStatement) =>
@@ -79,5 +75,4 @@ export const asGoto = (s: LuaStatement) =>
79
75
  export const asSemicolon = (s: LuaStatement) =>
80
76
  s as NarrowByType<LuaStatement, "Semicolon">;
81
77
 
82
- // Pull ctx with a single shape
83
78
  export const ctxOf = (node: { ctx: ASTCtx }): ASTCtx => node.ctx;
@@ -0,0 +1,113 @@
1
+ export const BUDGET_TICKS = 5000;
2
+ export const YIELD_AFTER_MS = 500;
3
+ export const YIELD_EVERY_MS = 50;
4
+ export const YIELD_COUNT_LIMIT = 200;
5
+ export const BUSY_LIMIT_DEFAULT_MS = 2000;
6
+ export const BUSY_LIMIT_COMMAND_MS = 10000;
7
+ export const KEEP_GOING_MS = 10000;
8
+
9
+ export class LuaBudgetStopped extends Error {
10
+ constructor() {
11
+ super("Script stopped by the user");
12
+ this.name = "LuaBudgetStopped";
13
+ }
14
+ }
15
+
16
+ export type LuaBudget = {
17
+ ticks: number;
18
+ lastCheck: number;
19
+ lastYield: number;
20
+ busyMs: number;
21
+ yieldCount: number;
22
+ awaited: boolean;
23
+ stopped: boolean;
24
+ /** Set once the invocation this budget was created for has completed. */
25
+ finished: boolean;
26
+ limitReported: boolean;
27
+ busyLimitMs: number;
28
+ yieldAfterMs: number;
29
+ onLimit: ((b: LuaBudget) => void) | undefined;
30
+ now: () => number;
31
+ doYield: () => Promise<void>;
32
+ };
33
+
34
+ export function macrotaskYield(): Promise<void> {
35
+ // Do NOT use scheduler.yield() here: it resumes at a *higher* priority than
36
+ // ordinary tasks, so it never lets timers/input/rendering run and defeats
37
+ // the whole point of yielding. MessageChannel posts a genuine macrotask.
38
+ if (typeof MessageChannel === "function") {
39
+ return new Promise<void>((resolve) => {
40
+ const ch = new MessageChannel();
41
+ ch.port1.onmessage = () => {
42
+ ch.port1.close();
43
+ resolve();
44
+ };
45
+ ch.port2.postMessage(undefined);
46
+ });
47
+ }
48
+ return new Promise<void>((resolve) => setTimeout(resolve, 0));
49
+ }
50
+
51
+ export function makeLuaBudget(
52
+ opts: Partial<
53
+ Pick<
54
+ LuaBudget,
55
+ "busyLimitMs" | "yieldAfterMs" | "onLimit" | "now" | "doYield"
56
+ >
57
+ > = {},
58
+ ): LuaBudget {
59
+ const now = opts.now ?? (() => performance.now());
60
+ return {
61
+ ticks: BUDGET_TICKS,
62
+ lastCheck: now(),
63
+ lastYield: now(),
64
+ busyMs: 0,
65
+ yieldCount: 0,
66
+ awaited: false,
67
+ stopped: false,
68
+ finished: false,
69
+ limitReported: false,
70
+ busyLimitMs: opts.busyLimitMs ?? BUSY_LIMIT_DEFAULT_MS,
71
+ yieldAfterMs: opts.yieldAfterMs ?? YIELD_AFTER_MS,
72
+ onLimit: opts.onLimit,
73
+ now,
74
+ doYield: opts.doYield ?? macrotaskYield,
75
+ };
76
+ }
77
+
78
+ export function budgetTick(b: LuaBudget): Promise<void> | undefined {
79
+ if (b.stopped) {
80
+ throw new LuaBudgetStopped();
81
+ }
82
+ b.ticks = BUDGET_TICKS;
83
+
84
+ const now = b.now();
85
+ const sinceCheck = now - b.lastCheck;
86
+ b.lastCheck = now;
87
+ if (b.awaited) {
88
+ b.awaited = false;
89
+ } else {
90
+ b.busyMs += sinceCheck;
91
+ }
92
+
93
+ if (
94
+ !b.limitReported &&
95
+ (b.busyMs >= b.busyLimitMs || b.yieldCount >= YIELD_COUNT_LIMIT)
96
+ ) {
97
+ b.limitReported = true;
98
+ b.onLimit?.(b);
99
+ }
100
+
101
+ const threshold = b.yieldCount === 0 ? b.yieldAfterMs : YIELD_EVERY_MS;
102
+ if (now - b.lastYield < threshold) {
103
+ return undefined;
104
+ }
105
+ b.lastYield = now;
106
+ b.yieldCount++;
107
+ // The event-loop turn spent yielding (rendering, sync, other scripts) must
108
+ // not be billed to this script's busyMs at the next tick, so re-read the
109
+ // clock on resume rather than trusting the pre-yield lastCheck.
110
+ return b.doYield().then(() => {
111
+ b.lastCheck = b.now();
112
+ });
113
+ }
@@ -0,0 +1,49 @@
1
+ import { KEEP_GOING_MS, type LuaBudget } from "./budget.ts";
2
+
3
+ export type BudgetNotifier = {
4
+ flashNotification(
5
+ message: string,
6
+ type?: string,
7
+ options?: {
8
+ timeout?: number;
9
+ actions?: { name: string; run: () => void }[];
10
+ },
11
+ ): void;
12
+ };
13
+
14
+ export function offerStopNotification(
15
+ ui: BudgetNotifier,
16
+ message: string,
17
+ budget: LuaBudget,
18
+ onStop?: () => void,
19
+ ): void {
20
+ ui.flashNotification(message, "warning", {
21
+ timeout: 0,
22
+ actions: [
23
+ {
24
+ name: "Stop",
25
+ run: () => {
26
+ // The script may have finished running (this toast is persistent)
27
+ // between the limit tripping and the user clicking Stop; don't
28
+ // quarantine a script that isn't running anymore.
29
+ if (budget.finished) {
30
+ return;
31
+ }
32
+ budget.stopped = true;
33
+ onStop?.();
34
+ },
35
+ },
36
+ {
37
+ name: "Keep going",
38
+ run: () => {
39
+ budget.busyLimitMs = budget.busyMs + KEEP_GOING_MS;
40
+ // Resetting to 0 would restore the 500ms first-yield threshold,
41
+ // making the tab unresponsive again right after the user asked
42
+ // to continue.
43
+ budget.yieldCount = 1;
44
+ budget.limitReported = false;
45
+ },
46
+ },
47
+ ],
48
+ });
49
+ }