@silverbulletmd/silverbullet 2.4.1

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 (117) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +98 -0
  3. package/client/asset_bundle/bundle.ts +95 -0
  4. package/client/data/datastore.ts +85 -0
  5. package/client/data/kv_primitives.ts +25 -0
  6. package/client/markdown_parser/constants.ts +13 -0
  7. package/client/plugos/event.ts +36 -0
  8. package/client/plugos/eventhook.ts +8 -0
  9. package/client/plugos/hooks/code_widget.ts +59 -0
  10. package/client/plugos/hooks/command.ts +104 -0
  11. package/client/plugos/hooks/document_editor.ts +77 -0
  12. package/client/plugos/hooks/event.ts +187 -0
  13. package/client/plugos/hooks/mq.ts +154 -0
  14. package/client/plugos/hooks/plug_namespace.ts +85 -0
  15. package/client/plugos/hooks/slash_command.ts +192 -0
  16. package/client/plugos/hooks/syscall.ts +66 -0
  17. package/client/plugos/manifest_cache.ts +67 -0
  18. package/client/plugos/plug.ts +99 -0
  19. package/client/plugos/plug_compile.ts +202 -0
  20. package/client/plugos/protocol.ts +40 -0
  21. package/client/plugos/proxy_fetch.ts +53 -0
  22. package/client/plugos/sandboxes/deno_worker_sandbox.ts +6 -0
  23. package/client/plugos/sandboxes/sandbox.ts +14 -0
  24. package/client/plugos/sandboxes/web_worker_sandbox.ts +17 -0
  25. package/client/plugos/sandboxes/worker_sandbox.ts +132 -0
  26. package/client/plugos/syscalls/asset.ts +35 -0
  27. package/client/plugos/syscalls/clientStore.ts +21 -0
  28. package/client/plugos/syscalls/client_code_widget.ts +12 -0
  29. package/client/plugos/syscalls/code_widget.ts +24 -0
  30. package/client/plugos/syscalls/config.ts +46 -0
  31. package/client/plugos/syscalls/datastore.ts +89 -0
  32. package/client/plugos/syscalls/editor.ts +673 -0
  33. package/client/plugos/syscalls/event.ts +36 -0
  34. package/client/plugos/syscalls/fetch.ts +128 -0
  35. package/client/plugos/syscalls/index.ts +102 -0
  36. package/client/plugos/syscalls/jsonschema.ts +69 -0
  37. package/client/plugos/syscalls/language.ts +23 -0
  38. package/client/plugos/syscalls/lua.ts +58 -0
  39. package/client/plugos/syscalls/markdown.ts +84 -0
  40. package/client/plugos/syscalls/mq.ts +52 -0
  41. package/client/plugos/syscalls/service_registry.ts +43 -0
  42. package/client/plugos/syscalls/shell.ts +39 -0
  43. package/client/plugos/syscalls/space.ts +139 -0
  44. package/client/plugos/syscalls/sync.ts +77 -0
  45. package/client/plugos/syscalls/system.ts +150 -0
  46. package/client/plugos/system.ts +201 -0
  47. package/client/plugos/types.ts +60 -0
  48. package/client/plugos/util.ts +14 -0
  49. package/client/plugos/worker_runtime.ts +195 -0
  50. package/client/space_lua/ast.ts +328 -0
  51. package/client/space_lua/ast_narrow.ts +81 -0
  52. package/client/space_lua/eval.ts +2478 -0
  53. package/client/space_lua/labels.ts +416 -0
  54. package/client/space_lua/numeric.ts +240 -0
  55. package/client/space_lua/parse.ts +1522 -0
  56. package/client/space_lua/query_collection.ts +232 -0
  57. package/client/space_lua/rp.ts +27 -0
  58. package/client/space_lua/runtime.ts +1702 -0
  59. package/client/space_lua/stdlib/crypto.ts +10 -0
  60. package/client/space_lua/stdlib/encoding.ts +19 -0
  61. package/client/space_lua/stdlib/format.ts +770 -0
  62. package/client/space_lua/stdlib/js.ts +73 -0
  63. package/client/space_lua/stdlib/load.ts +52 -0
  64. package/client/space_lua/stdlib/math.ts +193 -0
  65. package/client/space_lua/stdlib/net.ts +113 -0
  66. package/client/space_lua/stdlib/os.ts +368 -0
  67. package/client/space_lua/stdlib/space_lua.ts +153 -0
  68. package/client/space_lua/stdlib/string.ts +286 -0
  69. package/client/space_lua/stdlib/table.ts +401 -0
  70. package/client/space_lua/stdlib.ts +489 -0
  71. package/client/space_lua/tonumber.ts +501 -0
  72. package/client/space_lua/util.ts +96 -0
  73. package/dist/plug-compile.js +1513 -0
  74. package/package.json +120 -0
  75. package/plug-api/constants.ts +42 -0
  76. package/plug-api/lib/async.ts +162 -0
  77. package/plug-api/lib/crypto.ts +202 -0
  78. package/plug-api/lib/dates.ts +13 -0
  79. package/plug-api/lib/json.ts +136 -0
  80. package/plug-api/lib/limited_map.ts +72 -0
  81. package/plug-api/lib/memory_cache.ts +21 -0
  82. package/plug-api/lib/native_fetch.ts +6 -0
  83. package/plug-api/lib/ref.ts +275 -0
  84. package/plug-api/lib/resolve.ts +90 -0
  85. package/plug-api/lib/tags.ts +15 -0
  86. package/plug-api/lib/transclusion.ts +122 -0
  87. package/plug-api/lib/tree.ts +232 -0
  88. package/plug-api/lib/yaml.ts +284 -0
  89. package/plug-api/syscall.ts +15 -0
  90. package/plug-api/syscalls/asset.ts +36 -0
  91. package/plug-api/syscalls/client_store.ts +33 -0
  92. package/plug-api/syscalls/code_widget.ts +8 -0
  93. package/plug-api/syscalls/config.ts +58 -0
  94. package/plug-api/syscalls/datastore.ts +96 -0
  95. package/plug-api/syscalls/editor.ts +517 -0
  96. package/plug-api/syscalls/event.ts +47 -0
  97. package/plug-api/syscalls/index.ts +77 -0
  98. package/plug-api/syscalls/jsonschema.ts +25 -0
  99. package/plug-api/syscalls/language.ts +23 -0
  100. package/plug-api/syscalls/lua.ts +20 -0
  101. package/plug-api/syscalls/markdown.ts +38 -0
  102. package/plug-api/syscalls/mq.ts +79 -0
  103. package/plug-api/syscalls/shell.ts +14 -0
  104. package/plug-api/syscalls/space.ts +212 -0
  105. package/plug-api/syscalls/sync.ts +28 -0
  106. package/plug-api/syscalls/system.ts +102 -0
  107. package/plug-api/syscalls/yaml.ts +28 -0
  108. package/plug-api/syscalls.ts +21 -0
  109. package/plug-api/system_mock.ts +89 -0
  110. package/plug-api/types/client.ts +116 -0
  111. package/plug-api/types/config.ts +22 -0
  112. package/plug-api/types/datastore.ts +28 -0
  113. package/plug-api/types/event.ts +27 -0
  114. package/plug-api/types/index.ts +56 -0
  115. package/plug-api/types/manifest.ts +98 -0
  116. package/plug-api/types/namespace.ts +6 -0
  117. package/plugs/builtin_plugs.ts +14 -0
@@ -0,0 +1,67 @@
1
+ import type { KvPrimitives } from "../data/kv_primitives.ts";
2
+ import type { Plug } from "./plug.ts";
3
+ import type { Manifest } from "./types.ts";
4
+
5
+ export interface ManifestCache<T> {
6
+ getManifest(
7
+ plug: Plug<T>,
8
+ cacheKey: string,
9
+ cacheHash: number,
10
+ ): Promise<Manifest<T>>;
11
+ }
12
+
13
+ export class KVPrimitivesManifestCache<T> implements ManifestCache<T> {
14
+ constructor(private kv: KvPrimitives, private manifestPrefix: string) {
15
+ }
16
+
17
+ async getManifest(
18
+ plug: Plug<T>,
19
+ cacheKey: string,
20
+ cacheHash: number,
21
+ ): Promise<Manifest<T>> {
22
+ const [cached] = await this.kv.batchGet([[
23
+ this.manifestPrefix,
24
+ cacheKey,
25
+ ]]);
26
+ if (cached && cached.hash === cacheHash) {
27
+ // console.log("Using KV cached manifest for", plug.name);
28
+ return cached.manifest;
29
+ }
30
+ await plug.sandbox.init();
31
+ const manifest = plug.sandbox.manifest!;
32
+ await this.kv.batchSet([{
33
+ key: [this.manifestPrefix, cacheKey],
34
+ // Deliverately removing the assets from the manifest to preserve space, will be re-added upon load of actual worker
35
+ value: { manifest: { ...manifest, assets: undefined }, hash: cacheHash },
36
+ }]);
37
+ return manifest;
38
+ }
39
+ }
40
+
41
+ export class InMemoryManifestCache<T> implements ManifestCache<T> {
42
+ private cache = new Map<string, {
43
+ manifest: Manifest<T>;
44
+ hash: number;
45
+ }>();
46
+
47
+ async getManifest(
48
+ plug: Plug<T>,
49
+ cacheKey: string,
50
+ cacheHash: number,
51
+ ): Promise<Manifest<T>> {
52
+ const cached = this.cache.get(cacheKey);
53
+ if (cached && cached.hash === cacheHash) {
54
+ // console.log("Using memory cached manifest for", plug.name);
55
+ return cached.manifest;
56
+ }
57
+ await plug.sandbox.init();
58
+ const manifest = plug.sandbox.manifest!;
59
+
60
+ // Deliverately removing the assets from the manifest to preserve space, will be re-added upon load of actual worker
61
+ this.cache.set(cacheKey, {
62
+ manifest: { ...manifest, assets: undefined },
63
+ hash: cacheHash,
64
+ });
65
+ return manifest;
66
+ }
67
+ }
@@ -0,0 +1,99 @@
1
+ import type { Manifest } from "./types.ts";
2
+ import type { System } from "./system.ts";
3
+ import type { AssetBundle } from "../asset_bundle/bundle.ts";
4
+ import type { Sandbox, SandboxFactory } from "./sandboxes/sandbox.ts";
5
+
6
+ export class Plug<HookT> {
7
+ readonly runtimeEnv?: string;
8
+
9
+ public grantedPermissions: string[] = [];
10
+ public sandbox: Sandbox<HookT>;
11
+
12
+ public manifest!: Manifest<HookT>;
13
+ public assets?: AssetBundle;
14
+
15
+ constructor(
16
+ readonly system: System<HookT>,
17
+ sandboxFactory: SandboxFactory<HookT>,
18
+ ) {
19
+ this.runtimeEnv = system.env;
20
+ this.sandbox = sandboxFactory(this);
21
+ }
22
+
23
+ static async createLazily<HookT>(
24
+ system: System<HookT>,
25
+ cacheKey: string,
26
+ cacheHash: number,
27
+ sandboxFactory: SandboxFactory<HookT>,
28
+ ): Promise<Plug<HookT>> {
29
+ const plug = new Plug(
30
+ system,
31
+ sandboxFactory,
32
+ );
33
+
34
+ // Retrieve the manifest, which may either come from a cache or be loaded from the worker
35
+ plug.manifest = await system.options.manifestCache!.getManifest(
36
+ plug,
37
+ cacheKey,
38
+ cacheHash,
39
+ );
40
+
41
+ // TODO: These need to be explicitly granted, not just taken
42
+ plug.grantedPermissions = plug.manifest.requiredPermissions || [];
43
+
44
+ return plug;
45
+ }
46
+
47
+ // Invoke a syscall
48
+ syscall(name: string, args: any[]): Promise<any> {
49
+ return this.system.syscall({ plug: this.manifest.name }, name, args);
50
+ }
51
+
52
+ /**
53
+ * Checks if a function can be invoked (it may be restricted on its execution environment)
54
+ */
55
+ canInvoke(name: string) {
56
+ const funDef = this.manifest!.functions[name];
57
+ if (!funDef) {
58
+ throw new Error(`Function ${name} not found in manifest`);
59
+ }
60
+ return !funDef.env || !this.runtimeEnv || funDef.env === this.runtimeEnv;
61
+ }
62
+
63
+ // Invoke a function
64
+ async invoke(name: string, args: any[]): Promise<any> {
65
+ // Before we access the manifest
66
+ const funDef = this.manifest!.functions[name];
67
+ if (!funDef) {
68
+ throw new Error(`Function ${name} not found in manifest`);
69
+ }
70
+ const sandbox = this.sandbox!;
71
+ if (funDef.redirect) {
72
+ // Function redirect, look up
73
+ // deno-lint-ignore no-this-alias
74
+ let plug: Plug<HookT> | undefined = this;
75
+ if (funDef.redirect.indexOf(".") !== -1) {
76
+ const [plugName, functionName] = funDef.redirect.split(".");
77
+ plug = this.system.loadedPlugs.get(plugName);
78
+ if (!plug) {
79
+ throw Error(`Plug ${plugName} redirected to not found`);
80
+ }
81
+ name = functionName;
82
+ } else {
83
+ name = funDef.redirect;
84
+ }
85
+ return plug.invoke(name, args);
86
+ }
87
+ if (!await this.canInvoke(name)) {
88
+ throw new Error(
89
+ `Function ${name} is not available in ${this.runtimeEnv}`,
90
+ );
91
+ }
92
+ return sandbox.invoke(name, args);
93
+ }
94
+
95
+ stop() {
96
+ console.log("Stopping sandbox for", this.manifest.name);
97
+ this.sandbox.stop();
98
+ }
99
+ }
@@ -0,0 +1,202 @@
1
+ import * as path from "node:path";
2
+ import { readFile, writeFile, mkdtemp, rm, mkdir } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import * as YAML from "js-yaml";
5
+
6
+ import { denoPlugin, esbuild } from "../../build_deps.ts";
7
+ import { bundleAssets } from "../asset_bundle/builder.ts";
8
+ import type { Manifest } from "./types.ts";
9
+ import { version } from "../../version.ts";
10
+
11
+ // const workerRuntimeUrl = new URL(
12
+ // "../lib/plugos/worker_runtime.ts",
13
+ // import.meta.url,
14
+ // );
15
+ const workerRuntimeUrl =
16
+ `https://deno.land/x/silverbullet@${version}/client/plugos/worker_runtime.ts`;
17
+
18
+ export type CompileOptions = {
19
+ debug?: boolean;
20
+ runtimeUrl?: string;
21
+ // path to config file
22
+ configPath?: string;
23
+ // Print info on bundle size
24
+ info?: boolean;
25
+ };
26
+
27
+ export async function compileManifest(
28
+ manifestPath: string,
29
+ destPath: string,
30
+ options: CompileOptions = {},
31
+ ): Promise<string> {
32
+ const rootPath = path.dirname(manifestPath);
33
+ const manifestContent = await readFile(manifestPath, "utf-8");
34
+ const manifest = YAML.load(manifestContent) as Manifest<any>;
35
+
36
+ if (!manifest.name) {
37
+ throw new Error(`Missing 'name' in ${manifestPath}`);
38
+ }
39
+
40
+ // Assets
41
+ const assetsBundle = await bundleAssets(
42
+ path.resolve(rootPath),
43
+ manifest.assets as string[] || [],
44
+ );
45
+ manifest.assets = assetsBundle.toJSON();
46
+
47
+ // Normalize the edge case of a plug with no functions
48
+ if (!manifest.functions) {
49
+ manifest.functions = {};
50
+ }
51
+
52
+ const jsFile = `
53
+ import { setupMessageListener } from "${
54
+ options.runtimeUrl || workerRuntimeUrl
55
+ }";
56
+
57
+ // Imports
58
+ ${
59
+ Object.entries(manifest.functions).map(([funcName, def]) => {
60
+ if (!def.path) {
61
+ return "";
62
+ }
63
+ let [filePath, jsFunctionName] = def.path.split(":");
64
+ // Resolve path
65
+ filePath = path.join(rootPath, filePath);
66
+
67
+ return `import {${jsFunctionName} as ${funcName}} from "file://${
68
+ // Replacing \ with / for Windows
69
+ path.resolve(filePath).replaceAll(
70
+ "\\",
71
+ "\\\\",
72
+ )}";\n`;
73
+ }).join("")
74
+ }
75
+
76
+ // Function mapping
77
+ const functionMapping = {
78
+ ${
79
+ Object.entries(manifest.functions).map(([funcName, def]) => {
80
+ if (!def.path) {
81
+ return "";
82
+ }
83
+ return ` ${funcName}: ${funcName},\n`;
84
+ }).join("")
85
+ }
86
+ };
87
+
88
+ // Manifest
89
+ const manifest = ${JSON.stringify(manifest, null, 2)};
90
+
91
+ export const plug = {manifest, functionMapping};
92
+
93
+ setupMessageListener(functionMapping, manifest, self.postMessage);
94
+ `;
95
+
96
+ // console.log("Code:", jsFile);
97
+
98
+ const tempDir = await mkdtemp(path.join(tmpdir(), "plug-compile-"));
99
+ const inFile = path.join(tempDir, "input.js");
100
+ const outFile = `${destPath}/${manifest.name}.plug.js`;
101
+ await writeFile(inFile, jsFile, "utf-8");
102
+
103
+ const result = await esbuild.build({
104
+ entryPoints: [inFile],
105
+ bundle: true,
106
+ format: "esm",
107
+ globalName: "mod",
108
+ platform: "browser",
109
+ sourcemap: "linked",
110
+ minify: !options.debug,
111
+ outfile: outFile,
112
+ metafile: options.info,
113
+ treeShaking: true,
114
+ plugins: [
115
+ denoPlugin({
116
+ configPath: options.configPath &&
117
+ path.resolve(process.cwd(), options.configPath),
118
+ }),
119
+ ],
120
+ });
121
+
122
+ if (options.info) {
123
+ const text = await esbuild.analyzeMetafile(result.metafile!);
124
+ console.log("Bundle info for", manifestPath, text);
125
+ }
126
+
127
+ let jsCode = await readFile(outFile, "utf-8");
128
+ jsCode = patchDenoLibJS(jsCode);
129
+ await writeFile(outFile, jsCode, "utf-8");
130
+
131
+ // Clean up temp directory
132
+ await rm(tempDir, { recursive: true, force: true });
133
+
134
+ console.log(`Plug ${manifest.name} written to ${outFile}.`);
135
+ return outFile;
136
+ }
137
+
138
+ export async function compileManifests(
139
+ manifestFiles: string[],
140
+ dist: string,
141
+ options: CompileOptions = {},
142
+ ) {
143
+ let building = false;
144
+ dist = path.resolve(dist);
145
+
146
+ async function buildAll() {
147
+ if (building) {
148
+ return;
149
+ }
150
+ console.log("Building", manifestFiles);
151
+ building = true;
152
+ await mkdir(dist, { recursive: true });
153
+ const startTime = Date.now();
154
+ // Build all plugs in parallel
155
+ await Promise.all(manifestFiles.map(async (plugManifestPath) => {
156
+ const manifestPath = plugManifestPath as string;
157
+ try {
158
+ await compileManifest(
159
+ manifestPath,
160
+ dist,
161
+ options,
162
+ );
163
+ } catch (e: any) {
164
+ console.error(`Error building ${manifestPath}:`, e.message);
165
+ throw e;
166
+ }
167
+ }));
168
+ console.log(`Done building plugs in ${Date.now() - startTime}ms`);
169
+ building = false;
170
+ }
171
+
172
+ await buildAll();
173
+ }
174
+
175
+ export function patchDenoLibJS(code: string): string {
176
+ // The Deno std lib has one occurence of a regex that Webkit JS doesn't (yet parse), we'll strip it because it's likely never invoked anyway, YOLO
177
+ return code.replaceAll("/(?<=\\n)/", "/()/");
178
+ }
179
+
180
+ export async function plugCompileCommand(
181
+ { dist, debug, info, config, runtimeUrl }: {
182
+ dist: string;
183
+ debug: boolean;
184
+ info: boolean;
185
+ config?: string;
186
+ runtimeUrl?: string;
187
+ },
188
+ ...manifestPaths: string[]
189
+ ) {
190
+ await compileManifests(
191
+ manifestPaths,
192
+ dist,
193
+ {
194
+ debug: debug,
195
+ info: info,
196
+ runtimeUrl,
197
+ configPath: config,
198
+ },
199
+ );
200
+ esbuild.stop();
201
+ process.exit(0);
202
+ }
@@ -0,0 +1,40 @@
1
+ import type { Manifest } from "@silverbulletmd/silverbullet/type/manifest";
2
+
3
+ // Messages received from the worker
4
+ export type ControllerMessage =
5
+ | {
6
+ // Parsed manifest when worker is initialized
7
+ type: "manifest";
8
+ manifest: Manifest;
9
+ }
10
+ | {
11
+ // Function invocation result
12
+ type: "invr";
13
+ id: number;
14
+ error?: string;
15
+ result?: any;
16
+ }
17
+ | {
18
+ // Syscall
19
+ type: "sys";
20
+ id: number;
21
+ name: string;
22
+ args: any[];
23
+ };
24
+
25
+ // Messages received inside the worker
26
+ export type WorkerMessage =
27
+ | {
28
+ // Function invocation
29
+ type: "inv";
30
+ id: number;
31
+ name: string;
32
+ args: any[];
33
+ }
34
+ | {
35
+ // Syscall result
36
+ type: "sysr";
37
+ id: number;
38
+ result?: any;
39
+ error?: any;
40
+ };
@@ -0,0 +1,53 @@
1
+ import { base64Decode, base64Encode } from "../../plug-api/lib/crypto.ts";
2
+
3
+ export type ProxyFetchRequest64 = {
4
+ method?: string;
5
+ headers?: Record<string, string>;
6
+ base64Body?: string;
7
+ };
8
+
9
+ export type ProxyFetchResponse64 = {
10
+ ok: boolean;
11
+ status: number;
12
+ headers: Record<string, string>;
13
+ // We base64 encode the body because the body can be binary data that we have to push through the worker boundary
14
+ base64Body: string;
15
+ };
16
+
17
+ export type ProxyFetchRequest = {
18
+ method?: string;
19
+ headers?: Record<string, string>;
20
+ responseEncoding?: string;
21
+ body?: Uint8Array | string | any;
22
+ };
23
+
24
+ export type ProxyFetchResponse = {
25
+ ok: boolean;
26
+ status: number;
27
+ headers: Record<string, string>;
28
+ // We base64 encode the body because the body can be binary data that we have to push through the worker boundary
29
+ body: Uint8Array | string | any;
30
+ };
31
+
32
+ export async function performLocalFetch(
33
+ url: string,
34
+ req: ProxyFetchRequest64,
35
+ ): Promise<ProxyFetchResponse64> {
36
+ const result = await fetch(
37
+ url,
38
+ req && {
39
+ method: req.method,
40
+ headers: req.headers,
41
+ body: req.base64Body && base64Decode(req.base64Body),
42
+ // Casting to "any" for now, since of weird Deno typing
43
+ } as any,
44
+ );
45
+ return {
46
+ ok: result.ok,
47
+ status: result.status,
48
+ headers: Object.fromEntries(result.headers.entries()),
49
+ base64Body: base64Encode(
50
+ new Uint8Array(await (await result.blob()).arrayBuffer()),
51
+ ),
52
+ };
53
+ }
@@ -0,0 +1,6 @@
1
+ import { WorkerSandbox } from "./worker_sandbox.ts";
2
+ import type { SandboxFactory } from "./sandbox.ts";
3
+
4
+ export function createSandbox<HookT>(workerUrl: URL): SandboxFactory<HookT> {
5
+ return (plug) => new WorkerSandbox(plug, workerUrl);
6
+ }
@@ -0,0 +1,14 @@
1
+ import type { Plug } from "../plug.ts";
2
+ import type { Manifest } from "../types.ts";
3
+
4
+ export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox<HookT>;
5
+
6
+ export interface Sandbox<HookT> {
7
+ manifest?: Manifest<HookT>;
8
+
9
+ init(): Promise<void>;
10
+
11
+ invoke(name: string, args: any[]): Promise<any>;
12
+
13
+ stop(): void;
14
+ }
@@ -0,0 +1,17 @@
1
+ import { WorkerSandbox } from "./worker_sandbox.ts";
2
+ import type { Plug } from "../plug.ts";
3
+ import type { SandboxFactory } from "./sandbox.ts";
4
+ import { fsEndpoint } from "../../spaces/constants.ts";
5
+
6
+ export function createWorkerSandboxFromLocalPath<HookT>(
7
+ name: string,
8
+ ): SandboxFactory<HookT> {
9
+ return (plug: Plug<HookT>) =>
10
+ new WorkerSandbox(
11
+ plug,
12
+ new URL(
13
+ name,
14
+ document.baseURI.slice(0, -1) + fsEndpoint + "/", // We're NOT striping trailing '/', this used to be `location.origin`
15
+ ),
16
+ );
17
+ }
@@ -0,0 +1,132 @@
1
+ import type { Manifest } from "../types.ts";
2
+ import type { ControllerMessage, WorkerMessage } from "../protocol.ts";
3
+ import type { Plug } from "../plug.ts";
4
+ import { AssetBundle, type AssetJson } from "../../asset_bundle/bundle.ts";
5
+ import type { Sandbox } from "./sandbox.ts";
6
+ import { race, timeout } from "@silverbulletmd/silverbullet/lib/async";
7
+
8
+ /**
9
+ * Represents a "safe" execution environment for plug code
10
+ * Effectively this wraps a web worker, the reason to have this split from Plugs is to allow plugs to manage multiple sandboxes, e.g. for performance in the future
11
+ */
12
+ export class WorkerSandbox<HookT> implements Sandbox<HookT> {
13
+ public manifest?: Manifest<HookT>;
14
+ private worker?: Worker;
15
+ private reqId = 0;
16
+ private outstandingInvocations = new Map<
17
+ number,
18
+ { resolve: (result: any) => void; reject: (e: any) => void }
19
+ >();
20
+
21
+ constructor(
22
+ readonly plug: Plug<HookT>,
23
+ public workerUrl: URL,
24
+ private workerOptions = {},
25
+ ) {
26
+ }
27
+
28
+ /**
29
+ * Should only invoked lazily (either by invoke, or by a ManifestCache to load the manifest)
30
+ */
31
+ init(): Promise<void> {
32
+ console.log("Booting up worker from", this.workerUrl.pathname);
33
+ if (this.worker) {
34
+ // Race condition
35
+ console.warn("Double init of sandbox, ignoring");
36
+ return Promise.resolve();
37
+ }
38
+ this.worker = new Worker(this.workerUrl, {
39
+ ...this.workerOptions,
40
+ type: "module",
41
+ });
42
+
43
+ return race([
44
+ // We're adding a timeout of 5s here to handle the case where a plug blows up during initialization
45
+ timeout(5000).catch((_) =>
46
+ Promise.reject(new Error("Plug timed out during creation"))
47
+ ),
48
+ new Promise((resolve) => {
49
+ this.worker!.onmessage = (ev) => {
50
+ if (ev.data.type === "manifest") {
51
+ this.manifest = ev.data.manifest;
52
+ // Set manifest in the plug
53
+ this.plug.manifest = this.manifest!;
54
+
55
+ // Set assets in the plug
56
+ this.plug.assets = new AssetBundle(
57
+ this.manifest?.assets ? this.manifest.assets as AssetJson : {},
58
+ );
59
+
60
+ return resolve();
61
+ }
62
+
63
+ this.onMessage(ev.data);
64
+ };
65
+ }),
66
+ ]);
67
+ }
68
+
69
+ async onMessage(data: ControllerMessage) {
70
+ if (!this.worker) {
71
+ console.warn("Received message for terminated worker, ignoring");
72
+ return;
73
+ }
74
+ switch (data.type) {
75
+ case "sys":
76
+ try {
77
+ const result = await this.plug.syscall(data.name!, data.args!);
78
+
79
+ this.worker && this.worker!.postMessage({
80
+ type: "sysr",
81
+ id: data.id,
82
+ result: result,
83
+ } as WorkerMessage);
84
+ } catch (e: any) {
85
+ // console.error("Syscall fail", e);
86
+ this.worker && this.worker!.postMessage({
87
+ type: "sysr",
88
+ id: data.id,
89
+ error: e.message,
90
+ } as WorkerMessage);
91
+ }
92
+ break;
93
+ case "invr": {
94
+ const resultCbs = this.outstandingInvocations.get(data.id!);
95
+ this.outstandingInvocations.delete(data.id!);
96
+ if (data.error) {
97
+ resultCbs &&
98
+ resultCbs.reject(new Error(data.error));
99
+ } else {
100
+ resultCbs && resultCbs.resolve(data.result);
101
+ }
102
+ break;
103
+ }
104
+ default:
105
+ console.error("Unknown message type", data);
106
+ }
107
+ }
108
+
109
+ async invoke(name: string, args: any[]): Promise<any> {
110
+ if (!this.worker) {
111
+ // Lazy initialization
112
+ await this.init();
113
+ }
114
+ this.reqId++;
115
+ this.worker!.postMessage({
116
+ type: "inv",
117
+ id: this.reqId,
118
+ name,
119
+ args,
120
+ } as WorkerMessage);
121
+ return new Promise((resolve, reject) => {
122
+ this.outstandingInvocations.set(this.reqId, { resolve, reject });
123
+ });
124
+ }
125
+
126
+ stop() {
127
+ if (this.worker) {
128
+ this.worker.terminate();
129
+ this.worker = undefined;
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,35 @@
1
+ import type { SysCallMapping, System } from "../system.ts";
2
+ import type { FileMeta } from "../../../plug-api/types/index.ts";
3
+
4
+ export default function assetSyscalls(system: System<any>): SysCallMapping {
5
+ return {
6
+ "asset.readAsset": (_ctx, plugName: string, name: string): string => {
7
+ return system.loadedPlugs.get(plugName)!.assets!.readFileAsDataUrl(
8
+ name,
9
+ );
10
+ },
11
+ "asset.listFiles": (_ctx, plugName: string): FileMeta[] => {
12
+ const assets = system.loadedPlugs.get(plugName)!.assets!;
13
+ const fileNames = assets.listFiles();
14
+ return fileNames.map((name) => ({
15
+ name,
16
+ contentType: assets.getMimeType(name),
17
+ created: assets.getMtime(name),
18
+ lastModified: assets.getMtime(name),
19
+ size: -1,
20
+ perm: "ro",
21
+ }));
22
+ },
23
+ "asset.getFileMeta": (_ctx, plugName: string, name: string): FileMeta => {
24
+ const assets = system.loadedPlugs.get(plugName)!.assets!;
25
+ return {
26
+ name,
27
+ contentType: assets.getMimeType(name),
28
+ created: assets.getMtime(name),
29
+ lastModified: assets.getMtime(name),
30
+ size: -1,
31
+ perm: "ro",
32
+ };
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,21 @@
1
+ import type { SysCallMapping } from "../../plugos/system.ts";
2
+ import type { DataStore } from "../../data/datastore.ts";
3
+
4
+ import type { KvKey } from "@silverbulletmd/silverbullet/type/datastore";
5
+
6
+ export function clientStoreSyscalls(
7
+ ds: DataStore,
8
+ prefix: KvKey = ["client"],
9
+ ): SysCallMapping {
10
+ return {
11
+ "clientStore.get": (_ctx, key: string): Promise<any> => {
12
+ return ds.get([...prefix, key]);
13
+ },
14
+ "clientStore.set": (_ctx, key: string, val: any): Promise<void> => {
15
+ return ds.set([...prefix, key], val);
16
+ },
17
+ "clientStore.delete": (_ctx, key: string): Promise<void> => {
18
+ return ds.delete([...prefix, key]);
19
+ },
20
+ };
21
+ }