Skip to content

Commit f5da9ff

Browse files
committed
fix: keep cached link views reactive
Amp-Thread-ID: https://ampcode.com/threads/T-019fef81-c9e1-7658-a571-55818c44c32a
1 parent f0fa1fd commit f5da9ff

4 files changed

Lines changed: 26 additions & 30 deletions

File tree

docs/architecture/link-topology-store.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ All `graph.links` mutation funnels through its store-backed `LinkMap` and
106106
registration/unregistration (and link-layout cleanup on removal).
107107
`LinkMap` caches its owner-local regular or floating view so rendering can use
108108
native `Map` reads and snapshot iterators without rebuilding topology on each
109-
access. Store mutations centrally invalidate the view when its membership
110-
changes; endpoint-only updates keep the same link objects and do not invalidate
111-
it.
109+
access. Store mutations centrally invalidate the reactive view.
112110
`addFloatingLink` / `removeFloatingLink` apply floating-specific lifecycle
113111
policy through the same topology collection. `LLink.disconnect` performs the
114112
equivalent effects inline because it only holds a `LinkNetwork`, and

src/lib/litegraph/src/LGraph.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
observeRerouteId
3333
} from './idAllocation'
3434
import type { LGraphState } from './idAllocation'
35-
import { getLinkStoreRevision, useLinkStore } from '@/stores/linkStore'
35+
import { useLinkStore } from '@/stores/linkStore'
3636
import { useNodeDataStore } from '@/stores/nodeDataStore'
3737
import { useRerouteStore } from '@/stores/rerouteStore'
3838
import {
@@ -536,15 +536,16 @@ export class LGraph
536536
* @param o data from previous serialization [optional]
537537
*/
538538
constructor(o?: ISerialisedGraph | SerialisableGraph) {
539+
const linkStore = useLinkStore()
539540
/** @see MapProxyHandler */
540541
const links = new LinkMap(
541542
() => (this.rootGraph ? graphScopeOf(this) : undefined),
542543
(scope) =>
543-
[...useLinkStore().graphTopologies(scope)]
544+
[...linkStore.graphTopologies(scope)]
544545
.filter((topology) => !isFloatingTopology(topology))
545546
.map(resolveLinkTopology)
546547
.filter((link): link is LLink => link !== undefined),
547-
getLinkStoreRevision,
548+
linkStore.getRevision,
548549
(link) => this._addLink(link),
549550
(id) => this._removeLink(id)
550551
)
@@ -558,11 +559,11 @@ export class LGraph
558559
this.floatingLinks = new LinkMap(
559560
() => (this.rootGraph ? graphScopeOf(this) : undefined),
560561
(scope) =>
561-
[...useLinkStore().graphTopologies(scope)]
562+
[...linkStore.graphTopologies(scope)]
562563
.filter(isFloatingTopology)
563564
.map(resolveLinkTopology)
564565
.filter((link): link is LLink => link !== undefined),
565-
getLinkStoreRevision,
566+
linkStore.getRevision,
566567
(link) => this.addFloatingLink(link),
567568
(id) => {
568569
const link = this.floatingLinks.get(id)

src/lib/litegraph/src/LLink.store.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ describe('LLink ↔ linkStore integration', () => {
473473
)
474474
})
475475

476-
it('reuses link views until membership changes', () => {
476+
it('reactively updates cached link views', () => {
477477
const graph = new LGraph()
478478
const a = new LGraphNode('A')
479479
const b = new LGraphNode('B')
@@ -482,10 +482,15 @@ describe('LLink ↔ linkStore integration', () => {
482482
b.addInput('in1', 'INT')
483483
graph.add(a)
484484
graph.add(b)
485+
const linkCount = computed(() => graph.links.size)
486+
487+
expect(linkCount.value).toBe(0)
488+
485489
const link = a.connect(0, b, 0)!
486490
const store = useLinkStore()
487491
const graphTopologies = vi.spyOn(store, 'graphTopologies')
488492

493+
expect(linkCount.value).toBe(1)
489494
expect(graph.links.size).toBe(1)
490495
const traversalCount = graphTopologies.mock.calls.length
491496
expect([...graph.links.values()]).toEqual([link])
@@ -495,7 +500,7 @@ describe('LLink ↔ linkStore integration', () => {
495500
link.target_slot = 1
496501

497502
expect(graph.links.get(link.id)).toBe(link)
498-
expect(graphTopologies).toHaveBeenCalledTimes(traversalCount)
503+
expect(graphTopologies.mock.calls.length).toBeGreaterThan(traversalCount)
499504

500505
link.target_id = UNASSIGNED_NODE_ID
501506

src/stores/linkStore.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineStore } from 'pinia'
2-
import { reactive, toRaw } from 'vue'
2+
import { reactive, shallowRef, toRaw } from 'vue'
33

44
import type {
55
GraphScope,
@@ -82,11 +82,6 @@ interface RootTopologyBucket {
8282
}
8383

8484
const EMPTY_LINKS: ReadonlySet<LinkTopology> = new Set()
85-
let revision = 0
86-
87-
export function getLinkStoreRevision(): number {
88-
return revision
89-
}
9085

9186
/**
9287
* A link is keyed by its target input slot only when that slot uniquely
@@ -110,6 +105,11 @@ function hasUniqueTarget(topology: LinkTopology): boolean {
110105
*/
111106
export const useLinkStore = defineStore('link', () => {
112107
const roots = reactive(new Map<RootGraphId, RootTopologyBucket>())
108+
const revision = shallowRef(0)
109+
110+
function getRevision(): number {
111+
return revision.value
112+
}
113113

114114
function rootBucket(rootGraphId: RootGraphId): RootTopologyBucket {
115115
const existing = roots.get(rootGraphId)
@@ -234,7 +234,7 @@ export const useLinkStore = defineStore('link', () => {
234234
if (expected) displace(targetBucket, expected)
235235
const owned = Object.assign(replacement, { graphId: scope.owningGraphId })
236236
const placed = placeValidated(targetBucket, owned)
237-
revision++
237+
revision.value++
238238
return placed
239239
}
240240

@@ -262,7 +262,7 @@ export const useLinkStore = defineStore('link', () => {
262262
if (!bucket || !ownsPlacement(scope, bucket, topology)) return false
263263
displace(bucket, topology)
264264
if (bucket.byId.size === 0) roots.delete(scope.rootGraphId)
265-
revision++
265+
revision.value++
266266
return true
267267
}
268268

@@ -339,15 +339,6 @@ export const useLinkStore = defineStore('link', () => {
339339
): EndpointUpdateResult<LinkTopology[]> {
340340
const error = validateEndpointUpdates(scope, updates, removals)
341341
if (error) return { ok: false, error }
342-
const changesLinkView =
343-
removals.length > 0 ||
344-
updates.some(
345-
({ topology, patch }) =>
346-
isFloatingTopology({
347-
...toRaw(topology),
348-
...patchedEndpoints(topology, patch)
349-
}) !== isFloatingTopology(topology)
350-
)
351342

352343
const bucket = rootBucket(scope.rootGraphId)
353344
for (const { topology } of updates) displace(bucket, topology)
@@ -358,7 +349,7 @@ export const useLinkStore = defineStore('link', () => {
358349
return placeValidated(bucket, topology)
359350
})
360351
if (bucket.byId.size === 0) roots.delete(scope.rootGraphId)
361-
if (changesLinkView) revision++
352+
revision.value++
362353
return { ok: true, value }
363354
}
364355

@@ -438,7 +429,7 @@ export const useLinkStore = defineStore('link', () => {
438429
}
439430

440431
function clearGraph(graphId: RootGraphId): void {
441-
if (roots.delete(graphId)) revision++
432+
if (roots.delete(graphId)) revision.value++
442433
}
443434

444435
function clearOwner(scope: GraphScope): void {
@@ -450,7 +441,7 @@ export const useLinkStore = defineStore('link', () => {
450441
if (topology) displace(bucket, topology)
451442
}
452443
if (bucket.byId.size === 0) roots.delete(scope.rootGraphId)
453-
revision++
444+
revision.value++
454445
}
455446

456447
return {
@@ -465,6 +456,7 @@ export const useLinkStore = defineStore('link', () => {
465456
getOutputSlotLinks,
466457
getTopology,
467458
graphTopologies,
459+
getRevision,
468460
clearOwner,
469461
clearGraph
470462
}

0 commit comments

Comments
 (0)