@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.160

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 (50) hide show
  1. package/AGENTS.md +9 -6
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +177 -20
  5. package/dist/application/add-note.js +13 -44
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +64 -3
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +241 -51
  11. package/dist/application/frontend/client-html.js +50 -27
  12. package/dist/application/frontend/client-js.js +1369 -605
  13. package/dist/application/frontend/client-render-worker-js.js +622 -0
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-contexts.js +33 -0
  16. package/dist/application/get-graph-layout.js +62 -8
  17. package/dist/application/get-graph-stream-chunk.js +326 -0
  18. package/dist/application/get-graph-view.js +246 -0
  19. package/dist/application/graph-view-state.js +66 -0
  20. package/dist/application/import-legacy-sqlite.js +266 -0
  21. package/dist/application/index-vault.js +262 -23
  22. package/dist/application/migrate-context-links.js +79 -0
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +63 -3
  25. package/dist/application/server/routes.js +247 -7
  26. package/dist/application/start-server.js +75 -4
  27. package/dist/application/watch-vault.js +23 -2
  28. package/dist/cli/commands/agent-commands.js +7 -0
  29. package/dist/cli/commands/write-commands.js +924 -14
  30. package/dist/cli/runtime.js +10 -2
  31. package/dist/domain/context.js +54 -11
  32. package/dist/domain/graph-contexts.js +180 -0
  33. package/dist/domain/graph-layout.js +389 -18
  34. package/dist/domain/markdown.js +53 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +121 -4
  37. package/dist/infrastructure/file-index.js +76 -6
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +286 -15
  42. package/dist/infrastructure/vault-migration-state.js +69 -0
  43. package/dist/infrastructure/volatile-memory.js +100 -0
  44. package/dist/mcp/runtime.js +20 -0
  45. package/dist/mcp/server.js +39 -11
  46. package/dist/mcp/tools.js +183 -7
  47. package/docs/AGENT_USAGE.md +96 -5
  48. package/docs/ARCHITECTURE.md +8 -0
  49. package/docs/QUICKSTART.md +7 -0
  50. package/package.json +7 -2
@@ -0,0 +1,622 @@
1
+ export const createClientRenderWorkerJs = () => `let canvas = null
2
+ let gl = null
3
+ let viewportWidth = 320
4
+ let viewportHeight = 320
5
+ let devicePixelRatio = 1
6
+ const camera = { x: 0, y: 0, scale: 1 }
7
+ const state = {
8
+ nodeCount: 0,
9
+ edgeCount: 0,
10
+ ids: [],
11
+ titles: [],
12
+ kinds: [],
13
+ x: new Float32Array(0),
14
+ y: new Float32Array(0),
15
+ relevance: new Float32Array(0),
16
+ radius: new Float32Array(0),
17
+ visible: new Uint8Array(0),
18
+ highlighted: new Uint8Array(0),
19
+ focused: new Uint8Array(0),
20
+ selected: new Uint8Array(0),
21
+ edgeSource: new Uint32Array(0),
22
+ edgeTarget: new Uint32Array(0),
23
+ edgeWeight: new Float32Array(0)
24
+ }
25
+ const nodeIndexById = new Map()
26
+ const highlightedIds = new Set()
27
+ const focusedIds = new Set()
28
+ let selectedNodeId = null
29
+ let dirty = true
30
+ let renderScheduled = false
31
+ let hoverX = null
32
+ let hoverY = null
33
+ let lastFrameAt = 0
34
+ let lastVisibleEdges = 0
35
+ let edgePositionsBuffer = new Float32Array(0)
36
+ let pointPositionsBuffer = new Float32Array(0)
37
+ let pointSizesBuffer = new Float32Array(0)
38
+
39
+ const defaultTheme = {
40
+ node: [0.68, 0.72, 0.78, 1],
41
+ nodeCluster: [0.42, 0.76, 0.92, 1],
42
+ nodeHighlight: [0.95, 0.76, 0.22, 1],
43
+ nodeSelected: [0.99, 0.99, 1, 1],
44
+ edge: [0.58, 0.64, 0.74, 0.24],
45
+ edgeHeavy: [0.78, 0.84, 0.92, 0.44],
46
+ clear: [0.05, 0.06, 0.08, 1]
47
+ }
48
+
49
+ const theme = { ...defaultTheme }
50
+
51
+ const createShader = (type, source) => {
52
+ const shader = gl.createShader(type)
53
+ if (!shader) return null
54
+ gl.shaderSource(shader, source)
55
+ gl.compileShader(shader)
56
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
57
+ gl.deleteShader(shader)
58
+ return null
59
+ }
60
+ return shader
61
+ }
62
+
63
+ const createProgram = (vertexSource, fragmentSource) => {
64
+ const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
65
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
66
+ if (!vertexShader || !fragmentShader) return null
67
+ const program = gl.createProgram()
68
+ if (!program) return null
69
+ gl.attachShader(program, vertexShader)
70
+ gl.attachShader(program, fragmentShader)
71
+ gl.linkProgram(program)
72
+ gl.deleteShader(vertexShader)
73
+ gl.deleteShader(fragmentShader)
74
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
75
+ gl.deleteProgram(program)
76
+ return null
77
+ }
78
+ return program
79
+ }
80
+
81
+ let lineProgram = null
82
+ let pointProgram = null
83
+ let lineBuffer = null
84
+ let pointPositionBuffer = null
85
+ let pointSizeBuffer = null
86
+ let linePositionLocation = -1
87
+ let lineResolutionLocation = null
88
+ let lineColorLocation = null
89
+ let pointPositionLocation = -1
90
+ let pointSizeLocation = -1
91
+ let pointResolutionLocation = null
92
+ let pointColorLocation = null
93
+
94
+ const initWebGl = () => {
95
+ if (!canvas) {
96
+ return false
97
+ }
98
+
99
+ gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
100
+ canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
101
+
102
+ if (!gl) {
103
+ return false
104
+ }
105
+
106
+ lineProgram = createProgram(
107
+ 'attribute vec2 a_position; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); }',
108
+ 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
109
+ )
110
+
111
+ pointProgram = createProgram(
112
+ 'attribute vec2 a_position; attribute float a_size; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); gl_PointSize = a_size; }',
113
+ 'precision mediump float; uniform vec4 u_color; void main() { vec2 center = gl_PointCoord - vec2(0.5); float distanceFromCenter = length(center); if (distanceFromCenter > 0.5) discard; float alpha = smoothstep(0.5, 0.38, distanceFromCenter); gl_FragColor = vec4(u_color.rgb, u_color.a * alpha); }'
114
+ )
115
+
116
+ if (!lineProgram || !pointProgram) {
117
+ return false
118
+ }
119
+
120
+ lineBuffer = gl.createBuffer()
121
+ pointPositionBuffer = gl.createBuffer()
122
+ pointSizeBuffer = gl.createBuffer()
123
+ if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
124
+ return false
125
+ }
126
+
127
+ linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
128
+ lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
129
+ lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
130
+ pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
131
+ pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
132
+ pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
133
+ pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
134
+
135
+ gl.enable(gl.BLEND)
136
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
137
+ gl.disable(gl.DEPTH_TEST)
138
+
139
+ return true
140
+ }
141
+
142
+ const ensureFloat32Capacity = (buffer, neededLength) => {
143
+ if (buffer.length >= neededLength) {
144
+ return buffer
145
+ }
146
+ const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
147
+ return new Float32Array(next)
148
+ }
149
+
150
+ const resizeCanvas = (width, height, ratio) => {
151
+ viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
152
+ viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
153
+ devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
154
+
155
+ if (!canvas) return
156
+
157
+ canvas.width = Math.floor(viewportWidth * devicePixelRatio)
158
+ canvas.height = Math.floor(viewportHeight * devicePixelRatio)
159
+ if (gl) {
160
+ gl.viewport(0, 0, canvas.width, canvas.height)
161
+ }
162
+ dirty = true
163
+ requestRender()
164
+ }
165
+
166
+ const toScreenPoint = (x, y) => {
167
+ const sx = (x * camera.scale + camera.x) * devicePixelRatio
168
+ const sy = (y * camera.scale + camera.y) * devicePixelRatio
169
+ return [sx, sy]
170
+ }
171
+
172
+ const ensureNodeCapacity = (count) => {
173
+ if (state.x.length >= count) {
174
+ return
175
+ }
176
+
177
+ const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
178
+ state.x = new Float32Array(nextCapacity)
179
+ state.y = new Float32Array(nextCapacity)
180
+ state.relevance = new Float32Array(nextCapacity)
181
+ state.radius = new Float32Array(nextCapacity)
182
+ state.visible = new Uint8Array(nextCapacity)
183
+ state.highlighted = new Uint8Array(nextCapacity)
184
+ state.focused = new Uint8Array(nextCapacity)
185
+ state.selected = new Uint8Array(nextCapacity)
186
+ }
187
+
188
+ const ensureEdgeCapacity = (count) => {
189
+ if (state.edgeSource.length >= count) {
190
+ return
191
+ }
192
+
193
+ const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
194
+ state.edgeSource = new Uint32Array(nextCapacity)
195
+ state.edgeTarget = new Uint32Array(nextCapacity)
196
+ state.edgeWeight = new Float32Array(nextCapacity)
197
+ }
198
+
199
+ const nodeRadius = (relevance, kind) => {
200
+ const base = kind === 'cluster' ? 7.8 : 4.6
201
+ const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
202
+ return base + modifier
203
+ }
204
+
205
+ const loadChunk = (chunk) => {
206
+ const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
207
+ const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
208
+
209
+ ensureNodeCapacity(nodes.length)
210
+ nodeIndexById.clear()
211
+ state.ids = new Array(nodes.length)
212
+ state.titles = new Array(nodes.length)
213
+ state.kinds = new Array(nodes.length)
214
+
215
+ for (let index = 0; index < nodes.length; index += 1) {
216
+ const row = nodes[index]
217
+ const id = typeof row?.[0] === 'string' ? row[0] : ''
218
+ if (!id) {
219
+ continue
220
+ }
221
+ const title = typeof row?.[1] === 'string' ? row[1] : id
222
+ const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
223
+ const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
224
+ const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
225
+ const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
226
+
227
+ state.ids[index] = id
228
+ state.titles[index] = title
229
+ state.kinds[index] = kind
230
+ state.x[index] = x
231
+ state.y[index] = y
232
+ state.relevance[index] = relevance
233
+ state.radius[index] = nodeRadius(relevance, kind)
234
+ state.visible[index] = 0
235
+ state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
236
+ state.focused[index] = focusedIds.has(id) ? 1 : 0
237
+ state.selected[index] = selectedNodeId === id ? 1 : 0
238
+ nodeIndexById.set(id, index)
239
+ }
240
+
241
+ state.nodeCount = nodes.length
242
+
243
+ ensureEdgeCapacity(edges.length)
244
+ let edgeCount = 0
245
+ for (let index = 0; index < edges.length; index += 1) {
246
+ const row = edges[index]
247
+ const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
248
+ const targetId = typeof row?.[1] === 'string' ? row[1] : ''
249
+ const source = nodeIndexById.get(sourceId)
250
+ const target = nodeIndexById.get(targetId)
251
+ if (source === undefined || target === undefined || source === target) {
252
+ continue
253
+ }
254
+ state.edgeSource[edgeCount] = source
255
+ state.edgeTarget[edgeCount] = target
256
+ state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
257
+ edgeCount += 1
258
+ }
259
+
260
+ state.edgeCount = edgeCount
261
+ dirty = true
262
+ }
263
+
264
+ const cullVisibleNodes = () => {
265
+ const minX = -280
266
+ const minY = -280
267
+ const maxX = viewportWidth + 280
268
+ const maxY = viewportHeight + 280
269
+
270
+ for (let index = 0; index < state.nodeCount; index += 1) {
271
+ const radius = state.radius[index] * camera.scale
272
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
273
+ const screenX = sx / devicePixelRatio
274
+ const screenY = sy / devicePixelRatio
275
+ state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
276
+ }
277
+ }
278
+
279
+ const drawEdges = () => {
280
+ if (!gl || state.edgeCount === 0) return
281
+
282
+ edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
283
+ let cursor = 0
284
+ let visibleEdges = 0
285
+ for (let index = 0; index < state.edgeCount; index += 1) {
286
+ const source = state.edgeSource[index]
287
+ const target = state.edgeTarget[index]
288
+ if (state.visible[source] === 0 && state.visible[target] === 0) {
289
+ continue
290
+ }
291
+ const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
292
+ const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
293
+ edgePositionsBuffer[cursor] = sx
294
+ edgePositionsBuffer[cursor + 1] = sy
295
+ edgePositionsBuffer[cursor + 2] = tx
296
+ edgePositionsBuffer[cursor + 3] = ty
297
+ cursor += 4
298
+ visibleEdges += 1
299
+ }
300
+
301
+ lastVisibleEdges = visibleEdges
302
+ if (cursor === 0) return
303
+
304
+ gl.useProgram(lineProgram)
305
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
306
+ gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
307
+ gl.enableVertexAttribArray(linePositionLocation)
308
+ gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
309
+ gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
310
+ gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
311
+ gl.lineWidth(1)
312
+ gl.drawArrays(gl.LINES, 0, cursor / 2)
313
+ }
314
+
315
+ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
316
+ if (!gl || state.nodeCount === 0) return
317
+
318
+ pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
319
+ pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
320
+ let positionCursor = 0
321
+ let sizeCursor = 0
322
+ for (let index = 0; index < state.nodeCount; index += 1) {
323
+ if (!predicate(index)) continue
324
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
325
+ pointPositionsBuffer[positionCursor] = sx
326
+ pointPositionsBuffer[positionCursor + 1] = sy
327
+ pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
328
+ positionCursor += 2
329
+ sizeCursor += 1
330
+ }
331
+
332
+ if (positionCursor === 0) return
333
+
334
+ gl.useProgram(pointProgram)
335
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
336
+ gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
337
+ gl.enableVertexAttribArray(pointPositionLocation)
338
+ gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
339
+
340
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
341
+ gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
342
+ gl.enableVertexAttribArray(pointSizeLocation)
343
+ gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
344
+
345
+ gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
346
+ gl.uniform4fv(pointColorLocation, color)
347
+ gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
348
+ }
349
+
350
+ const clear = () => {
351
+ if (!gl || !canvas) return
352
+ gl.viewport(0, 0, canvas.width, canvas.height)
353
+ gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
354
+ gl.clear(gl.COLOR_BUFFER_BIT)
355
+ }
356
+
357
+ const renderFrame = (now) => {
358
+ renderScheduled = false
359
+ if (!dirty) {
360
+ return
361
+ }
362
+
363
+ const delta = now - lastFrameAt
364
+ const minInterval = state.nodeCount > 20000 ? 22 : 16
365
+ if (delta < minInterval) {
366
+ requestRender()
367
+ return
368
+ }
369
+ lastFrameAt = now
370
+
371
+ if (!gl) {
372
+ return
373
+ }
374
+
375
+ cullVisibleNodes()
376
+ clear()
377
+ drawEdges()
378
+
379
+ drawNodeLayer(
380
+ (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
381
+ theme.node,
382
+ 1
383
+ )
384
+
385
+ drawNodeLayer(
386
+ (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
387
+ theme.nodeCluster,
388
+ 1.15
389
+ )
390
+
391
+ drawNodeLayer(
392
+ (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
393
+ theme.nodeHighlight,
394
+ 1.22
395
+ )
396
+
397
+ drawNodeLayer(
398
+ (index) => state.visible[index] === 1 && state.focused[index] === 1,
399
+ theme.nodeHighlight,
400
+ 1.12
401
+ )
402
+
403
+ drawNodeLayer(
404
+ (index) => state.visible[index] === 1 && state.selected[index] === 1,
405
+ theme.nodeSelected,
406
+ 1.32
407
+ )
408
+
409
+ if (dirty) {
410
+ postMessage({
411
+ type: 'frame-stats',
412
+ visibleNodes: (() => {
413
+ let count = 0
414
+ for (let index = 0; index < state.nodeCount; index += 1) {
415
+ if (state.visible[index] === 1) count += 1
416
+ }
417
+ return count
418
+ })(),
419
+ visibleEdges: lastVisibleEdges
420
+ })
421
+ dirty = false
422
+ }
423
+ }
424
+
425
+ const requestRender = () => {
426
+ if (renderScheduled) {
427
+ return
428
+ }
429
+ renderScheduled = true
430
+ const raf = self.requestAnimationFrame
431
+ if (typeof raf === 'function') {
432
+ raf.call(self, renderFrame)
433
+ return
434
+ }
435
+ setTimeout(() => renderFrame(performance.now()), 16)
436
+ }
437
+
438
+ const setCamera = (nextCamera) => {
439
+ if (!nextCamera || typeof nextCamera !== 'object') {
440
+ return
441
+ }
442
+ camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
443
+ camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
444
+ camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
445
+ dirty = true
446
+ requestRender()
447
+ }
448
+
449
+ const worldAtScreen = (screenX, screenY) => {
450
+ const x = (screenX - camera.x) / camera.scale
451
+ const y = (screenY - camera.y) / camera.scale
452
+ return [x, y]
453
+ }
454
+
455
+ const pickNode = (screenX, screenY) => {
456
+ const [worldX, worldY] = worldAtScreen(screenX, screenY)
457
+ let bestIndex = -1
458
+ let bestDistance = Infinity
459
+
460
+ for (let index = 0; index < state.nodeCount; index += 1) {
461
+ if (state.visible[index] === 0) continue
462
+ const dx = state.x[index] - worldX
463
+ const dy = state.y[index] - worldY
464
+ const distance = Math.hypot(dx, dy)
465
+ const maxDistance = state.radius[index] * 1.2
466
+ if (distance <= maxDistance && distance < bestDistance) {
467
+ bestDistance = distance
468
+ bestIndex = index
469
+ }
470
+ }
471
+
472
+ if (bestIndex < 0) {
473
+ return null
474
+ }
475
+
476
+ return {
477
+ id: state.ids[bestIndex],
478
+ title: state.titles[bestIndex],
479
+ kind: state.kinds[bestIndex],
480
+ x: state.x[bestIndex],
481
+ y: state.y[bestIndex]
482
+ }
483
+ }
484
+
485
+ const setHighlights = (ids) => {
486
+ highlightedIds.clear()
487
+ const list = Array.isArray(ids) ? ids : []
488
+ for (let index = 0; index < list.length; index += 1) {
489
+ const id = list[index]
490
+ if (typeof id === 'string' && id.length > 0) {
491
+ highlightedIds.add(id)
492
+ }
493
+ }
494
+
495
+ for (let index = 0; index < state.nodeCount; index += 1) {
496
+ state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
497
+ }
498
+ dirty = true
499
+ requestRender()
500
+ }
501
+
502
+ const setFocus = (ids) => {
503
+ focusedIds.clear()
504
+ const list = Array.isArray(ids) ? ids : []
505
+ for (let index = 0; index < list.length; index += 1) {
506
+ const id = list[index]
507
+ if (typeof id === 'string' && id.length > 0) {
508
+ focusedIds.add(id)
509
+ }
510
+ }
511
+
512
+ for (let index = 0; index < state.nodeCount; index += 1) {
513
+ state.focused[index] = focusedIds.has(state.ids[index]) ? 1 : 0
514
+ }
515
+ dirty = true
516
+ requestRender()
517
+ }
518
+
519
+ const setSelected = (id) => {
520
+ selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
521
+ for (let index = 0; index < state.nodeCount; index += 1) {
522
+ state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
523
+ }
524
+ dirty = true
525
+ requestRender()
526
+ }
527
+
528
+ const moveNode = (id, x, y) => {
529
+ if (typeof id !== 'string' || !Number.isFinite(x) || !Number.isFinite(y)) {
530
+ return
531
+ }
532
+
533
+ const index = nodeIndexById.get(id)
534
+ if (index === undefined) {
535
+ return
536
+ }
537
+
538
+ state.x[index] = x
539
+ state.y[index] = y
540
+ dirty = true
541
+ requestRender()
542
+ }
543
+
544
+ self.onmessage = (event) => {
545
+ const payload = event.data
546
+ if (!payload || typeof payload !== 'object') {
547
+ return
548
+ }
549
+
550
+ if (payload.type === 'init') {
551
+ canvas = payload.canvas
552
+ if (payload.theme && typeof payload.theme === 'object') {
553
+ Object.assign(theme, payload.theme)
554
+ }
555
+ const initialized = initWebGl()
556
+ if (!initialized) {
557
+ postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
558
+ return
559
+ }
560
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
561
+ setCamera(payload.camera)
562
+ requestRender()
563
+ postMessage({ type: 'ready' })
564
+ return
565
+ }
566
+
567
+ if (payload.type === 'resize') {
568
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
569
+ return
570
+ }
571
+
572
+ if (payload.type === 'camera') {
573
+ setCamera(payload.camera)
574
+ return
575
+ }
576
+
577
+ if (payload.type === 'chunk') {
578
+ loadChunk(payload.chunk)
579
+ requestRender()
580
+ return
581
+ }
582
+
583
+ if (payload.type === 'highlight') {
584
+ setHighlights(payload.ids)
585
+ return
586
+ }
587
+
588
+ if (payload.type === 'focus') {
589
+ setFocus(payload.ids)
590
+ return
591
+ }
592
+
593
+ if (payload.type === 'select') {
594
+ setSelected(payload.id)
595
+ return
596
+ }
597
+
598
+ if (payload.type === 'move-node') {
599
+ moveNode(payload.id, Number(payload.x), Number(payload.y))
600
+ return
601
+ }
602
+
603
+ if (payload.type === 'pick') {
604
+ const node = pickNode(
605
+ Number.isFinite(payload.x) ? Number(payload.x) : 0,
606
+ Number.isFinite(payload.y) ? Number(payload.y) : 0
607
+ )
608
+ postMessage({
609
+ type: 'pick-result',
610
+ requestId: payload.requestId,
611
+ node
612
+ })
613
+ return
614
+ }
615
+
616
+ if (payload.type === 'pointer') {
617
+ hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
618
+ hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
619
+ return
620
+ }
621
+ }
622
+ `;
@@ -0,0 +1,66 @@
1
+ export const createClientWorkerJs = () => `const normalize = value => String(value || '')
2
+ .normalize('NFKD')
3
+ .replace(/\\p{Diacritic}/gu, '')
4
+ .toLowerCase()
5
+
6
+ let nodeIndex = []
7
+
8
+ const toNodeIndex = nodes =>
9
+ (Array.isArray(nodes) ? nodes : [])
10
+ .map(node => {
11
+ const id = typeof node.id === 'string' ? node.id : ''
12
+ if (!id) {
13
+ return null
14
+ }
15
+ const title = normalize(node.title)
16
+ const path = normalize(node.path)
17
+ const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
18
+ return {
19
+ id,
20
+ text: [title, path, ...tags].join(' ')
21
+ }
22
+ })
23
+ .filter(Boolean)
24
+
25
+ const scoreText = (text, query) => {
26
+ if (!query) return 0
27
+ if (!text.includes(query)) return 0
28
+ if (text.startsWith(query)) return 4
29
+ return 1
30
+ }
31
+
32
+ const filterIds = (query, limit) => {
33
+ const normalizedQuery = normalize(query).trim()
34
+ if (!normalizedQuery) {
35
+ return []
36
+ }
37
+ const rows = []
38
+ for (let index = 0; index < nodeIndex.length; index += 1) {
39
+ const row = nodeIndex[index]
40
+ const score = scoreText(row.text, normalizedQuery)
41
+ if (score > 0) {
42
+ rows.push({ id: row.id, score })
43
+ }
44
+ }
45
+ rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
46
+ return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
47
+ }
48
+
49
+ self.onmessage = event => {
50
+ const payload = event.data
51
+ if (!payload || typeof payload !== 'object') {
52
+ return
53
+ }
54
+ if (payload.type === 'load-nodes') {
55
+ nodeIndex = toNodeIndex(payload.nodes)
56
+ return
57
+ }
58
+ if (payload.type === 'filter') {
59
+ const token = payload.token
60
+ const ids = filterIds(payload.query, payload.limit)
61
+ self.postMessage({ type: 'filter-result', token, ids })
62
+ }
63
+ }
64
+
65
+ self.postMessage({ type: 'ready' })
66
+ `;