@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
@@ -0,0 +1,270 @@
1
+ import type { ASTCtx, LuaBlock, LuaExpression } from "./ast.ts";
2
+ import { evalExpression } from "./eval.ts";
3
+ import { parseBlock, parseExpressionString } from "./parse.ts";
4
+ import {
5
+ type PrintOptions,
6
+ prettyPrintBlock,
7
+ prettyPrintExpression,
8
+ } from "./pretty_print.ts";
9
+ import {
10
+ isILuaFunction,
11
+ LuaEnv,
12
+ LuaStackFrame,
13
+ LuaTable,
14
+ luaGet,
15
+ luaKeys,
16
+ luaToString,
17
+ luaTypeOf,
18
+ luaValueToJS,
19
+ } from "./runtime.ts";
20
+ import { buildThreadLocalEnv } from "../space_lua_api.ts";
21
+ import type { SysCallMapping, System } from "../plugos/system.ts";
22
+ import { isSendable } from "../plugos/util.ts";
23
+ import type {
24
+ LuaFunctionInfo,
25
+ LuaPropertyInspection,
26
+ LuaValueInspection,
27
+ } from "../../plug-api/types/index.ts";
28
+ import { encodeRef } from "../../plug-api/lib/ref.ts";
29
+ import { resolveASTReference } from "../space_lua.ts";
30
+
31
+ async function inspectValue(
32
+ value: unknown,
33
+ path: string[],
34
+ ): Promise<Omit<LuaValueInspection, "properties">> {
35
+ const type = value instanceof LuaEnv ? "table" : await luaTypeOf(value);
36
+ if (!isILuaFunction(value)) {
37
+ return { type };
38
+ }
39
+ const functionInfo: LuaFunctionInfo = {
40
+ ...(value.info ?? { kind: "builtin" }),
41
+ name: value.info?.name ?? path.join("."),
42
+ };
43
+ const definitionRef =
44
+ functionInfo.kind === "lua" && functionInfo.source
45
+ ? resolveASTReference(functionInfo.source as ASTCtx)
46
+ : null;
47
+ return {
48
+ type,
49
+ functionInfo,
50
+ definition: definitionRef ? encodeRef(definitionRef) : undefined,
51
+ };
52
+ }
53
+
54
+ async function inspectLuaPath(
55
+ env: LuaEnv,
56
+ path: string[],
57
+ ): Promise<LuaValueInspection | null> {
58
+ const sf = LuaStackFrame.createWithGlobalEnv(env);
59
+ let value: unknown = env;
60
+ for (const key of path) {
61
+ value = await luaGet(value, key, null, sf);
62
+ if (value === null || value === undefined) {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ const properties: LuaPropertyInspection[] = [];
68
+ const keys =
69
+ value instanceof LuaEnv
70
+ ? value.keys()
71
+ : value instanceof LuaTable ||
72
+ Array.isArray(value) ||
73
+ (typeof value === "object" && value !== null)
74
+ ? luaKeys(value)
75
+ : [];
76
+ for (const key of [...new Set(keys)]
77
+ .filter((candidate): candidate is string => typeof candidate === "string")
78
+ .sort()) {
79
+ const child = await luaGet(value, key, null, sf);
80
+ if (child === null || child === undefined) continue;
81
+ properties.push({
82
+ key,
83
+ ...(await inspectValue(child, [...path, key])),
84
+ });
85
+ }
86
+
87
+ return {
88
+ ...(await inspectValue(value, path)),
89
+ properties,
90
+ };
91
+ }
92
+
93
+ export function luaSyscalls(
94
+ system: System<any>,
95
+ getEnvironment: () => LuaEnv,
96
+ ): SysCallMapping {
97
+ return {
98
+ "lua.parseBlock": {
99
+ callback: (_ctx, code: string): LuaBlock => parseBlock(code),
100
+ description:
101
+ "Parses a Space Lua chunk and returns its AST. Blocks retain comments in source order with their exact text, kind, and source range.",
102
+ parameters: [
103
+ { name: "code", type: "string", description: "Lua code to parse." },
104
+ ],
105
+ returns: [{ type: "table", description: "Parsed Lua block AST." }],
106
+ examples: [
107
+ {
108
+ code: 'local ast = lua.parseBlock("print(\\"Hello\\")")',
109
+ },
110
+ ],
111
+ see: "API/lua",
112
+ },
113
+ // Deprecated alias for `lua.parseBlock`, kept for backwards compatibility.
114
+ "lua.parse": {
115
+ callback: (_ctx, code: string): LuaBlock => parseBlock(code),
116
+ description: "Deprecated alias for lua.parseBlock.",
117
+ parameters: [
118
+ { name: "code", type: "string", description: "Lua code to parse." },
119
+ ],
120
+ returns: [{ type: "table", description: "Parsed Lua block AST." }],
121
+ deprecated: "Use lua.parseBlock instead.",
122
+ see: "API/lua",
123
+ },
124
+ "lua.parseExpression": {
125
+ callback: (_ctx, expression: string): LuaExpression =>
126
+ parseExpressionString(expression),
127
+ description: "Parses a Space Lua expression and returns its AST.",
128
+ parameters: [
129
+ {
130
+ name: "expression",
131
+ type: "string",
132
+ description: "Lua expression to parse.",
133
+ },
134
+ ],
135
+ returns: [{ type: "table", description: "Parsed expression AST." }],
136
+ examples: [
137
+ {
138
+ code: 'local expression = lua.parseExpression("1 + 2 * 3")',
139
+ },
140
+ ],
141
+ see: "API/lua",
142
+ },
143
+ "lua.prettyPrintBlock": {
144
+ callback: (_ctx, block: LuaBlock, opts?: PrintOptions): string =>
145
+ prettyPrintBlock(block, opts),
146
+ description:
147
+ "Pretty-prints a parsed Space Lua block. Comments are preserved while their placement and indentation are normalized.",
148
+ parameters: [
149
+ { name: "block", type: "table", description: "Parsed block AST." },
150
+ {
151
+ name: "options",
152
+ type: "table",
153
+ description:
154
+ "Formatting options: `indentWidth`, `quote`, and `trailingComma`.",
155
+ optional: true,
156
+ },
157
+ ],
158
+ returns: [{ type: "string", description: "Formatted Lua source." }],
159
+ examples: [
160
+ {
161
+ code: 'local formatted = lua.prettyPrintBlock(lua.parseBlock("if a then return 1 end"))',
162
+ },
163
+ ],
164
+ see: "API/lua",
165
+ },
166
+ "lua.prettyPrintExpression": {
167
+ callback: (
168
+ _ctx,
169
+ expression: LuaExpression,
170
+ opts?: PrintOptions,
171
+ ): string => prettyPrintExpression(expression, opts),
172
+ description: "Pretty-prints a parsed Space Lua expression.",
173
+ parameters: [
174
+ {
175
+ name: "expression",
176
+ type: "table",
177
+ description: "Parsed expression AST.",
178
+ },
179
+ {
180
+ name: "options",
181
+ type: "table",
182
+ description:
183
+ "Formatting options: `indentWidth`, `quote`, and `trailingComma`.",
184
+ optional: true,
185
+ },
186
+ ],
187
+ returns: [{ type: "string", description: "Formatted Lua source." }],
188
+ examples: [
189
+ {
190
+ code: 'local formatted = lua.prettyPrintExpression(lua.parseExpression("{a=1,b=2}"))',
191
+ },
192
+ ],
193
+ see: "API/lua",
194
+ },
195
+ /**
196
+ * Evaluates a Lua expression and returns the result as a JavaScript value
197
+ * @param _ctx
198
+ * @param expression
199
+ * @returns
200
+ */
201
+ "lua.evalExpression": {
202
+ callback: async (_ctx, expression: string) => {
203
+ try {
204
+ const ast = parseExpressionString(expression);
205
+ const globalEnv = getEnvironment();
206
+ const env = await buildThreadLocalEnv(system, globalEnv);
207
+ const sf = new LuaStackFrame(env, null);
208
+ const luaResult = await evalExpression(ast, globalEnv, sf);
209
+ const jsResult = luaValueToJS(luaResult, sf);
210
+ if (isSendable(jsResult)) {
211
+ return jsResult;
212
+ } else {
213
+ // This may evaluate to e.g. a function, which is not sendable, in this case we'll console.warn and return a stringified version of the result
214
+ console.warn(
215
+ "Lua eval result is not sendable, returning stringified version",
216
+ jsResult,
217
+ );
218
+ return luaToString(luaResult);
219
+ }
220
+ } catch (e: any) {
221
+ console.error("Lua eval error: ", e.message, e.sf?.astCtx);
222
+ throw e;
223
+ }
224
+ },
225
+ description: "Evaluates a Space Lua expression.",
226
+ parameters: [
227
+ {
228
+ name: "expression",
229
+ type: "string",
230
+ description: "Lua expression to evaluate.",
231
+ },
232
+ ],
233
+ returns: [{ description: "Evaluated result." }],
234
+ examples: [
235
+ {
236
+ code: 'local result = lua.evalExpression("1 + 2 * 3")\nprint(result)',
237
+ },
238
+ ],
239
+ see: "API/lua",
240
+ },
241
+ "lua.inspect": {
242
+ callback: (_ctx, path: string[] = []) =>
243
+ inspectLuaPath(getEnvironment(), path),
244
+ description:
245
+ "Inspects a value in the live Space Lua environment and returns serializable type, function, definition, and property metadata.",
246
+ parameters: [
247
+ {
248
+ name: "path",
249
+ type: "table",
250
+ description:
251
+ "Sequence of property names from the global environment; omit to inspect globals.",
252
+ optional: true,
253
+ },
254
+ ],
255
+ returns: [
256
+ {
257
+ type: "table|nil",
258
+ description:
259
+ "Inspection metadata, or nil when the requested path does not exist.",
260
+ },
261
+ ],
262
+ examples: [
263
+ {
264
+ code: 'local info = lua.inspect({"editor", "getText"})',
265
+ },
266
+ ],
267
+ see: "API/lua",
268
+ },
269
+ };
270
+ }
@@ -142,7 +142,6 @@ function parseInt(s: string): { ok: boolean; value: number } {
142
142
  let acc = 0;
143
143
  let any = false;
144
144
 
145
- // hex?
146
145
  if (s.charCodeAt(i) === 48 && i + 1 < n) {
147
146
  // '0'
148
147
  const x = s.charCodeAt(i + 1);
@@ -179,7 +178,6 @@ function parseInt(s: string): { ok: boolean; value: number } {
179
178
  }
180
179
  }
181
180
 
182
- // decimal integer
183
181
  while (i < n) {
184
182
  const c = s.charCodeAt(i);
185
183
  if (c < 48 || c > 57) {
@@ -221,7 +219,6 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
221
219
  let fracAny = false;
222
220
  let val = 0;
223
221
 
224
- // integer part
225
222
  while (i < n) {
226
223
  const c = s.charCodeAt(i);
227
224
  if (c < 48 || c > 57) {
@@ -233,7 +230,6 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
233
230
  i++;
234
231
  }
235
232
 
236
- // fractional part
237
233
  if (i < n) {
238
234
  if (s.charCodeAt(i) === 46) {
239
235
  // '.'
@@ -253,7 +249,6 @@ function parseDecFloat(s: string): { ok: boolean; value: number } {
253
249
  }
254
250
  }
255
251
 
256
- // exponent
257
252
  let exp = 0;
258
253
  let hasExp = false;
259
254
  if (i < n) {
@@ -347,7 +342,6 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
347
342
  let anyHex = false;
348
343
  let sawDot = false;
349
344
 
350
- // integer hex digits
351
345
  while (i < n) {
352
346
  const c = s.charCodeAt(i);
353
347
  let d = -1;
@@ -371,7 +365,6 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
371
365
  i++;
372
366
  }
373
367
 
374
- // optional fractional part
375
368
  if (i < n) {
376
369
  if (s.charCodeAt(i) === 46) {
377
370
  // '.'
@@ -444,7 +437,6 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
444
437
  return { ok: false, value: 0 };
445
438
  }
446
439
  } else {
447
- // no exponent marker
448
440
  if (!sawDot) {
449
441
  // without dot we must reject so parseInt can claim it as integer
450
442
  return { ok: false, value: 0 };
@@ -456,7 +448,6 @@ function parseHexFloat(s: string): { ok: boolean; value: number } {
456
448
  exp = 0; // implicit p0
457
449
  }
458
450
  } else {
459
- // end of string
460
451
  if (!sawDot) {
461
452
  return { ok: false, value: 0 };
462
453
  }
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // version.ts
7
- var version = "2.9.0";
7
+ var version = "2.11.0";
8
8
 
9
9
  // client/plugos/plug_compile.ts
10
10
  import * as path from "node:path";
@@ -1371,8 +1371,7 @@ ${Object.entries(manifest.functions).map(([funcName, def]) => {
1371
1371
  }
1372
1372
  let [filePath, jsFunctionName] = def.path.split(":");
1373
1373
  filePath = path.join(rootPath, filePath);
1374
- return `import {${jsFunctionName} as ${funcName}} from "${// Replacing \ with / for Windows
1375
- path.resolve(filePath).replaceAll("\\", "\\\\")}";
1374
+ return `import {${jsFunctionName} as ${funcName}} from "${path.resolve(filePath).replaceAll("\\", "\\\\")}";
1376
1375
  `;
1377
1376
  }).join("")}
1378
1377
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverbulletmd/silverbullet",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "type": "module",
5
5
  "description": "A self-hosted, web-based note taking app",
6
6
  "publishConfig": {
@@ -15,20 +15,26 @@
15
15
  "./lib/tree": "./plug-api/lib/tree.ts",
16
16
  "./lib/ref": "./plug-api/lib/ref.ts",
17
17
  "./lib/resolve": "./plug-api/lib/resolve.ts",
18
+ "./lib/resolve_path": "./plug-api/lib/resolve_path.ts",
19
+ "./lib/link_write": "./plug-api/lib/link_write.ts",
18
20
  "./lib/dates": "./plug-api/lib/dates.ts",
21
+ "./lib/fuzzy": "./plug-api/lib/fuzzy.ts",
19
22
  "./lib/async": "./plug-api/lib/async.ts",
20
23
  "./lib/crypto": "./plug-api/lib/crypto.ts",
21
24
  "./lib/limited_map": "./plug-api/lib/limited_map.ts",
22
25
  "./lib/tags": "./plug-api/lib/tags.ts",
26
+ "./lib/collation": "./plug-api/lib/collation.ts",
23
27
  "./lib/transclusion": "./plug-api/lib/transclusion.ts",
24
28
  "./lib/native_fetch": "./plug-api/lib/native_fetch.ts",
25
29
  "./lib/query_expression": "./plug-api/lib/query_expression.ts",
30
+ "./lib/panel_styles": "./plug-api/lib/panel_styles.ts",
26
31
  "./type/client": "./plug-api/types/client.ts",
27
32
  "./type/config": "./plug-api/types/config.ts",
28
33
  "./type/manifest": "./plug-api/types/manifest.ts",
29
34
  "./type/namespace": "./plug-api/types/namespace.ts",
30
35
  "./type/datastore": "./plug-api/types/datastore.ts",
31
36
  "./type/event": "./plug-api/types/event.ts",
37
+ "./type/revisions": "./plug-api/types/revisions.ts",
32
38
  "./type/index": "./plug-api/types/index.ts"
33
39
  },
34
40
  "bin": {
@@ -41,6 +47,7 @@
41
47
  "client/plugos/**/*.ts",
42
48
  "!client/plugos/**/*.test.ts",
43
49
  "client/asset_bundle/bundle.ts",
50
+ "client/config.ts",
44
51
  "client/data/kv_primitives.ts",
45
52
  "client/data/datastore.ts",
46
53
  "client/space_lua/**/*.ts",
@@ -48,6 +55,7 @@
48
55
  "!client/space_lua/**/*.bench.ts",
49
56
  "client/markdown_parser/constants.ts",
50
57
  "plugs/builtin_plugs.ts",
58
+ "plugs/index/types.ts",
51
59
  "dist/plug-compile.js"
52
60
  ],
53
61
  "scripts": {
@@ -55,16 +63,17 @@
55
63
  "build:plugs": "tsx build/build_plugs.ts",
56
64
  "build:client": "tsx build/build_client.ts",
57
65
  "build:plug-compile": "tsx build/build_plug_compile.ts",
66
+ "prebuild:client": "node build/patch_codemirror_view.mjs",
58
67
  "prepublishOnly": "npm run build:plug-compile",
59
68
  "test": "vitest run",
60
- "check": "tsc --noEmit",
69
+ "check": "tsc --noEmit && tsc -p tsconfig.e2e.json",
61
70
  "lint": "biome lint .",
62
71
  "fmt": "biome format --write .",
63
72
  "fmt:check": "biome format .",
64
73
  "bench": "vitest bench",
65
74
  "index-stats": "tsx plugs/index/index_stats.ts",
66
- "test:e2e": "npx playwright test",
67
- "test:e2e:headed": "npx playwright test --headed"
75
+ "test:e2e": "npx playwright test --project=chromium",
76
+ "test:e2e:headed": "npx playwright test --project=chromium --headed"
68
77
  },
69
78
  "peerDependencies": {
70
79
  "preact": "^10.25.0"
@@ -92,8 +101,10 @@
92
101
  "@lezer/lr": "1.4.8",
93
102
  "@lezer/markdown": "1.6.3",
94
103
  "@msgpack/msgpack": "3.1.3",
104
+ "@noble/hashes": "2.0.1",
95
105
  "@replit/codemirror-lang-nix": "6.0.1",
96
106
  "@replit/codemirror-vim": "6.3.0",
107
+ "@types/js-yaml": "^4.0.9",
97
108
  "commander": "^14.0.3",
98
109
  "crelt": "1.0.6",
99
110
  "d3-force": "^3.0.0",
@@ -118,7 +129,6 @@
118
129
  "@preact/preset-vite": "^2.10.3",
119
130
  "@types/d3-force": "^3.0.10",
120
131
  "@types/gitignore-parser": "^0.0.3",
121
- "@types/js-yaml": "^4.0.9",
122
132
  "@types/node": "^22.0.0",
123
133
  "@types/turndown": "^5.0.6",
124
134
  "fake-indexeddb": "6.0.1",
@@ -11,7 +11,6 @@ export function throttle(
11
11
  }, limit);
12
12
  }
13
13
  };
14
- // Immediately execute any pending call and cancel the timer
15
14
  throttled.flush = () => {
16
15
  if (timer) {
17
16
  clearTimeout(timer);
@@ -91,7 +90,7 @@ export class PromiseQueue {
91
90
  reject(error);
92
91
  }
93
92
 
94
- void this.process(); // Continue processing the next promise in the queue
93
+ void this.process();
95
94
  }
96
95
  }
97
96
 
@@ -108,17 +107,13 @@ export async function batchRequests<I, O>(
108
107
  batchSize: number,
109
108
  ): Promise<O[]> {
110
109
  const results: O[] = [];
111
- // Split values into batches of batchSize
112
110
  const batches: I[][] = [];
113
111
  for (let i = 0; i < values.length; i += batchSize) {
114
112
  batches.push(values.slice(i, i + batchSize));
115
113
  }
116
- // Run fn on them in parallel
117
114
  const batchResults = await Promise.all(batches.map(fn));
118
- // Flatten the results
119
115
  for (const batchResult of batchResults) {
120
116
  if (Array.isArray(batchResult)) {
121
- // If fn returns an array, collect them
122
117
  results.push(...batchResult);
123
118
  }
124
119
  }
@@ -0,0 +1,22 @@
1
+ import type { QueryCollationConfig } from "../types/config.ts";
2
+
3
+ /**
4
+ * Compares two values the same way the query engine's `order by` does:
5
+ * codepoint order by default, or `Intl.Collator` order when the space's
6
+ * `queryCollation` config turns it on and both sides are strings.
7
+ */
8
+ export function compareCollated(
9
+ // deno-lint-ignore no-explicit-any
10
+ a: any,
11
+ // deno-lint-ignore no-explicit-any
12
+ b: any,
13
+ collation: QueryCollationConfig | undefined,
14
+ collator: Intl.Collator,
15
+ ): number {
16
+ if (collation?.enabled && typeof a === "string" && typeof b === "string") {
17
+ return collator.compare(a, b);
18
+ }
19
+ if (a < b) return -1;
20
+ if (a > b) return 1;
21
+ return 0;
22
+ }
@@ -1,3 +1,5 @@
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
+
1
3
  export function base64Decode(s: string): Uint8Array {
2
4
  const binString = atob(s);
3
5
  const len = binString.length;
@@ -32,31 +34,33 @@ export function base64DecodeDataUrl(dataUrl: string): Uint8Array {
32
34
  return base64Decode(b64Encoded);
33
35
  }
34
36
 
35
- /**
36
- * Perform sha256 hash using the browser's crypto APIs
37
- * Note: this will only work over HTTPS
38
- * @param message
39
- */
40
37
  export async function hashSHA256(
41
38
  message: string | Uint8Array,
42
39
  ): Promise<string> {
43
- // Transform the string into an ArrayBuffer
44
40
  const encoder = new TextEncoder();
45
41
  const data: Uint8Array =
46
42
  typeof message === "string" ? encoder.encode(message) : message;
47
43
 
48
- // Generate the hash
49
- const hashBuffer = await globalThis.crypto.subtle.digest(
50
- "SHA-256",
51
- data as BufferSource,
52
- );
44
+ const hashBuffer = globalThis.crypto.subtle
45
+ ? await globalThis.crypto.subtle.digest("SHA-256", data as BufferSource)
46
+ : sha256(data);
53
47
 
54
- // Transform the hash into a hex string
55
48
  return Array.from(new Uint8Array(hashBuffer))
56
49
  .map((b) => b.toString(16).padStart(2, "0"))
57
50
  .join("");
58
51
  }
59
52
 
53
+ export function randomUUID(): string {
54
+ if (globalThis.crypto.randomUUID) return globalThis.crypto.randomUUID();
55
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
56
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
57
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
58
+ const hex = Array.from(bytes, (byte) =>
59
+ byte.toString(16).padStart(2, "0"),
60
+ ).join("");
61
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
62
+ }
63
+
60
64
  /**
61
65
  * To avoid database clashes based on space folder path name, base URLs and encryption keys we derive
62
66
  * a database name from a hash of all these combined together
@@ -117,7 +121,6 @@ export async function encryptAesGcm(
117
121
  );
118
122
  const encrypted = new Uint8Array(encryptedBuffer);
119
123
 
120
- // Prepend IV to ciphertext
121
124
  const result = new Uint8Array(iv.length + encrypted.length);
122
125
  result.set(iv, 0);
123
126
  result.set(encrypted, iv.length);
@@ -143,10 +146,8 @@ export async function deriveCTRKeyFromPassword(
143
146
  password: string,
144
147
  salt: Uint8Array,
145
148
  ): Promise<CryptoKey> {
146
- // Encode password to ArrayBuffer
147
149
  const passwordBytes = new TextEncoder().encode(password);
148
150
 
149
- // Import password as a CryptoKey
150
151
  const baseKey = await globalThis.crypto.subtle.importKey(
151
152
  "raw",
152
153
  passwordBytes,
@@ -2,6 +2,39 @@ export function niceDate(d: Date): string {
2
2
  return localDateString(d).split("T")[0];
3
3
  }
4
4
 
5
+ /** Largest-unit-first, so the cascade below divides down to the right one. */
6
+ const RELATIVE_DIVISIONS: {
7
+ amount: number;
8
+ unit: Intl.RelativeTimeFormatUnit;
9
+ }[] = [
10
+ { amount: 60, unit: "second" },
11
+ { amount: 60, unit: "minute" },
12
+ { amount: 24, unit: "hour" },
13
+ { amount: 7, unit: "day" },
14
+ { amount: 4.34524, unit: "week" },
15
+ { amount: 12, unit: "month" },
16
+ ];
17
+
18
+ /**
19
+ * "10 minutes ago", "yesterday", "3 months ago". `numeric: "auto"` is what
20
+ * turns the -1 cases into words rather than "1 day ago".
21
+ */
22
+ export function relativeTime(
23
+ timestamp: number,
24
+ locale?: string,
25
+ now: number = Date.now(),
26
+ ): string {
27
+ const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
28
+ let duration = (timestamp - now) / 1000;
29
+ for (const { amount, unit } of RELATIVE_DIVISIONS) {
30
+ if (Math.abs(duration) < amount) {
31
+ return formatter.format(Math.round(duration), unit);
32
+ }
33
+ duration /= amount;
34
+ }
35
+ return formatter.format(Math.round(duration), "year");
36
+ }
37
+
5
38
  export function localDateString(d: Date): string {
6
39
  return (
7
40
  d.getFullYear() +