@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.
Files changed (57) hide show
  1. package/dist/core/ActionDispatcher.d.ts +7 -0
  2. package/dist/core/ActionDispatcher.d.ts.map +1 -1
  3. package/dist/dataview/types.d.ts +6 -1
  4. package/dist/dataview/types.d.ts.map +1 -1
  5. package/dist/handles/BaseHandle.d.ts +9 -18
  6. package/dist/handles/BaseHandle.d.ts.map +1 -1
  7. package/dist/handles/EntityHandle.d.ts +12 -4
  8. package/dist/handles/EntityHandle.d.ts.map +1 -1
  9. package/dist/handles/FieldHandle.d.ts.map +1 -1
  10. package/dist/handles/HasManyListHandle.d.ts +0 -4
  11. package/dist/handles/HasManyListHandle.d.ts.map +1 -1
  12. package/dist/handles/HasOneHandle.d.ts +0 -4
  13. package/dist/handles/HasOneHandle.d.ts.map +1 -1
  14. package/dist/handles/PlaceholderHandle.d.ts +18 -0
  15. package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
  16. package/dist/handles/types.d.ts +10 -0
  17. package/dist/handles/types.d.ts.map +1 -1
  18. package/dist/persistence/BatchPersister.d.ts +8 -3
  19. package/dist/persistence/BatchPersister.d.ts.map +1 -1
  20. package/dist/store/DirtyTracker.d.ts +3 -1
  21. package/dist/store/DirtyTracker.d.ts.map +1 -1
  22. package/dist/store/EntityMetaStore.d.ts +4 -0
  23. package/dist/store/EntityMetaStore.d.ts.map +1 -1
  24. package/dist/store/EntitySnapshotStore.d.ts +14 -0
  25. package/dist/store/EntitySnapshotStore.d.ts.map +1 -1
  26. package/dist/store/ReachabilityAnalyzer.d.ts +33 -0
  27. package/dist/store/ReachabilityAnalyzer.d.ts.map +1 -0
  28. package/dist/store/RelationStore.d.ts +21 -10
  29. package/dist/store/RelationStore.d.ts.map +1 -1
  30. package/dist/store/RootRegistry.d.ts +28 -0
  31. package/dist/store/RootRegistry.d.ts.map +1 -0
  32. package/dist/store/SnapshotStore.d.ts +42 -2
  33. package/dist/store/SnapshotStore.d.ts.map +1 -1
  34. package/dist/store/SubscriptionManager.d.ts.map +1 -1
  35. package/package.json +2 -2
  36. package/src/core/ActionDispatcher.ts +38 -21
  37. package/src/dataview/filterHandlers.ts +4 -4
  38. package/src/dataview/types.ts +6 -1
  39. package/src/handles/BaseHandle.ts +9 -30
  40. package/src/handles/EntityHandle.ts +35 -30
  41. package/src/handles/FieldHandle.ts +0 -4
  42. package/src/handles/HasManyListHandle.ts +0 -22
  43. package/src/handles/HasOneHandle.ts +0 -19
  44. package/src/handles/PlaceholderHandle.ts +93 -0
  45. package/src/handles/types.ts +10 -0
  46. package/src/persistence/BatchPersister.ts +60 -22
  47. package/src/store/DirtyTracker.ts +9 -1
  48. package/src/store/EntityMetaStore.ts +10 -0
  49. package/src/store/EntitySnapshotStore.ts +32 -0
  50. package/src/store/ReachabilityAnalyzer.ts +92 -0
  51. package/src/store/RelationStore.ts +61 -37
  52. package/src/store/RootRegistry.ts +44 -0
  53. package/src/store/SnapshotStore.ts +91 -30
  54. package/src/store/SubscriptionManager.ts +8 -2
  55. package/dist/core/Disposable.d.ts +0 -57
  56. package/dist/core/Disposable.d.ts.map +0 -1
  57. 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 the key if a removal was planned (caller should notify), or null if handled inline.
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): 'planned_removal' | 'cancelled_connection' | null {
392
+ removeFromHasMany(key: string, itemId: string, removalType: HasManyRemovalType): boolean {
425
393
  const existing = this.hasManyStates.get(key)
426
- if (!existing) return null
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 'cancelled_connection'
429
+ return true
462
430
  } else {
463
431
  this.planHasManyRemoval(key, itemId, removalType)
464
- return 'planned_removal'
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.dirtyTracker = new DirtyTracker(this.entitySnapshots, this.meta, this.relations)
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.clearLoadState(key)
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
- const result = this.relations.removeFromHasMany(key, itemId, removalType)
517
- if (result === 'planned_removal') {
518
- this.notifyRelationSubscribers(key)
519
- } else if (result === 'cancelled_connection') {
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, not for each parent)
183
- if (notifiedKeys.size === 1) {
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"}
@@ -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
- }