@andespindola/brainlink 0.1.0-beta.103 → 0.1.0-beta.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -600,6 +600,7 @@ The graph UI shows:
600
600
  - double-click on canvas zooms in at cursor position
601
601
  - floating graph totals (notes, links, tags) below the Brainlink title
602
602
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
603
+ - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
603
604
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
604
605
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
605
606
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
@@ -42,6 +42,10 @@ const dragSettleRounds = 3
42
42
  const wheelZoomExponent = 0.0009
43
43
  const wheelZoomExponentCap = 0.035
44
44
  const wheelZoomModifierBoost = 1.08
45
+ const physicsDragFrameIntervalMs = 16
46
+ const physicsIdleFrameIntervalMs = 78
47
+ const physicsLargeGraphIdleFrameIntervalMs = 108
48
+ const physicsStepDeltaCapMs = 96
45
49
  const state = {
46
50
  graph: { nodes: [], edges: [] },
47
51
  nodes: [],
@@ -67,7 +71,10 @@ const state = {
67
71
  graphSignature: '',
68
72
  graphStatus: '',
69
73
  graphTotals: { nodes: 0, edges: 0 },
74
+ viewport: { width: 320, height: 320 },
70
75
  last: performance.now(),
76
+ lastPhysicsAt: performance.now(),
77
+ physicsRestFrames: 0,
71
78
  offscreenFrameCount: 0,
72
79
  recoveringViewport: false,
73
80
  renderVisibilityDirty: true,
@@ -434,6 +441,7 @@ const resize = () => {
434
441
  glCanvas.width = Math.floor(width * ratio)
435
442
  glCanvas.height = Math.floor(height * ratio)
436
443
  }
444
+ state.viewport = { width, height }
437
445
  ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
438
446
  markRenderDirty()
439
447
  }
@@ -2565,7 +2573,7 @@ const scheduleContentFilterSync = () => {
2565
2573
  }
2566
2574
  }
2567
2575
 
2568
- const tick = delta => {
2576
+ const tick = (delta, now) => {
2569
2577
  const nodes = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
2570
2578
  const edges = state.renderEdges.length > 0 ? state.renderEdges : state.visibleEdges
2571
2579
  const shouldRunPhysics =
@@ -2573,9 +2581,24 @@ const tick = delta => {
2573
2581
  nodes.length <= 320 &&
2574
2582
  state.transform.scale >= 0.08
2575
2583
  if (!shouldRunPhysics) {
2584
+ state.physicsRestFrames = 0
2576
2585
  return
2577
2586
  }
2578
- const strength = Math.min(delta / 16, 2)
2587
+ const isDragging = Boolean(state.pointer.dragNode)
2588
+ const intervalMs = isDragging
2589
+ ? physicsDragFrameIntervalMs
2590
+ : state.nodes.length > largeGraphNodeThreshold
2591
+ ? physicsLargeGraphIdleFrameIntervalMs
2592
+ : physicsIdleFrameIntervalMs
2593
+ if (!isDragging && state.physicsRestFrames >= 18) {
2594
+ return
2595
+ }
2596
+ const elapsedSincePhysics = now - state.lastPhysicsAt
2597
+ if (elapsedSincePhysics < intervalMs) {
2598
+ return
2599
+ }
2600
+ state.lastPhysicsAt = now
2601
+ const strength = Math.min(Math.max(elapsedSincePhysics, delta, 16) / 16, physicsStepDeltaCapMs / 16)
2579
2602
 
2580
2603
  edges.forEach(edge => {
2581
2604
  const source = edge.sourceNode
@@ -2617,6 +2640,7 @@ const tick = delta => {
2617
2640
  }
2618
2641
  }
2619
2642
 
2643
+ let kinetic = 0
2620
2644
  nodes.forEach(node => {
2621
2645
  node.vx = Number.isFinite(node.vx) ? node.vx : 0
2622
2646
  node.vy = Number.isFinite(node.vy) ? node.vy : 0
@@ -2633,7 +2657,16 @@ const tick = delta => {
2633
2657
  node.vy *= 0.88
2634
2658
  node.x += node.vx * strength
2635
2659
  node.y += node.vy * strength
2660
+ kinetic += Math.abs(node.vx) + Math.abs(node.vy)
2636
2661
  })
2662
+ if (isDragging) {
2663
+ state.physicsRestFrames = 0
2664
+ return
2665
+ }
2666
+ const kineticFloor = Math.max(0.16, nodes.length * 0.01)
2667
+ state.physicsRestFrames = kinetic <= kineticFloor
2668
+ ? state.physicsRestFrames + 1
2669
+ : 0
2637
2670
  }
2638
2671
 
2639
2672
  const worldPoint = event => {
@@ -2789,9 +2822,8 @@ const clusterOpacity = cluster =>
2789
2822
  Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
2790
2823
 
2791
2824
  const worldViewportBounds = () => {
2792
- const rect = canvas.getBoundingClientRect()
2793
- const width = Math.max(rect.width, 320)
2794
- const height = Math.max(rect.height, 320)
2825
+ const width = Math.max(state.viewport.width, 320)
2826
+ const height = Math.max(state.viewport.height, 320)
2795
2827
  const paddingMultiplier =
2796
2828
  state.nodes.length > massiveGraphNodeThreshold
2797
2829
  ? (state.transform.scale >= 0.6 ? 2.8 : state.transform.scale >= 0.25 ? 2.35 : 1.9)
@@ -3136,10 +3168,10 @@ const render = now => {
3136
3168
  state.last = now
3137
3169
  const backgroundFrameIntervalMs =
3138
3170
  state.nodes.length > massiveGraphNodeThreshold
3139
- ? (state.transform.scale < 0.035 ? 130 : state.transform.scale < 0.08 ? 110 : 86)
3171
+ ? (state.transform.scale < 0.035 ? 156 : state.transform.scale < 0.08 ? 128 : 98)
3140
3172
  : state.nodes.length > largeGraphNodeThreshold
3141
- ? 64
3142
- : 16
3173
+ ? 72
3174
+ : 18
3143
3175
  const isInteracting =
3144
3176
  state.pointer.down ||
3145
3177
  state.renderVisibilityDirty ||
@@ -3152,6 +3184,7 @@ const render = now => {
3152
3184
  const rect = canvas.getBoundingClientRect()
3153
3185
  const width = Math.max(rect.width, 320)
3154
3186
  const height = Math.max(rect.height, 320)
3187
+ state.viewport = { width, height }
3155
3188
  sanitizeGraphState()
3156
3189
  if (!hasValidTransform()) {
3157
3190
  resetView()
@@ -3168,7 +3201,7 @@ const render = now => {
3168
3201
  }
3169
3202
 
3170
3203
  computeRenderVisibility()
3171
- tick(delta)
3204
+ tick(delta, now)
3172
3205
  const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
3173
3206
  const manualZoomGuardActive = now - state.lastManualZoomAt < zoomRecoveryGuardMs
3174
3207
  const allowViewportAutoRecovery = state.nodes.length <= massiveGraphNodeThreshold
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.103",
3
+ "version": "0.1.0-beta.104",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",