@silverbulletmd/silverbullet 2.10.0 → 2.12.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 (105) hide show
  1. package/README.md +6 -6
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/data/kv_primitives.ts +2 -2
  5. package/client/markdown_parser/constants.ts +5 -1
  6. package/client/plugos/hooks/command.ts +8 -6
  7. package/client/plugos/hooks/event.ts +60 -24
  8. package/client/plugos/hooks/mq.ts +0 -3
  9. package/client/plugos/hooks/slash_command.ts +3 -20
  10. package/client/plugos/hooks/syscall.ts +0 -4
  11. package/client/plugos/manifest_cache.ts +2 -34
  12. package/client/plugos/plug.ts +0 -3
  13. package/client/plugos/plug_compile.ts +3 -13
  14. package/client/plugos/sandboxes/sandbox.ts +2 -2
  15. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  16. package/client/plugos/syscalls/capture.ts +104 -0
  17. package/client/plugos/syscalls/editor.ts +197 -56
  18. package/client/plugos/syscalls/event.ts +12 -9
  19. package/client/plugos/syscalls/fetch.ts +0 -4
  20. package/client/plugos/syscalls/icon.ts +46 -0
  21. package/client/plugos/syscalls/index.ts +33 -13
  22. package/client/plugos/syscalls/jsonschema.ts +0 -1
  23. package/client/plugos/syscalls/mq.ts +0 -1
  24. package/client/plugos/syscalls/navigator.ts +138 -0
  25. package/client/plugos/syscalls/search.ts +44 -0
  26. package/client/plugos/syscalls/shell.ts +0 -1
  27. package/client/plugos/syscalls/space.ts +250 -7
  28. package/client/plugos/syscalls/sync.ts +70 -4
  29. package/client/plugos/syscalls/system.ts +39 -0
  30. package/client/plugos/system.ts +9 -7
  31. package/client/plugos/types.ts +6 -6
  32. package/client/plugos/worker_runtime.ts +0 -2
  33. package/client/space_lua/aggregates.ts +6 -22
  34. package/client/space_lua/ast.ts +0 -5
  35. package/client/space_lua/ast_narrow.ts +0 -5
  36. package/client/space_lua/budget.ts +113 -0
  37. package/client/space_lua/budget_ui.ts +49 -0
  38. package/client/space_lua/eval.ts +113 -88
  39. package/client/space_lua/labels.ts +0 -4
  40. package/client/space_lua/numeric.ts +2 -7
  41. package/client/space_lua/parse.ts +0 -17
  42. package/client/space_lua/quarantine.ts +127 -0
  43. package/client/space_lua/query_collection.ts +18 -51
  44. package/client/space_lua/query_env.ts +0 -2
  45. package/client/space_lua/render_lua_markdown.ts +2 -7
  46. package/client/space_lua/render_widget.ts +18 -1
  47. package/client/space_lua/runtime.ts +64 -77
  48. package/client/space_lua/stdlib/format.ts +0 -12
  49. package/client/space_lua/stdlib/js.ts +0 -2
  50. package/client/space_lua/stdlib/load.ts +0 -1
  51. package/client/space_lua/stdlib/math.ts +0 -7
  52. package/client/space_lua/stdlib/net.ts +0 -4
  53. package/client/space_lua/stdlib/os.ts +0 -10
  54. package/client/space_lua/stdlib/pattern.ts +18 -23
  55. package/client/space_lua/stdlib/space_lua.ts +1 -2
  56. package/client/space_lua/stdlib/string_pack.ts +4 -7
  57. package/client/space_lua/stdlib/table.ts +0 -5
  58. package/client/space_lua/stdlib.ts +18 -31
  59. package/client/space_lua/tonumber.ts +0 -9
  60. package/dist/plug-compile.js +3 -4
  61. package/package.json +52 -41
  62. package/plug-api/lib/async.ts +1 -6
  63. package/plug-api/lib/collation.ts +22 -0
  64. package/plug-api/lib/crypto.ts +16 -15
  65. package/plug-api/lib/dates.ts +33 -0
  66. package/plug-api/lib/fuzzy.ts +282 -0
  67. package/plug-api/lib/json.ts +0 -6
  68. package/plug-api/lib/limited_map.ts +0 -2
  69. package/plug-api/lib/link_write.ts +27 -0
  70. package/plug-api/lib/ref.ts +28 -5
  71. package/plug-api/lib/resolve.ts +1 -9
  72. package/plug-api/lib/resolve_path.ts +290 -0
  73. package/plug-api/lib/shortcut.ts +15 -0
  74. package/plug-api/lib/tags.ts +0 -3
  75. package/plug-api/lib/transclusion.ts +28 -1
  76. package/plug-api/lib/tree.ts +0 -5
  77. package/plug-api/lib/yaml.ts +5 -44
  78. package/plug-api/syscall.ts +1 -5
  79. package/plug-api/syscalls/config.ts +2 -2
  80. package/plug-api/syscalls/editor.ts +75 -213
  81. package/plug-api/syscalls/icon.ts +18 -0
  82. package/plug-api/syscalls/index.ts +22 -4
  83. package/plug-api/syscalls/search.ts +11 -0
  84. package/plug-api/syscalls/space.ts +122 -5
  85. package/plug-api/syscalls/sync.ts +31 -2
  86. package/plug-api/syscalls/system.ts +46 -0
  87. package/plug-api/syscalls.ts +2 -0
  88. package/plug-api/system_mock.ts +13 -0
  89. package/plug-api/types/client.ts +20 -4
  90. package/plug-api/types/datastore.ts +11 -2
  91. package/plug-api/types/index.ts +10 -0
  92. package/plug-api/types/manifest.ts +7 -0
  93. package/plug-api/types/profile.ts +13 -0
  94. package/plug-api/types/revisions.ts +67 -0
  95. package/plug-api/ui/description.ts +64 -0
  96. package/plug-api/ui/hover.ts +69 -0
  97. package/plug-api/ui/index.ts +58 -2
  98. package/plug-api/ui/scroll.ts +30 -0
  99. package/plug-api/ui/tree_model.ts +285 -0
  100. package/plug-api/ui/tree_types.ts +67 -0
  101. package/plug-api/ui/use_fit_collapse.ts +67 -0
  102. package/plugs/builtin_plugs.ts +0 -1
  103. package/plugs/index/types.ts +9 -0
  104. package/plug-api/lib/memory_cache.ts +0 -21
  105. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
package/README.md CHANGED
@@ -24,7 +24,7 @@ You were told there’s no such thing as a silver bullet. You were told wrong.
24
24
  [Much more detail can be found on silverbullet.md](https://silverbullet.md)
25
25
 
26
26
  ## Installing SilverBullet
27
- Check out the [instructions](https://silverbullet.md/Install).
27
+ Check out the [instructions](https://docs.silverbullet.md/Install).
28
28
 
29
29
  ## Developing SilverBullet
30
30
  SilverBullet's frontend ("client") is written in [TypeScript](https://www.typescriptlang.org/) and built on top of the excellent [CodeMirror 6](https://codemirror.net/) editor component. Additional UI is built using [Preact](https://preactjs.com). [ESBuild](https://esbuild.github.io) is used to build the frontend.
@@ -46,7 +46,7 @@ The server backend is written in [Rust](https://www.rust-lang.org/) (a Cargo wor
46
46
  * `syscalls/`: TypeScript wrappers around syscalls
47
47
  * `types/`: Various (client) types that can be references from plugs
48
48
  * `scripts/`: Useful scripts
49
- * `docs/`: documentation (also serves a silverbullet.md website content)
49
+ * `docs/`: documentation (published as docs.silverbullet.md and bundled as Desktop's Help)
50
50
 
51
51
  ### Requirements
52
52
  * [Node.js](https://nodejs.org/) 24+ and npm 10+: Used to build the frontend (client) and plugs
@@ -83,7 +83,7 @@ Run the server in development with `cargo run`. A **debug** build serves the cli
83
83
 
84
84
  ```shell
85
85
  npm run build
86
- cargo run <PATH-TO-YOUR-SPACE>
86
+ cargo run <PATH-TO-YOUR-DATA-FOLDER>
87
87
  ```
88
88
 
89
89
  **When you change only the client** (TypeScript in `client/`): you do **not** need to restart the server. Rebuild just the client bundle and reload the page in your browser — the debug server serves the freshly-built bundle straight from disk:
@@ -96,7 +96,7 @@ To build a self-contained **release** binary (with the client bundle embedded),
96
96
 
97
97
  ```shell
98
98
  make
99
- ./target/release/silverbullet <PATH-TO-YOUR-SPACE>
99
+ ./target/release/silverbullet <PATH-TO-YOUR-DATA-FOLDER>
100
100
  ```
101
101
 
102
102
  ### Useful development tasks
@@ -115,7 +115,7 @@ make bench
115
115
  ```
116
116
 
117
117
  ### Build a docker container
118
- Note, you do not need Node.js nor Go locally installed for this to work:
118
+ Note, you do not need Node.js nor Rust locally installed for this to work:
119
119
 
120
120
  ```shell
121
121
  docker build -t silverbullet .
@@ -123,5 +123,5 @@ docker build -t silverbullet .
123
123
 
124
124
  To run:
125
125
  ```shell
126
- docker run -p 3000:3000 -v <PATH-TO-YOUR-SPACE>:/space silverbullet
126
+ docker run -p 3000:3000 -v <PATH-TO-YOUR-DATA-FOLDER>:/data silverbullet
127
127
  ```
@@ -66,7 +66,6 @@ export class AssetBundle {
66
66
  data: Uint8Array,
67
67
  mtime: number = Date.now(),
68
68
  ) {
69
- // Replace \ with / for windows
70
69
  path = path.replaceAll("\\", "/");
71
70
  this.bundle[path] = {
72
71
  data: base64EncodedDataUrl(mimeType, data),
@@ -0,0 +1,343 @@
1
+ import { type OutputUnit, Validator, format } from "@cfworker/json-schema";
2
+ import { stripFunctions } from "./plugos/util.ts";
3
+
4
+ format.email = (data: string) => data.includes("@");
5
+ format["page-ref"] = (data: string) =>
6
+ data.startsWith("[[") && data.endsWith("]]");
7
+
8
+ function cfwFormatErrors(errors: OutputUnit[]): string {
9
+ // Filter out "properties" wrapper errors, keep only the specific leaf errors
10
+ const leafErrors = errors.filter((e) => e.keyword !== "properties");
11
+ const errorsToUse = leafErrors.length > 0 ? leafErrors : errors;
12
+
13
+ return errorsToUse
14
+ .map((e) => {
15
+ // Convert instanceLocation from "#/foo/bar" to "foo.bar"
16
+ const path =
17
+ e.instanceLocation === "#"
18
+ ? ""
19
+ : e.instanceLocation.slice(2).replaceAll("/", ".");
20
+ return path ? `${path}: ${e.error}` : e.error;
21
+ })
22
+ .join(", ");
23
+ }
24
+
25
+ /**
26
+ * Validates that a value looks like a valid JSON schema.
27
+ * Uses a lightweight structural check rather than full meta-schema validation.
28
+ */
29
+ function isValidJsonSchema(schema: any): { valid: boolean; error?: string } {
30
+ if (schema === null || schema === undefined) {
31
+ return { valid: false, error: "schema must not be null or undefined" };
32
+ }
33
+ if (typeof schema === "boolean") {
34
+ return { valid: true };
35
+ }
36
+ if (typeof schema !== "object" || Array.isArray(schema)) {
37
+ return { valid: false, error: "schema must be an object or boolean" };
38
+ }
39
+ if (schema.type !== undefined) {
40
+ const validTypes = [
41
+ "string",
42
+ "number",
43
+ "integer",
44
+ "boolean",
45
+ "object",
46
+ "array",
47
+ "null",
48
+ ];
49
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
50
+ for (const t of types) {
51
+ if (!validTypes.includes(t)) {
52
+ return {
53
+ valid: false,
54
+ error: `schema.type must be one of ${validTypes.join(", ")}`,
55
+ };
56
+ }
57
+ }
58
+ }
59
+ return { valid: true };
60
+ }
61
+
62
+ /**
63
+ * Configuration management (config.* APIs) for the client
64
+ */
65
+ export type CategoryDefinition = {
66
+ name: string;
67
+ description?: string;
68
+ priority?: number;
69
+ };
70
+
71
+ export class Config {
72
+ public schemas: Record<string, any> = {
73
+ type: "object",
74
+ properties: {},
75
+ };
76
+
77
+ /**
78
+ * Registered UI categories for the configuration manager. Categories appear
79
+ * in the UI in descending `priority` (higher first); categories referenced
80
+ * by a schema's `ui.category` but never registered fall to the bottom in
81
+ * alphabetical order.
82
+ */
83
+ public categories: Record<string, CategoryDefinition> = {};
84
+
85
+ constructor(public values: Record<string, any> = {}) {}
86
+
87
+ public clear() {
88
+ this.schemas = {
89
+ type: "object",
90
+ properties: {},
91
+ };
92
+ this.values = {};
93
+ this.categories = {};
94
+ }
95
+
96
+ /**
97
+ * Defines (or updates) a UI category for the configuration manager. The
98
+ * category's `name` is the same string used in a schema field's
99
+ * `ui.category`.
100
+ */
101
+ defineCategory(definition: CategoryDefinition): void {
102
+ if (typeof definition?.name !== "string" || !definition.name) {
103
+ throw new Error("defineCategory requires a non-empty name");
104
+ }
105
+ this.categories[definition.name] = { ...definition };
106
+ }
107
+
108
+ /**
109
+ * Defines a JSON schema for a configuration key
110
+ * @param key The configuration key to define a schema for
111
+ * @param schema The JSON schema to validate against
112
+ */
113
+ define(key: string | string[], schema: any): void {
114
+ const result = isValidJsonSchema(schema);
115
+ if (!result.valid) {
116
+ throw new Error(`Invalid schema for key ${key}: ${result.error}`);
117
+ }
118
+
119
+ if (typeof key === "string") {
120
+ key = key.split(".");
121
+ }
122
+
123
+ let current = this.schemas;
124
+ for (let i = 0; i < key.length - 1; i++) {
125
+ const part = key[i];
126
+ if (!current.properties[part]) {
127
+ current.properties[part] = {
128
+ type: "object",
129
+ properties: {},
130
+ };
131
+ }
132
+ current = current.properties[part];
133
+ }
134
+
135
+ const finalKey = key[key.length - 1];
136
+ current.properties[finalKey] = schema;
137
+
138
+ this.applySchemaDefaults(key, schema);
139
+ }
140
+
141
+ /**
142
+ * Recursively applies default values from a schema definition.
143
+ * Only sets a value if none exists at that path.
144
+ */
145
+ private applySchemaDefaults(path: string[], schema: any): void {
146
+ if ("default" in schema) {
147
+ if (!this.has(path)) {
148
+ this.set(path, structuredClone(schema.default));
149
+ }
150
+ }
151
+ if (schema.properties) {
152
+ for (const [propKey, propSchema] of Object.entries(schema.properties)) {
153
+ this.applySchemaDefaults([...path, propKey], propSchema as any);
154
+ }
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Gets a value from the config
160
+ * @param path The path to get, supports dot notation (e.g. "foo.bar.baz")
161
+ * @param defaultValue The default value to return if the path doesn't exist
162
+ * @returns The value at the path, or the default value
163
+ */
164
+ get<T>(path: string | string[], defaultValue: T): T {
165
+ if (typeof path === "string") {
166
+ path = path.split(".");
167
+ }
168
+ const resolved = resolvePath(this.values, path);
169
+ if (!resolved) {
170
+ return defaultValue;
171
+ }
172
+
173
+ return (resolved.obj[resolved.key] ?? defaultValue) as T;
174
+ }
175
+
176
+ /**
177
+ * Sets a value in the config
178
+ * @param path The path to set, supports dot notation (e.g. "foo.bar.baz")
179
+ * @param value The value to set
180
+ */
181
+ set<T>(path: string, value: T): void;
182
+ /**
183
+ * Sets a value in the config
184
+ * @param path The path to set (one path element per array element)
185
+ * @param value The value to set
186
+ */
187
+ set<T>(path: string[], value: T): void;
188
+
189
+ /**
190
+ * Sets multiple values in the config
191
+ * @param values An object containing key-value pairs to set
192
+ */
193
+ set(values: Record<string, any>): void;
194
+
195
+ set<T>(
196
+ keyOrValues: string | string[] | Record<string, any>,
197
+ value?: T,
198
+ ): void {
199
+ if (typeof keyOrValues === "string") {
200
+ keyOrValues = keyOrValues.split(".");
201
+ }
202
+ if (Array.isArray(keyOrValues)) {
203
+ const key = keyOrValues as string[];
204
+
205
+ const resolved = resolvePath(this.values, key, true);
206
+ if (resolved) {
207
+ resolved.obj[resolved.key] = value;
208
+ } else {
209
+ throw new Error(`Invalid key ${key}`);
210
+ }
211
+
212
+ // Re-apply nested schema defaults so partial object writes don't wipe
213
+ // out defaults for nested properties the user didn't specify.
214
+ const schema = this.getSchemaAtPath(key);
215
+ if (schema) {
216
+ this.applySchemaDefaults(key, schema);
217
+ }
218
+
219
+ this.validatePath(key);
220
+ } else {
221
+ for (const [key, val] of Object.entries(keyOrValues)) {
222
+ this.set(key, val);
223
+ }
224
+ }
225
+ }
226
+
227
+ insert<T>(key: string | string[], value?: T): void {
228
+ if (typeof key === "string") {
229
+ key = key.split(".");
230
+ }
231
+ const resolved = resolvePath(this.values, key, true);
232
+ if (resolved) {
233
+ if (!Array.isArray(resolved.obj[resolved.key])) {
234
+ resolved.obj[resolved.key] = [];
235
+ }
236
+ resolved.obj[resolved.key].push(value);
237
+ } else {
238
+ throw new Error(`Invalid key ${key}`);
239
+ }
240
+
241
+ this.validatePath(key);
242
+ }
243
+
244
+ /**
245
+ * Checks if a path exists in the config
246
+ * @param path The path to check, supports dot notation (e.g. "foo.bar.baz")
247
+ * @returns True if the path exists, false otherwise
248
+ */
249
+ has(path: string | string[]): boolean {
250
+ if (typeof path === "string") {
251
+ path = path.split(".");
252
+ }
253
+ const resolved = resolvePath(this.values, path);
254
+ if (!resolved) {
255
+ return false;
256
+ }
257
+
258
+ return resolved.key in resolved.obj;
259
+ }
260
+
261
+ /**
262
+ * Lists all configuration keys in the config
263
+ * @returns An array of keys
264
+ */
265
+ keys(): string[] {
266
+ return Object.keys(this.values);
267
+ }
268
+
269
+ /**
270
+ * Validates a specific path against its schema
271
+ */
272
+ private validatePath(path: string[]): void {
273
+ for (let i = path.length; i > 0; i--) {
274
+ const schemaPath = path.slice(0, i);
275
+ const schema = this.getSchemaAtPath(schemaPath);
276
+ if (schema) {
277
+ const valueAtPath = this.get(schemaPath, undefined);
278
+ if (valueAtPath !== undefined) {
279
+ const validator = new Validator(schema, "7");
280
+ const result = validator.validate(stripFunctions(valueAtPath));
281
+ if (!result.valid) {
282
+ const errorText = cfwFormatErrors(result.errors);
283
+ throw new Error(
284
+ `Validation error for ${schemaPath.join(".")}:> ${errorText}`,
285
+ );
286
+ }
287
+ }
288
+ break; // Only validate the most specific schema
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Gets the schema at a specific path
295
+ */
296
+ private getSchemaAtPath(path: string[]): any | null {
297
+ let current = this.schemas;
298
+ for (const part of path) {
299
+ if (!current.properties || !current.properties[part]) {
300
+ return null;
301
+ }
302
+ current = current.properties[part];
303
+ }
304
+ return current.type ? current : null;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Resolves a configuration path to the containing object and final key
310
+ * @param path The path to resolve (e.g. ["foo", "bar", "baz"])
311
+ * @param create Whether to create objects along the path if they don't exist
312
+ * @returns The containing object and the final key, or null if the path cannot be resolved
313
+ */
314
+ function resolvePath(
315
+ inObject: Record<string, any>,
316
+ path: string[],
317
+ create = false,
318
+ ): { obj: any; key: string } | null {
319
+ path = [...path];
320
+
321
+ const lastKey = path.pop()!;
322
+ let current = inObject;
323
+
324
+ for (const part of path) {
325
+ if (current[part] === undefined) {
326
+ if (create) {
327
+ current[part] = {};
328
+ } else {
329
+ return null;
330
+ }
331
+ } else if (typeof current[part] !== "object" || current[part] === null) {
332
+ if (create) {
333
+ current[part] = {};
334
+ } else {
335
+ return null;
336
+ }
337
+ }
338
+
339
+ current = current[part];
340
+ }
341
+
342
+ return { obj: current, key: lastKey };
343
+ }
@@ -4,7 +4,7 @@ export type KvQueryOptions = {
4
4
  prefix?: KvKey;
5
5
  };
6
6
 
7
- export interface KvPrimitives {
7
+ export type KvPrimitives = {
8
8
  batchGet(keys: KvKey[]): Promise<(any | undefined)[]>;
9
9
 
10
10
  batchSet(entries: KV[]): Promise<void>;
@@ -22,4 +22,4 @@ export interface KvPrimitives {
22
22
  clear(): Promise<void>;
23
23
 
24
24
  close(): void;
25
- }
25
+ };
@@ -3,7 +3,11 @@ export const wikiLinkRegex =
3
3
  export const mdLinkRegex =
4
4
  /!?\[(?<title>[^\]\\]*(?:\\.[^\]\\]*)*)\]\((?<url>.+)\)/g;
5
5
  export const tagRegex =
6
- /#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:<[^>\n]+>))/;
6
+ /#(?:(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\';\/–—][^\s!@#$%^&*(),.?":{}|<>\\]*)|(?:<[^>\n]+>))(?<!-)/;
7
+ // Dots and apostrophes may only join name segments, so `@pete.` and `@Chris'`
8
+ // end before them; a possessive `'s` never joins, so `@Foo's` is `@Foo`.
9
+ export const atMentionRegex =
10
+ /@(?:\d*[^\d\s!@#$%^&*(),.?":{}|<>\\\/'’][^\s!@#$%^&*(),.?":{}|<>\\\/'’]*(?:(?:\.|['’](?![sS](?![^\s!@#$%^&*(),.?":{}|<>\\\/'’])))[^\s!@#$%^&*(),.?":{}|<>\\\/'’]+)*)/;
7
11
  export const nakedUrlRegex =
8
12
  /(^https?:\/\/([-a-zA-Z0-9@:%_+~#=]|(?:[.](?!(\s|$)))){1,256})(([-a-zA-Z0-9(@:%_+~#?&=/]|(?:[.,:;)](?!(\s|$))))*)/;
9
13
  export const frontmatterQuotesRegex = /["'].*["']/g;
@@ -19,6 +19,7 @@ export class CommandHook
19
19
  constructor(
20
20
  private readOnly: boolean,
21
21
  private additionalCommands: Map<string, Command>,
22
+ private disableServiceWorker = false,
22
23
  ) {
23
24
  super();
24
25
  }
@@ -39,7 +40,6 @@ export class CommandHook
39
40
  cmd: Command,
40
41
  ) {
41
42
  if (commands.has(name)) {
42
- // Existing command, let's do some inline patching
43
43
  const existingCommand = commands.get(name)!;
44
44
  const command: Command = {
45
45
  ...existingCommand,
@@ -66,11 +66,12 @@ export class CommandHook
66
66
  if (cmd.requireMode === "rw" && this.readOnly) {
67
67
  continue;
68
68
  }
69
+ if (cmd.requireServiceWorker && this.disableServiceWorker) {
70
+ continue;
71
+ }
69
72
  commands.set(name, cmd);
70
73
  }
71
- // Add commands from plugs
72
74
  if (!this.system) {
73
- // Not initialized yet
74
75
  return commands;
75
76
  }
76
77
  for (const plug of this.system.loadedPlugs.values()) {
@@ -82,7 +83,9 @@ export class CommandHook
82
83
  }
83
84
  const cmd = functionDef.command;
84
85
  if (cmd.requireMode === "rw" && this.readOnly) {
85
- // Bit hacky, but don't expose commands that require write mode in read-only mode
86
+ continue;
87
+ }
88
+ if (cmd.requireServiceWorker && this.disableServiceWorker) {
86
89
  continue;
87
90
  }
88
91
  this.mergeCommand(commands, cmd.name, {
@@ -102,7 +105,7 @@ export class CommandHook
102
105
  }
103
106
 
104
107
  public buildAllCommandsAndEmit() {
105
- this.emit("commandsUpdated", this.buildAllCommands());
108
+ void this.emit("commandsUpdated", this.buildAllCommands());
106
109
  }
107
110
 
108
111
  apply(system: System<CommandHookT>): void {
@@ -112,7 +115,6 @@ export class CommandHook
112
115
  this.throttledBuildAllCommandsAndEmit();
113
116
  },
114
117
  });
115
- // On next tick
116
118
  setTimeout(() => {
117
119
  this.throttledBuildAllCommandsAndEmit();
118
120
  });
@@ -3,6 +3,10 @@ import type { System } from "../system.ts";
3
3
  import type { EventHookI } from "../eventhook.ts";
4
4
  import type { EventHookT } from "@silverbulletmd/silverbullet/type/manifest";
5
5
  import type { Config } from "../../config.ts";
6
+ import type { Ref } from "@silverbulletmd/silverbullet/lib/ref";
7
+ import { listenerDefinition, type DefinedListener } from "../syscalls/event.ts";
8
+
9
+ export type EventResultWithSource = { value: any; definition: Ref | null };
6
10
 
7
11
  // System events:
8
12
  // - plug:load (plugName: string)
@@ -66,10 +70,33 @@ export class EventHook implements EventHookI {
66
70
  }
67
71
 
68
72
  async dispatchEvent(eventName: string, ...args: any[]): Promise<any[]> {
73
+ return (await this.dispatch(eventName, args, false)).map(
74
+ ({ value }) => value,
75
+ );
76
+ }
77
+
78
+ async dispatchEventStrict(eventName: string, ...args: any[]): Promise<any[]> {
79
+ return (await this.dispatch(eventName, args, true)).map(
80
+ ({ value }) => value,
81
+ );
82
+ }
83
+
84
+ async dispatchEventWithSources(
85
+ eventName: string,
86
+ ...args: any[]
87
+ ): Promise<EventResultWithSource[]> {
88
+ return this.dispatch(eventName, args, false);
89
+ }
90
+
91
+ private async dispatch(
92
+ eventName: string,
93
+ args: any[],
94
+ strict: boolean,
95
+ ): Promise<EventResultWithSource[]> {
69
96
  if (!this.system) {
70
97
  throw new Error("Event hook is not initialized");
71
98
  }
72
- const promises: Promise<any>[] = [];
99
+ const promises: { promise: Promise<any>; definition: Ref | null }[] = [];
73
100
  for (const plug of this.system.loadedPlugs.values()) {
74
101
  const manifest = plug.manifest;
75
102
  for (const [name, functionDef] of Object.entries(manifest!.functions)) {
@@ -79,11 +106,10 @@ export class EventHook implements EventHookI {
79
106
  event === eventName ||
80
107
  eventNameToRegex(event).test(eventName)
81
108
  ) {
82
- // Only dispatch functions that can run in this environment
83
109
  if (plug.canInvoke(name)) {
84
- // Queue the promise
85
- promises.push(
86
- (async () => {
110
+ promises.push({
111
+ definition: null,
112
+ promise: (async () => {
87
113
  try {
88
114
  return await plug.invoke(name, args);
89
115
  } catch (e: any) {
@@ -93,7 +119,7 @@ export class EventHook implements EventHookI {
93
119
  throw e;
94
120
  }
95
121
  })(),
96
- );
122
+ });
97
123
  }
98
124
  }
99
125
  }
@@ -101,31 +127,28 @@ export class EventHook implements EventHookI {
101
127
  }
102
128
  }
103
129
 
104
- // Local listeners
105
130
  for (const [name, localListeners] of this.localListeners) {
106
131
  if (eventNameToRegex(name).test(eventName)) {
107
132
  for (const localListener of localListeners) {
108
- // Queue the promise
109
- promises.push(
110
- (async () => {
133
+ promises.push({
134
+ definition: null,
135
+ promise: (async () => {
111
136
  return await Promise.resolve(localListener(...args));
112
137
  })(),
113
- );
138
+ });
114
139
  }
115
140
  }
116
141
  }
117
142
 
118
- // Space Lua listeners
119
143
  if (this.config) {
120
- const configListeners: Record<string, Function[]> = this.config.get(
121
- "eventListeners",
122
- {},
123
- );
144
+ const configListeners: Record<string, DefinedListener[]> =
145
+ this.config.get("eventListeners", {});
124
146
  for (const [name, listeners] of Object.entries(configListeners)) {
125
147
  if (eventNameToRegex(name).test(eventName)) {
126
148
  for (const listener of listeners) {
127
- promises.push(
128
- (async () => {
149
+ promises.push({
150
+ definition: listener[listenerDefinition] ?? null,
151
+ promise: (async () => {
129
152
  return await Promise.resolve(
130
153
  listener({
131
154
  name: eventName,
@@ -134,15 +157,25 @@ export class EventHook implements EventHookI {
134
157
  }),
135
158
  );
136
159
  })(),
137
- );
160
+ });
138
161
  }
139
162
  }
140
163
  }
141
164
  }
142
165
 
143
- // Wait for all promises to resolve
144
- return (await Promise.allSettled(promises))
145
- .filter((result) => {
166
+ const settled = await Promise.allSettled(
167
+ promises.map(({ promise }) => promise),
168
+ );
169
+ if (strict) {
170
+ const failure = settled.find((result) => result.status === "rejected");
171
+ if (failure?.status === "rejected") throw failure.reason;
172
+ }
173
+ return settled
174
+ .map((result, index) => ({
175
+ result,
176
+ definition: promises[index].definition,
177
+ }))
178
+ .filter(({ result }) => {
146
179
  if (result.status === "rejected") {
147
180
  console.error(
148
181
  "Error while dispatching event",
@@ -153,8 +186,11 @@ export class EventHook implements EventHookI {
153
186
  }
154
187
  return result.status === "fulfilled";
155
188
  })
156
- .map((result) => result.value)
157
- .filter((result) => result != null); // This keeps non-null/undefined results
189
+ .map(({ result, definition }) => ({
190
+ value: result.status === "fulfilled" ? result.value : undefined,
191
+ definition,
192
+ }))
193
+ .filter(({ value }) => value != null); // This keeps non-null/undefined results
158
194
  }
159
195
 
160
196
  apply(system: System<EventHookT>): void {
@@ -48,7 +48,6 @@ export class MQHook implements Hook<MQHookT> {
48
48
 
49
49
  reloadQueues() {
50
50
  this.stop();
51
- // Plug based subscriptions
52
51
  for (const plug of this.system.loadedPlugs.values()) {
53
52
  if (!plug.manifest) {
54
53
  continue;
@@ -96,14 +95,12 @@ export class MQHook implements Hook<MQHookT> {
96
95
  }
97
96
  }
98
97
  }
99
- // Space Lua based subscriptions
100
98
  const configListeners: Record<string, MQListenerSpec[]> = this.config.get(
101
99
  "mqSubscriptions",
102
100
  {},
103
101
  );
104
102
  for (const [queue, listeners] of Object.entries(configListeners)) {
105
103
  for (const listener of listeners) {
106
- // console.log("Subscribing to", queue, listener);
107
104
  this.subscriptions.push(
108
105
  this.mq.subscribe(
109
106
  queue,