@contember/bindx 0.1.43 → 0.1.45
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/dist/core/ActionDispatcher.d.ts +7 -0
- package/dist/core/ActionDispatcher.d.ts.map +1 -1
- package/dist/dataview/types.d.ts +6 -1
- package/dist/dataview/types.d.ts.map +1 -1
- package/dist/handles/BaseHandle.d.ts +9 -18
- package/dist/handles/BaseHandle.d.ts.map +1 -1
- package/dist/handles/EntityHandle.d.ts +12 -4
- package/dist/handles/EntityHandle.d.ts.map +1 -1
- package/dist/handles/FieldHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +0 -4
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/HasOneHandle.d.ts +0 -4
- package/dist/handles/HasOneHandle.d.ts.map +1 -1
- package/dist/handles/PlaceholderHandle.d.ts +18 -0
- package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
- package/dist/handles/types.d.ts +10 -0
- package/dist/handles/types.d.ts.map +1 -1
- package/dist/persistence/BatchPersister.d.ts +8 -3
- package/dist/persistence/BatchPersister.d.ts.map +1 -1
- package/dist/store/DirtyTracker.d.ts +3 -1
- package/dist/store/DirtyTracker.d.ts.map +1 -1
- package/dist/store/EntityMetaStore.d.ts +4 -0
- package/dist/store/EntityMetaStore.d.ts.map +1 -1
- package/dist/store/EntitySnapshotStore.d.ts +14 -0
- package/dist/store/EntitySnapshotStore.d.ts.map +1 -1
- package/dist/store/ReachabilityAnalyzer.d.ts +33 -0
- package/dist/store/ReachabilityAnalyzer.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +21 -10
- package/dist/store/RelationStore.d.ts.map +1 -1
- package/dist/store/RootRegistry.d.ts +28 -0
- package/dist/store/RootRegistry.d.ts.map +1 -0
- package/dist/store/SnapshotStore.d.ts +42 -2
- package/dist/store/SnapshotStore.d.ts.map +1 -1
- package/dist/store/SubscriptionManager.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/ActionDispatcher.ts +38 -21
- package/src/dataview/filterHandlers.ts +4 -4
- package/src/dataview/types.ts +6 -1
- package/src/handles/BaseHandle.ts +9 -30
- package/src/handles/EntityHandle.ts +35 -30
- package/src/handles/FieldHandle.ts +0 -4
- package/src/handles/HasManyListHandle.ts +0 -22
- package/src/handles/HasOneHandle.ts +0 -19
- package/src/handles/PlaceholderHandle.ts +93 -0
- package/src/handles/types.ts +10 -0
- package/src/persistence/BatchPersister.ts +60 -22
- package/src/store/DirtyTracker.ts +9 -1
- package/src/store/EntityMetaStore.ts +10 -0
- package/src/store/EntitySnapshotStore.ts +32 -0
- package/src/store/ReachabilityAnalyzer.ts +92 -0
- package/src/store/RelationStore.ts +61 -37
- package/src/store/RootRegistry.ts +44 -0
- package/src/store/SnapshotStore.ts +91 -30
- package/src/store/SubscriptionManager.ts +8 -2
- package/dist/core/Disposable.d.ts +0 -57
- package/dist/core/Disposable.d.ts.map +0 -1
- package/src/core/Disposable.ts +0 -126
|
@@ -252,22 +252,6 @@ export class RelationStore {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
/**
|
|
256
|
-
* Cancels a planned removal for a has-many item.
|
|
257
|
-
*/
|
|
258
|
-
cancelHasManyRemoval(key: string, itemId: string): void {
|
|
259
|
-
const existing = this.hasManyStates.get(key)
|
|
260
|
-
if (!existing) return
|
|
261
|
-
|
|
262
|
-
const newPlannedRemovals = new Map(existing.plannedRemovals)
|
|
263
|
-
newPlannedRemovals.delete(itemId)
|
|
264
|
-
this.hasManyStates.set(key, {
|
|
265
|
-
...existing,
|
|
266
|
-
plannedRemovals: newPlannedRemovals,
|
|
267
|
-
version: existing.version + 1,
|
|
268
|
-
})
|
|
269
|
-
}
|
|
270
|
-
|
|
271
255
|
/**
|
|
272
256
|
* Plans a connection for a has-many item.
|
|
273
257
|
*/
|
|
@@ -302,22 +286,6 @@ export class RelationStore {
|
|
|
302
286
|
}
|
|
303
287
|
}
|
|
304
288
|
|
|
305
|
-
/**
|
|
306
|
-
* Cancels a planned connection for a has-many item.
|
|
307
|
-
*/
|
|
308
|
-
cancelHasManyConnection(key: string, itemId: string): void {
|
|
309
|
-
const existing = this.hasManyStates.get(key)
|
|
310
|
-
if (!existing) return
|
|
311
|
-
|
|
312
|
-
const newPlannedConnections = new Set(existing.plannedConnections)
|
|
313
|
-
newPlannedConnections.delete(itemId)
|
|
314
|
-
this.hasManyStates.set(key, {
|
|
315
|
-
...existing,
|
|
316
|
-
plannedConnections: newPlannedConnections,
|
|
317
|
-
version: existing.version + 1,
|
|
318
|
-
})
|
|
319
|
-
}
|
|
320
|
-
|
|
321
289
|
/**
|
|
322
290
|
* Commits has-many state after successful persist.
|
|
323
291
|
*/
|
|
@@ -419,11 +387,11 @@ export class RelationStore {
|
|
|
419
387
|
* Removes an entity from a has-many relation.
|
|
420
388
|
* For newly created entities (via add()), cancels the connection.
|
|
421
389
|
* For existing server entities, plans the specified removal type.
|
|
422
|
-
* Returns
|
|
390
|
+
* Returns true if the state changed (caller should notify), false if it was a no-op.
|
|
423
391
|
*/
|
|
424
|
-
removeFromHasMany(key: string, itemId: string, removalType: HasManyRemovalType):
|
|
392
|
+
removeFromHasMany(key: string, itemId: string, removalType: HasManyRemovalType): boolean {
|
|
425
393
|
const existing = this.hasManyStates.get(key)
|
|
426
|
-
if (!existing) return
|
|
394
|
+
if (!existing) return false
|
|
427
395
|
|
|
428
396
|
const isCreatedEntity = existing.createdEntities.has(itemId)
|
|
429
397
|
|
|
@@ -458,10 +426,66 @@ export class RelationStore {
|
|
|
458
426
|
}
|
|
459
427
|
|
|
460
428
|
this.hasManyStates.set(key, newState)
|
|
461
|
-
return
|
|
429
|
+
return true
|
|
462
430
|
} else {
|
|
463
431
|
this.planHasManyRemoval(key, itemId, removalType)
|
|
464
|
-
return
|
|
432
|
+
return true
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Collects the ids of child entities currently reachable through an entity's
|
|
438
|
+
* LIVE relations (key prefix "parentType:parentId:"). Used by reachability-based
|
|
439
|
+
* create detection to walk the relation graph from roots.
|
|
440
|
+
*
|
|
441
|
+
* Live edges are:
|
|
442
|
+
* - has-one: currentId, when the relation is not disconnected/deleted
|
|
443
|
+
* (a disconnected relation has a null currentId; a 'deleted' relation is
|
|
444
|
+
* removing its target, so the target is not anchored by it).
|
|
445
|
+
* - has-many: effective members = (serverIds ∪ plannedConnections ∪
|
|
446
|
+
* createdEntities) minus plannedRemovals.
|
|
447
|
+
*/
|
|
448
|
+
getLiveChildIds(keyPrefix: string): string[] {
|
|
449
|
+
const ids = new Set<string>()
|
|
450
|
+
|
|
451
|
+
for (const [key, state] of this.relationStates) {
|
|
452
|
+
if (!key.startsWith(keyPrefix)) continue
|
|
453
|
+
if (state.currentId !== null && state.state !== 'deleted') {
|
|
454
|
+
ids.add(state.currentId)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const [key, state] of this.hasManyStates) {
|
|
459
|
+
if (!key.startsWith(keyPrefix)) continue
|
|
460
|
+
for (const id of state.serverIds) {
|
|
461
|
+
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
462
|
+
}
|
|
463
|
+
for (const id of state.plannedConnections) {
|
|
464
|
+
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
465
|
+
}
|
|
466
|
+
for (const id of state.createdEntities) {
|
|
467
|
+
if (!state.plannedRemovals.has(id)) ids.add(id)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return Array.from(ids)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Removes all relation and has-many state owned by an entity (keys under the
|
|
476
|
+
* given owner prefix). Called by removeEntity so a removed entity leaves no
|
|
477
|
+
* stale relation state behind.
|
|
478
|
+
*/
|
|
479
|
+
removeOwnedRelations(keyPrefix: string): void {
|
|
480
|
+
for (const key of [...this.relationStates.keys()]) {
|
|
481
|
+
if (key.startsWith(keyPrefix)) {
|
|
482
|
+
this.relationStates.delete(key)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (const key of [...this.hasManyStates.keys()]) {
|
|
486
|
+
if (key.startsWith(keyPrefix)) {
|
|
487
|
+
this.hasManyStates.delete(key)
|
|
488
|
+
}
|
|
465
489
|
}
|
|
466
490
|
}
|
|
467
491
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks explicitly-created top-level entities ("roots").
|
|
3
|
+
*
|
|
4
|
+
* Reachability-based create detection treats a created entity as a `create`
|
|
5
|
+
* only when it is reachable from a root through live relations. Roots are:
|
|
6
|
+
* - every entity that exists on the server (tracked via EntityMetaStore), and
|
|
7
|
+
* - explicitly-created top-level entities that have no persisted parent.
|
|
8
|
+
*
|
|
9
|
+
* The first set is derivable from metadata. The second set is NOT derivable
|
|
10
|
+
* from the relation graph — a top-level `<Entity create>` or a `useEntityList`
|
|
11
|
+
* add has no parent relation anchoring it, so it must be registered here when
|
|
12
|
+
* created and unregistered when removed.
|
|
13
|
+
*
|
|
14
|
+
* Keys use the same composite format as the rest of the store: "entityType:id".
|
|
15
|
+
*/
|
|
16
|
+
export class RootRegistry {
|
|
17
|
+
private readonly roots = new Set<string>()
|
|
18
|
+
|
|
19
|
+
register(key: string): void {
|
|
20
|
+
this.roots.add(key)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
unregister(key: string): void {
|
|
24
|
+
this.roots.delete(key)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
keys(): IterableIterator<string> {
|
|
28
|
+
return this.roots.keys()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Moves a root entry from oldKey to newKey (used after persist rekeys a temp
|
|
33
|
+
* id to a server-assigned id).
|
|
34
|
+
*/
|
|
35
|
+
rekey(oldKey: string, newKey: string): void {
|
|
36
|
+
if (this.roots.delete(oldKey)) {
|
|
37
|
+
this.roots.add(newKey)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear(): void {
|
|
42
|
+
this.roots.clear()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -13,6 +13,8 @@ import { TouchedStore } from './TouchedStore.js'
|
|
|
13
13
|
import { generateTempId } from './entityId.js'
|
|
14
14
|
import { DirtyTracker } from './DirtyTracker.js'
|
|
15
15
|
import { EntitySnapshotStore } from './EntitySnapshotStore.js'
|
|
16
|
+
import { RootRegistry } from './RootRegistry.js'
|
|
17
|
+
import { ReachabilityAnalyzer } from './ReachabilityAnalyzer.js'
|
|
16
18
|
|
|
17
19
|
export type { HasManyRemovalType, StoredHasManyState, StoredRelationState } from './RelationStore.js'
|
|
18
20
|
export type { EntityMeta } from './EntityMetaStore.js'
|
|
@@ -44,6 +46,8 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
44
46
|
private readonly relations = new RelationStore()
|
|
45
47
|
private readonly meta = new EntityMetaStore()
|
|
46
48
|
private readonly touched = new TouchedStore()
|
|
49
|
+
private readonly roots = new RootRegistry()
|
|
50
|
+
private readonly reachability: ReachabilityAnalyzer
|
|
47
51
|
private readonly dirtyTracker: DirtyTracker
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -55,7 +59,8 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
55
59
|
private readonly lastPropagatedData = new Map<string, unknown>()
|
|
56
60
|
|
|
57
61
|
constructor() {
|
|
58
|
-
this.
|
|
62
|
+
this.reachability = new ReachabilityAnalyzer(this.entitySnapshots, this.meta, this.relations, this.roots)
|
|
63
|
+
this.dirtyTracker = new DirtyTracker(this.entitySnapshots, this.meta, this.relations, this.reachability)
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
// ==================== Key Generation ====================
|
|
@@ -238,14 +243,47 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
238
243
|
this.notifyEntitySubscribers(key)
|
|
239
244
|
}
|
|
240
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Removes a single entity and all of its own per-entity state (snapshot,
|
|
248
|
+
* metadata, root registration, errors, touched state, propagation tracking,
|
|
249
|
+
* and owned relation state).
|
|
250
|
+
*
|
|
251
|
+
* This does NOT cascade into descendants, and it does NOT strip inbound edges
|
|
252
|
+
* (other entities' has-many membership / has-one currentId that point AT the
|
|
253
|
+
* removed id). Callers must therefore only remove an entity that is unreachable
|
|
254
|
+
* — i.e. no live parent relation references it — so the graph cannot dangle. The
|
|
255
|
+
* lazy {@link sweepUnreachableCreated} guarantees this by construction; the React
|
|
256
|
+
* unmount cleanup routes through it (via {@link unregisterRootEntity} + a sweep)
|
|
257
|
+
* rather than calling removeEntity on a possibly-referenced id.
|
|
258
|
+
*/
|
|
241
259
|
removeEntity(entityType: string, id: string): void {
|
|
242
260
|
const key = this.getEntityKey(entityType, id)
|
|
261
|
+
const keyPrefix = `${key}:`
|
|
262
|
+
|
|
243
263
|
this.entitySnapshots.remove(key)
|
|
244
|
-
this.meta.
|
|
264
|
+
this.meta.remove(key)
|
|
265
|
+
this.roots.unregister(key)
|
|
266
|
+
this.errors.clearAllErrors(key, keyPrefix)
|
|
267
|
+
this.touched.clearForEntity(keyPrefix)
|
|
245
268
|
this.clearPropagatedDataForEntity(entityType, id)
|
|
269
|
+
this.relations.removeOwnedRelations(keyPrefix)
|
|
270
|
+
|
|
246
271
|
this.notifyEntitySubscribers(key)
|
|
247
272
|
}
|
|
248
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Whether an entity (looked up by id alone) exists in the store and has never
|
|
276
|
+
* been persisted to the server. Used to decide relation-state semantics when a
|
|
277
|
+
* has-one target is deleted — deleting a never-persisted target reverts the
|
|
278
|
+
* relation to 'disconnected' rather than 'deleted', since there is no server
|
|
279
|
+
* row to delete.
|
|
280
|
+
*/
|
|
281
|
+
isNeverPersisted(entityId: string): boolean {
|
|
282
|
+
const key = this.entitySnapshots.keyForId(entityId)
|
|
283
|
+
if (!key) return false
|
|
284
|
+
return !this.meta.existsOnServer(key)
|
|
285
|
+
}
|
|
286
|
+
|
|
249
287
|
// ==================== Load State (delegated to EntityMetaStore) ====================
|
|
250
288
|
|
|
251
289
|
getLoadState(entityType: string, id: string): { status: LoadStatus; error?: FieldError } | undefined {
|
|
@@ -306,9 +344,28 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
306
344
|
this.setEntityData(entityType, id, data, false)
|
|
307
345
|
this.setExistsOnServer(entityType, id, false)
|
|
308
346
|
|
|
347
|
+
// A freshly created entity is pending-persist by default — a root for
|
|
348
|
+
// reachability-based create detection. It stops being a root the moment a
|
|
349
|
+
// relation anchors it as a child (see registerParentChild). A top-level
|
|
350
|
+
// create (<Entity create>, useEntityList add) is never anchored, so it stays
|
|
351
|
+
// a root and is reported as a `create`.
|
|
352
|
+
this.roots.register(this.getEntityKey(entityType, id))
|
|
353
|
+
|
|
309
354
|
return id
|
|
310
355
|
}
|
|
311
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Unregisters a top-level root WITHOUT removing its snapshot. Called by the React
|
|
359
|
+
* unmount cleanup (a `<Entity create>` form / `useEntityList` draft) before a
|
|
360
|
+
* {@link sweepUnreachableCreated} pass: dropping the root lets the sweep reclaim a
|
|
361
|
+
* truly-orphaned create, while a create still anchored by another live relation
|
|
362
|
+
* stays reachable and survives. (Un-rooting also happens implicitly via
|
|
363
|
+
* {@link registerParentChild} and the rekey in {@link mapTempIdToPersistedId}.)
|
|
364
|
+
*/
|
|
365
|
+
unregisterRootEntity(entityType: string, id: string): void {
|
|
366
|
+
this.roots.unregister(this.getEntityKey(entityType, id))
|
|
367
|
+
}
|
|
368
|
+
|
|
312
369
|
mapTempIdToPersistedId(entityType: string, tempId: string, persistedId: string): void {
|
|
313
370
|
// Use raw keys (bypass resolveId) since we're the ones creating the mapping
|
|
314
371
|
const oldKey = `${entityType}:${tempId}`
|
|
@@ -319,6 +376,10 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
319
376
|
// Register redirect so future lookups by temp ID resolve to persisted key
|
|
320
377
|
this.rekeyedEntities.set(oldKey, newKey)
|
|
321
378
|
|
|
379
|
+
// Keep root registration consistent across the rekey (the persisted entity
|
|
380
|
+
// is now a server root via existsOnServer, but keep the registry aligned).
|
|
381
|
+
this.roots.rekey(oldKey, newKey)
|
|
382
|
+
|
|
322
383
|
// Rekey entity snapshot (moves data, updates id field)
|
|
323
384
|
this.entitySnapshots.rekey(oldKey, newKey, persistedId)
|
|
324
385
|
|
|
@@ -402,18 +463,6 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
402
463
|
this.notifyRelationSubscribers(key)
|
|
403
464
|
}
|
|
404
465
|
|
|
405
|
-
cancelHasManyRemoval(
|
|
406
|
-
parentType: string,
|
|
407
|
-
parentId: string,
|
|
408
|
-
fieldName: string,
|
|
409
|
-
itemId: string,
|
|
410
|
-
alias?: string,
|
|
411
|
-
): void {
|
|
412
|
-
const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
|
|
413
|
-
this.relations.cancelHasManyRemoval(key, itemId)
|
|
414
|
-
this.notifyRelationSubscribers(key)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
466
|
getHasManyPlannedRemovals(
|
|
418
467
|
parentType: string,
|
|
419
468
|
parentId: string,
|
|
@@ -436,18 +485,6 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
436
485
|
this.notifyRelationSubscribers(key)
|
|
437
486
|
}
|
|
438
487
|
|
|
439
|
-
cancelHasManyConnection(
|
|
440
|
-
parentType: string,
|
|
441
|
-
parentId: string,
|
|
442
|
-
fieldName: string,
|
|
443
|
-
itemId: string,
|
|
444
|
-
alias?: string,
|
|
445
|
-
): void {
|
|
446
|
-
const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
|
|
447
|
-
this.relations.cancelHasManyConnection(key, itemId)
|
|
448
|
-
this.notifyRelationSubscribers(key)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
488
|
getHasManyPlannedConnections(
|
|
452
489
|
parentType: string,
|
|
453
490
|
parentId: string,
|
|
@@ -513,10 +550,10 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
513
550
|
alias?: string,
|
|
514
551
|
): void {
|
|
515
552
|
const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
553
|
+
// Cancelling the add of a never-persisted child just removes it from the
|
|
554
|
+
// list; its now-unreachable snapshot is no longer reported as a `create` and
|
|
555
|
+
// is collected by the lazy memory sweep.
|
|
556
|
+
if (this.relations.removeFromHasMany(key, itemId, removalType)) {
|
|
520
557
|
this.notifyRelationSubscribers(key)
|
|
521
558
|
}
|
|
522
559
|
}
|
|
@@ -807,6 +844,9 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
807
844
|
const parentKey = this.getEntityKey(parentType, parentId)
|
|
808
845
|
const childKey = this.getEntityKey(childType, childId)
|
|
809
846
|
this.subscriptions.registerParentChild(parentKey, childKey)
|
|
847
|
+
// A child anchored by a parent relation is no longer a top-level root; its
|
|
848
|
+
// reachability now flows through the parent. (No-op for server children.)
|
|
849
|
+
this.roots.unregister(childKey)
|
|
810
850
|
}
|
|
811
851
|
|
|
812
852
|
unregisterParentChild(parentType: string, parentId: string, childType: string, childId: string): void {
|
|
@@ -870,6 +910,26 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
870
910
|
return this.dirtyTracker.getAllDirtyEntities()
|
|
871
911
|
}
|
|
872
912
|
|
|
913
|
+
/**
|
|
914
|
+
* Removes the snapshots of created (never-persisted) entities that are no longer
|
|
915
|
+
* reachable from any root — the lazy memory sweep that replaces eager purge.
|
|
916
|
+
*
|
|
917
|
+
* Correctness never depends on this (unreachable creates are already excluded
|
|
918
|
+
* from getAllDirtyEntities); it only reclaims memory. Run it off the hot path,
|
|
919
|
+
* e.g. once a persist settles. Shared (diamond) children are kept because
|
|
920
|
+
* reachability reports them as live as long as any parent references them.
|
|
921
|
+
*/
|
|
922
|
+
sweepUnreachableCreated(): void {
|
|
923
|
+
const reachable = this.reachability.computeReachableCreated()
|
|
924
|
+
for (const key of [...this.entitySnapshots.keys()]) {
|
|
925
|
+
if (this.meta.existsOnServer(key)) continue
|
|
926
|
+
if (reachable.has(key)) continue
|
|
927
|
+
const separator = key.indexOf(':')
|
|
928
|
+
if (separator === -1) continue
|
|
929
|
+
this.removeEntity(key.slice(0, separator), key.slice(separator + 1))
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
873
933
|
getDirtyFields(entityType: string, entityId: string): string[] {
|
|
874
934
|
return this.dirtyTracker.getDirtyFields(entityType, entityId)
|
|
875
935
|
}
|
|
@@ -892,6 +952,7 @@ export class SnapshotStore implements SnapshotVersionBumper {
|
|
|
892
952
|
this.relations.clear()
|
|
893
953
|
this.errors.clear()
|
|
894
954
|
this.touched.clear()
|
|
955
|
+
this.roots.clear()
|
|
895
956
|
this.lastPropagatedData.clear()
|
|
896
957
|
|
|
897
958
|
this.subscriptions.notify()
|
|
@@ -157,6 +157,11 @@ export class SubscriptionManager {
|
|
|
157
157
|
): void {
|
|
158
158
|
// Prevent infinite recursion
|
|
159
159
|
if (notifiedKeys.has(key)) return
|
|
160
|
+
// Capture "am I the root invocation" BEFORE adding/recursing — the parent
|
|
161
|
+
// propagation below grows the shared `notifiedKeys` set, so checking its
|
|
162
|
+
// size after recursion would misclassify any child-with-parent as non-root
|
|
163
|
+
// and skip the global notification (see issue #51).
|
|
164
|
+
const isRoot = notifiedKeys.size === 0
|
|
160
165
|
notifiedKeys.add(key)
|
|
161
166
|
|
|
162
167
|
this.globalVersion++
|
|
@@ -179,8 +184,9 @@ export class SubscriptionManager {
|
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
// Notify global subscribers (only once,
|
|
183
|
-
|
|
187
|
+
// Notify global subscribers (only once, from the root invocation — not
|
|
188
|
+
// again for each parent reached via propagation)
|
|
189
|
+
if (isRoot) {
|
|
184
190
|
for (const sub of this.globalSubscribers) {
|
|
185
191
|
sub()
|
|
186
192
|
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Disposable pattern for cleanup management.
|
|
3
|
-
* Ensures proper cleanup of subscriptions and resources.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Interface for objects that can be disposed.
|
|
7
|
-
*/
|
|
8
|
-
export interface Disposable {
|
|
9
|
-
dispose(): void;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* A group of disposables that can be disposed together.
|
|
13
|
-
* Provides safe cleanup even if individual dispose() calls throw.
|
|
14
|
-
*/
|
|
15
|
-
export declare class DisposableGroup implements Disposable {
|
|
16
|
-
private disposables;
|
|
17
|
-
private disposed;
|
|
18
|
-
/**
|
|
19
|
-
* Adds a disposable to the group.
|
|
20
|
-
* If the group is already disposed, the disposable is disposed immediately.
|
|
21
|
-
*/
|
|
22
|
-
add(disposable: Disposable): void;
|
|
23
|
-
/**
|
|
24
|
-
* Adds a callback as a disposable.
|
|
25
|
-
*/
|
|
26
|
-
addCallback(callback: () => void): void;
|
|
27
|
-
/**
|
|
28
|
-
* Adds an unsubscribe function as a disposable.
|
|
29
|
-
*/
|
|
30
|
-
addUnsubscribe(unsubscribe: () => void): void;
|
|
31
|
-
/**
|
|
32
|
-
* Disposes all items in the group.
|
|
33
|
-
* Each item is disposed even if previous ones throw.
|
|
34
|
-
*/
|
|
35
|
-
dispose(): void;
|
|
36
|
-
/**
|
|
37
|
-
* Checks if the group has been disposed.
|
|
38
|
-
*/
|
|
39
|
-
isDisposed(): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Gets the number of disposables in the group.
|
|
42
|
-
*/
|
|
43
|
-
get size(): number;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Creates a disposable from a callback function.
|
|
47
|
-
*/
|
|
48
|
-
export declare function createDisposable(callback: () => void): Disposable;
|
|
49
|
-
/**
|
|
50
|
-
* Combines multiple disposables into one.
|
|
51
|
-
*/
|
|
52
|
-
export declare function combineDisposables(...disposables: Disposable[]): Disposable;
|
|
53
|
-
/**
|
|
54
|
-
* Type guard to check if something is disposable.
|
|
55
|
-
*/
|
|
56
|
-
export declare function isDisposable(value: unknown): value is Disposable;
|
|
57
|
-
//# sourceMappingURL=Disposable.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Disposable.d.ts","sourceRoot":"","sources":["../../src/core/Disposable.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,OAAO,IAAI,IAAI,CAAA;CACf;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,UAAU;IACjD,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,QAAQ,CAAQ;IAExB;;;OAGG;IACH,GAAG,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAajC;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIvC;;OAEG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,IAAI,GAAG,IAAI;IAI7C;;;OAGG;IACH,OAAO,IAAI,IAAI;IAqBf;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;CACD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,UAAU,CASjE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,WAAW,EAAE,UAAU,EAAE,GAAG,UAAU,CAM3E;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE"}
|
package/src/core/Disposable.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Disposable pattern for cleanup management.
|
|
3
|
-
* Ensures proper cleanup of subscriptions and resources.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Interface for objects that can be disposed.
|
|
8
|
-
*/
|
|
9
|
-
export interface Disposable {
|
|
10
|
-
dispose(): void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* A group of disposables that can be disposed together.
|
|
15
|
-
* Provides safe cleanup even if individual dispose() calls throw.
|
|
16
|
-
*/
|
|
17
|
-
export class DisposableGroup implements Disposable {
|
|
18
|
-
private disposables: Disposable[] = []
|
|
19
|
-
private disposed = false
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Adds a disposable to the group.
|
|
23
|
-
* If the group is already disposed, the disposable is disposed immediately.
|
|
24
|
-
*/
|
|
25
|
-
add(disposable: Disposable): void {
|
|
26
|
-
if (this.disposed) {
|
|
27
|
-
// Already disposed, dispose immediately
|
|
28
|
-
try {
|
|
29
|
-
disposable.dispose()
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error('Error disposing:', error)
|
|
32
|
-
}
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
this.disposables.push(disposable)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Adds a callback as a disposable.
|
|
40
|
-
*/
|
|
41
|
-
addCallback(callback: () => void): void {
|
|
42
|
-
this.add({ dispose: callback })
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Adds an unsubscribe function as a disposable.
|
|
47
|
-
*/
|
|
48
|
-
addUnsubscribe(unsubscribe: () => void): void {
|
|
49
|
-
this.addCallback(unsubscribe)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Disposes all items in the group.
|
|
54
|
-
* Each item is disposed even if previous ones throw.
|
|
55
|
-
*/
|
|
56
|
-
dispose(): void {
|
|
57
|
-
if (this.disposed) return
|
|
58
|
-
this.disposed = true
|
|
59
|
-
|
|
60
|
-
const errors: Error[] = []
|
|
61
|
-
|
|
62
|
-
for (const disposable of this.disposables) {
|
|
63
|
-
try {
|
|
64
|
-
disposable.dispose()
|
|
65
|
-
} catch (error) {
|
|
66
|
-
errors.push(error instanceof Error ? error : new Error(String(error)))
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.disposables = []
|
|
71
|
-
|
|
72
|
-
if (errors.length > 0) {
|
|
73
|
-
console.error('Errors during disposal:', errors)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Checks if the group has been disposed.
|
|
79
|
-
*/
|
|
80
|
-
isDisposed(): boolean {
|
|
81
|
-
return this.disposed
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Gets the number of disposables in the group.
|
|
86
|
-
*/
|
|
87
|
-
get size(): number {
|
|
88
|
-
return this.disposables.length
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Creates a disposable from a callback function.
|
|
94
|
-
*/
|
|
95
|
-
export function createDisposable(callback: () => void): Disposable {
|
|
96
|
-
let disposed = false
|
|
97
|
-
return {
|
|
98
|
-
dispose() {
|
|
99
|
-
if (disposed) return
|
|
100
|
-
disposed = true
|
|
101
|
-
callback()
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Combines multiple disposables into one.
|
|
108
|
-
*/
|
|
109
|
-
export function combineDisposables(...disposables: Disposable[]): Disposable {
|
|
110
|
-
const group = new DisposableGroup()
|
|
111
|
-
for (const d of disposables) {
|
|
112
|
-
group.add(d)
|
|
113
|
-
}
|
|
114
|
-
return group
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Type guard to check if something is disposable.
|
|
119
|
-
*/
|
|
120
|
-
export function isDisposable(value: unknown): value is Disposable {
|
|
121
|
-
return (
|
|
122
|
-
typeof value === 'object' &&
|
|
123
|
-
value !== null &&
|
|
124
|
-
typeof (value as Disposable).dispose === 'function'
|
|
125
|
-
)
|
|
126
|
-
}
|