@silverbulletmd/silverbullet 2.10.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.
- package/README.md +4 -4
- package/client/asset_bundle/bundle.ts +0 -1
- package/client/config.ts +343 -0
- package/client/markdown_parser/constants.ts +3 -1
- package/client/plugos/hooks/command.ts +0 -5
- package/client/plugos/hooks/event.ts +0 -6
- package/client/plugos/hooks/mq.ts +0 -3
- package/client/plugos/hooks/slash_command.ts +1 -7
- package/client/plugos/hooks/syscall.ts +0 -4
- package/client/plugos/manifest_cache.ts +0 -32
- package/client/plugos/plug.ts +0 -3
- package/client/plugos/plug_compile.ts +3 -13
- package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
- package/client/plugos/syscalls/editor.ts +128 -53
- package/client/plugos/syscalls/event.ts +0 -2
- package/client/plugos/syscalls/fetch.ts +0 -4
- package/client/plugos/syscalls/icon.ts +46 -0
- package/client/plugos/syscalls/index.ts +33 -13
- package/client/plugos/syscalls/jsonschema.ts +0 -1
- package/client/plugos/syscalls/mq.ts +0 -1
- package/client/plugos/syscalls/navigator.ts +143 -0
- package/client/plugos/syscalls/search.ts +44 -0
- package/client/plugos/syscalls/shell.ts +0 -1
- package/client/plugos/syscalls/space.ts +250 -7
- package/client/plugos/syscalls/sync.ts +56 -4
- package/client/plugos/syscalls/system.ts +38 -0
- package/client/plugos/system.ts +7 -5
- package/client/plugos/worker_runtime.ts +0 -2
- package/client/space_lua/aggregates.ts +0 -16
- package/client/space_lua/ast.ts +0 -5
- package/client/space_lua/ast_narrow.ts +0 -5
- package/client/space_lua/budget.ts +113 -0
- package/client/space_lua/budget_ui.ts +49 -0
- package/client/space_lua/eval.ts +113 -88
- package/client/space_lua/labels.ts +0 -4
- package/client/space_lua/numeric.ts +0 -5
- package/client/space_lua/parse.ts +0 -17
- package/client/space_lua/quarantine.ts +127 -0
- package/client/space_lua/query_collection.ts +3 -47
- package/client/space_lua/query_env.ts +0 -2
- package/client/space_lua/render_lua_markdown.ts +2 -7
- package/client/space_lua/render_widget.ts +18 -1
- package/client/space_lua/runtime.ts +50 -70
- package/client/space_lua/stdlib/format.ts +0 -12
- package/client/space_lua/stdlib/js.ts +0 -2
- package/client/space_lua/stdlib/load.ts +0 -1
- package/client/space_lua/stdlib/math.ts +0 -7
- package/client/space_lua/stdlib/net.ts +0 -4
- package/client/space_lua/stdlib/os.ts +0 -10
- package/client/space_lua/stdlib/pattern.ts +10 -15
- package/client/space_lua/stdlib/space_lua.ts +1 -2
- package/client/space_lua/stdlib/string_pack.ts +0 -3
- package/client/space_lua/stdlib/table.ts +0 -5
- package/client/space_lua/stdlib.ts +18 -31
- package/client/space_lua/tonumber.ts +0 -9
- package/dist/plug-compile.js +2 -3
- package/package.json +13 -3
- package/plug-api/lib/async.ts +1 -6
- package/plug-api/lib/collation.ts +22 -0
- package/plug-api/lib/crypto.ts +16 -15
- package/plug-api/lib/dates.ts +33 -0
- package/plug-api/lib/fuzzy.ts +282 -0
- package/plug-api/lib/json.ts +0 -6
- package/plug-api/lib/limited_map.ts +0 -2
- package/plug-api/lib/link_write.ts +27 -0
- package/plug-api/lib/ref.ts +28 -5
- package/plug-api/lib/resolve.ts +0 -1
- package/plug-api/lib/resolve_path.ts +290 -0
- package/plug-api/lib/shortcut.ts +15 -0
- package/plug-api/lib/tags.ts +0 -3
- package/plug-api/lib/transclusion.ts +28 -1
- package/plug-api/lib/tree.ts +0 -5
- package/plug-api/lib/yaml.ts +3 -42
- package/plug-api/syscall.ts +1 -5
- package/plug-api/syscalls/config.ts +2 -2
- package/plug-api/syscalls/editor.ts +59 -213
- package/plug-api/syscalls/icon.ts +18 -0
- package/plug-api/syscalls/index.ts +22 -4
- package/plug-api/syscalls/search.ts +11 -0
- package/plug-api/syscalls/space.ts +122 -5
- package/plug-api/syscalls/sync.ts +23 -2
- package/plug-api/syscalls/system.ts +46 -0
- package/plug-api/syscalls.ts +2 -0
- package/plug-api/system_mock.ts +13 -0
- package/plug-api/types/client.ts +4 -3
- package/plug-api/types/datastore.ts +11 -2
- package/plug-api/types/index.ts +10 -0
- package/plug-api/types/profile.ts +13 -0
- package/plug-api/types/revisions.ts +67 -0
- package/plug-api/ui/description.ts +64 -0
- package/plug-api/ui/hover.ts +69 -0
- package/plug-api/ui/index.ts +58 -2
- package/plug-api/ui/scroll.ts +30 -0
- package/plug-api/ui/tree_model.ts +263 -0
- package/plug-api/ui/tree_types.ts +66 -0
- package/plug-api/ui/use_fit_collapse.ts +67 -0
- package/plugs/builtin_plugs.ts +0 -1
- package/plugs/index/types.ts +9 -0
- package/plug-api/lib/memory_cache.ts +0 -21
- /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { SysCallMapping } from "../system.ts";
|
|
2
1
|
import type { Client } from "../../client.ts";
|
|
2
|
+
import type { SysCallMapping } from "../system.ts";
|
|
3
3
|
|
|
4
4
|
export function syncSyscalls(client: Client): SysCallMapping {
|
|
5
5
|
const syncTimeoutMs = 30000;
|
|
6
|
+
// Deferring indexing only pays off while the sync engine is actually
|
|
7
|
+
// delivering files. Once it has gone this long without reporting progress
|
|
8
|
+
// there is nothing left to yield to, and continuing to defer would stall
|
|
9
|
+
// the initial index forever.
|
|
10
|
+
const syncStallMs = 5000;
|
|
6
11
|
|
|
7
12
|
function waitForServiceWorkerActivation(path?: string): Promise<any> {
|
|
8
13
|
return new Promise<any>((resolve, reject) => {
|
|
@@ -70,14 +75,47 @@ export function syncSyscalls(client: Client): SysCallMapping {
|
|
|
70
75
|
{ type: "boolean", description: "Whether initial sync is complete." },
|
|
71
76
|
],
|
|
72
77
|
},
|
|
78
|
+
"sync.areFilesReadyToIndex": {
|
|
79
|
+
callback: (_ctx, paths: string[]): boolean[] => {
|
|
80
|
+
const allReady =
|
|
81
|
+
!!client.bootConfig.disableServiceWorker ||
|
|
82
|
+
client.fullSyncCompleted ||
|
|
83
|
+
client.fullIndexCompleted ||
|
|
84
|
+
(client.serverPingMs !== undefined && client.serverPingMs < 25) ||
|
|
85
|
+
Date.now() - client.lastSyncProgressAt > syncStallMs;
|
|
86
|
+
return paths.map((path) => allReady || client.syncedPaths.has(path));
|
|
87
|
+
},
|
|
88
|
+
description:
|
|
89
|
+
"For each file, whether indexing it now would read it locally or cheaply (true), or race the initial sync and expensively re-download it (false).",
|
|
90
|
+
parameters: [
|
|
91
|
+
{
|
|
92
|
+
name: "paths",
|
|
93
|
+
type: "string[]",
|
|
94
|
+
description: "Space-relative file paths.",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
returns: [
|
|
98
|
+
{
|
|
99
|
+
type: "boolean[]",
|
|
100
|
+
description: "Per-path readiness, in input order.",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
73
104
|
"sync.performFileSync": {
|
|
74
|
-
callback: async (
|
|
105
|
+
callback: async (
|
|
106
|
+
_ctx,
|
|
107
|
+
path: string,
|
|
108
|
+
remoteLastModified?: number,
|
|
109
|
+
remoteRevisionHash?: string,
|
|
110
|
+
): Promise<void> => {
|
|
75
111
|
await client.postServiceWorkerMessage({
|
|
76
112
|
type: "perform-file-sync",
|
|
77
113
|
path,
|
|
114
|
+
remoteLastModified,
|
|
115
|
+
remoteRevisionHash,
|
|
78
116
|
});
|
|
79
117
|
// postServiceWorkerMessage returns silently if no SW, so only wait if SW is active
|
|
80
|
-
const registration = await navigator.serviceWorker
|
|
118
|
+
const registration = await navigator.serviceWorker?.getRegistration();
|
|
81
119
|
if (registration?.active) {
|
|
82
120
|
return waitForServiceWorkerActivation(path);
|
|
83
121
|
}
|
|
@@ -90,13 +128,27 @@ export function syncSyscalls(client: Client): SysCallMapping {
|
|
|
90
128
|
type: "string",
|
|
91
129
|
description: "Space-relative file path.",
|
|
92
130
|
},
|
|
131
|
+
{
|
|
132
|
+
name: "remoteLastModified",
|
|
133
|
+
type: "number",
|
|
134
|
+
optional: true,
|
|
135
|
+
description:
|
|
136
|
+
"lastModified of the remote change event, used for echo suppression.",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "remoteRevisionHash",
|
|
140
|
+
type: "string",
|
|
141
|
+
optional: true,
|
|
142
|
+
description:
|
|
143
|
+
"Content revision the remote change event reported, which tells a same-millisecond change from an echo.",
|
|
144
|
+
},
|
|
93
145
|
],
|
|
94
146
|
examples: [{ code: 'sync.performFileSync("notes/important.md")' }],
|
|
95
147
|
},
|
|
96
148
|
"sync.performSpaceSync": {
|
|
97
149
|
callback: async (): Promise<number> => {
|
|
98
150
|
await client.postServiceWorkerMessage({ type: "perform-space-sync" });
|
|
99
|
-
const registration = await navigator.serviceWorker
|
|
151
|
+
const registration = await navigator.serviceWorker?.getRegistration();
|
|
100
152
|
if (registration?.active) {
|
|
101
153
|
return waitForServiceWorkerActivation();
|
|
102
154
|
}
|
|
@@ -3,6 +3,11 @@ import type { Client } from "../../client.ts";
|
|
|
3
3
|
import { version as publicVersion } from "../../../version.json";
|
|
4
4
|
import type { CommandDef } from "@silverbulletmd/silverbullet/type/manifest";
|
|
5
5
|
import type { SyscallMeta } from "@silverbulletmd/silverbullet/type/index";
|
|
6
|
+
import { loadAccounts, loadProfile } from "../../accounts.ts";
|
|
7
|
+
import type {
|
|
8
|
+
Account,
|
|
9
|
+
ClientProfile,
|
|
10
|
+
} from "../../../plug-api/types/profile.ts";
|
|
6
11
|
|
|
7
12
|
export function systemSyscalls(
|
|
8
13
|
client: Client,
|
|
@@ -97,6 +102,27 @@ export function systemSyscalls(
|
|
|
97
102
|
description:
|
|
98
103
|
"Returns a map of every currently available command definition.",
|
|
99
104
|
},
|
|
105
|
+
"system.listPaletteCommands": {
|
|
106
|
+
callback: () => client.listPaletteCommands(),
|
|
107
|
+
description: `Returns the commands the command palette would show right now: context- and mode-filtered, hidden ones dropped, each with its prettified key hint and the time it was last run on this client.`,
|
|
108
|
+
signatures: ["system.listPaletteCommands()"],
|
|
109
|
+
},
|
|
110
|
+
"system.runPaletteCommand": {
|
|
111
|
+
callback: async (_ctx, name: string) => {
|
|
112
|
+
await client.registerCommandRun(name);
|
|
113
|
+
try {
|
|
114
|
+
return await client.runCommandByName(name);
|
|
115
|
+
} catch (e: any) {
|
|
116
|
+
client.reportError(e, "Command invocation");
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
description: `Runs a command as if it had been picked from the command palette: records it as the most recently run, then invokes it and returns its result. A command that throws is reported to the user and answered as false, which in this protocol means "do not move focus on my behalf".`,
|
|
121
|
+
parameters: [
|
|
122
|
+
{ name: "name", type: "string", description: "Command name." },
|
|
123
|
+
],
|
|
124
|
+
returns: [{ description: "Whatever the command returned." }],
|
|
125
|
+
},
|
|
100
126
|
"system.listSyscalls": {
|
|
101
127
|
callback: (): SyscallMeta[] => {
|
|
102
128
|
const syscalls: SyscallMeta[] = [];
|
|
@@ -226,6 +252,18 @@ export function systemSyscalls(
|
|
|
226
252
|
callback: () => publicVersion,
|
|
227
253
|
description: "Returns the running SilverBullet version.",
|
|
228
254
|
},
|
|
255
|
+
"system.getProfile": {
|
|
256
|
+
callback: (): Promise<ClientProfile> => loadProfile(client),
|
|
257
|
+
description:
|
|
258
|
+
'The current user\'s identity. `username` is "me" when the server has no account for this session.',
|
|
259
|
+
signatures: ["system.getProfile()"],
|
|
260
|
+
},
|
|
261
|
+
"system.listAccounts": {
|
|
262
|
+
callback: (): Promise<Account[]> => loadAccounts(client),
|
|
263
|
+
description:
|
|
264
|
+
"Every account with access to this space, with the current user marked.",
|
|
265
|
+
signatures: ["system.listAccounts()"],
|
|
266
|
+
},
|
|
229
267
|
"system.getConfig": {
|
|
230
268
|
callback: (_ctx, key: string, defaultValue: any = undefined) =>
|
|
231
269
|
client.config.get(key, defaultValue),
|
package/client/plugos/system.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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 {
|
|
5
|
+
import type { ManifestCache } from "./manifest_cache.ts";
|
|
6
6
|
import {
|
|
7
7
|
builtinPlugNames,
|
|
8
8
|
builtinPlugPaths,
|
|
@@ -55,7 +55,12 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
|
55
55
|
) {
|
|
56
56
|
super();
|
|
57
57
|
if (!options.manifestCache) {
|
|
58
|
-
options.manifestCache =
|
|
58
|
+
options.manifestCache = {
|
|
59
|
+
async getManifest(plug) {
|
|
60
|
+
await plug.sandbox.init();
|
|
61
|
+
return plug.sandbox.manifest!;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -99,7 +104,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
|
99
104
|
* @param args an array of arguments to pass to the function
|
|
100
105
|
*/
|
|
101
106
|
invokeFunction(name: string, args: any[]): Promise<any> {
|
|
102
|
-
// Some sanity type checks
|
|
103
107
|
if (typeof name !== "string") {
|
|
104
108
|
throw new Error(
|
|
105
109
|
`invokeFunction: function name should be a string, got ${typeof name}`,
|
|
@@ -112,7 +116,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
|
112
116
|
}
|
|
113
117
|
const [plugName, functionName] = name.split(".");
|
|
114
118
|
if (!functionName) {
|
|
115
|
-
// Sanity check
|
|
116
119
|
throw new Error(`Missing function name: ${name}`);
|
|
117
120
|
}
|
|
118
121
|
const plug = this.loadedPlugs.get(plugName);
|
|
@@ -169,7 +172,6 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
|
|
|
169
172
|
throw new Error("Plug tried to overwrite internal plug");
|
|
170
173
|
}
|
|
171
174
|
|
|
172
|
-
// Validate the manifest
|
|
173
175
|
let errors: string[] = [];
|
|
174
176
|
for (const feature of this.enabledHooks) {
|
|
175
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
|
}
|
package/client/space_lua/ast.ts
CHANGED
|
@@ -38,7 +38,6 @@ export type LuaComment = {
|
|
|
38
38
|
ctx: ASTCtx;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
// STATEMENTS
|
|
42
41
|
export type LuaReturnStatement = {
|
|
43
42
|
type: "Return";
|
|
44
43
|
expressions: LuaExpression[];
|
|
@@ -177,7 +176,6 @@ export type LuaFunctionCallStatement = {
|
|
|
177
176
|
call: LuaFunctionCallExpression;
|
|
178
177
|
} & ASTContext;
|
|
179
178
|
|
|
180
|
-
// EXPRESSIONS
|
|
181
179
|
export type LuaExpression =
|
|
182
180
|
| LuaNilLiteral
|
|
183
181
|
| LuaBooleanLiteral
|
|
@@ -294,21 +292,18 @@ export type LuaFunctionDefinition = {
|
|
|
294
292
|
body: LuaFunctionBody;
|
|
295
293
|
} & ASTContext;
|
|
296
294
|
|
|
297
|
-
// Aggregate with per-row filter
|
|
298
295
|
export type LuaFilteredCallExpression = {
|
|
299
296
|
type: "FilteredCall";
|
|
300
297
|
call: LuaFunctionCallExpression;
|
|
301
298
|
filter: LuaExpression;
|
|
302
299
|
} & ASTContext;
|
|
303
300
|
|
|
304
|
-
// Aggregate call with intra-aggregate order by
|
|
305
301
|
export type LuaAggregateCallExpression = {
|
|
306
302
|
type: "AggregateCall";
|
|
307
303
|
call: LuaFunctionCallExpression;
|
|
308
304
|
orderBy: LuaOrderBy[];
|
|
309
305
|
} & ASTContext;
|
|
310
306
|
|
|
311
|
-
// Query stuff
|
|
312
307
|
export type LuaQueryExpression = {
|
|
313
308
|
type: "Query";
|
|
314
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
|
+
}
|