@contember/bindx 0.1.52 → 0.1.54

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 (49) hide show
  1. package/dist/core/ActionDispatcher.d.ts.map +1 -1
  2. package/dist/core/EntityLoader.d.ts +7 -0
  3. package/dist/core/EntityLoader.d.ts.map +1 -1
  4. package/dist/core/actions.d.ts +1 -2
  5. package/dist/core/actions.d.ts.map +1 -1
  6. package/dist/dataview/types.d.ts +6 -6
  7. package/dist/dataview/types.d.ts.map +1 -1
  8. package/dist/handles/EntityHandle.d.ts.map +1 -1
  9. package/dist/handles/HasManyListHandle.d.ts +8 -0
  10. package/dist/handles/HasManyListHandle.d.ts.map +1 -1
  11. package/dist/handles/HasOneHandle.d.ts.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/store/HasManyStore.d.ts +94 -78
  15. package/dist/store/HasManyStore.d.ts.map +1 -1
  16. package/dist/store/RelationStore.d.ts +21 -12
  17. package/dist/store/RelationStore.d.ts.map +1 -1
  18. package/dist/store/ResponseOccurrenceIndex.d.ts +65 -0
  19. package/dist/store/ResponseOccurrenceIndex.d.ts.map +1 -0
  20. package/dist/store/SnapshotStore.d.ts +49 -14
  21. package/dist/store/SnapshotStore.d.ts.map +1 -1
  22. package/dist/store/hasManyState.d.ts +164 -0
  23. package/dist/store/hasManyState.d.ts.map +1 -0
  24. package/dist/store/relationKey.d.ts +11 -0
  25. package/dist/store/relationKey.d.ts.map +1 -1
  26. package/dist/undo/UndoJournal.d.ts.map +1 -1
  27. package/dist/undo/rekeyJournalEntry.d.ts +4 -4
  28. package/dist/undo/rekeyJournalEntry.d.ts.map +1 -1
  29. package/dist/utils/aliasGenerator.d.ts +1 -1
  30. package/dist/utils/aliasGenerator.d.ts.map +1 -1
  31. package/package.json +2 -2
  32. package/src/core/ActionDispatcher.ts +0 -1
  33. package/src/core/EntityLoader.ts +7 -0
  34. package/src/core/actions.ts +1 -3
  35. package/src/dataview/types.ts +6 -6
  36. package/src/handles/EntityHandle.ts +11 -0
  37. package/src/handles/HasManyListHandle.ts +15 -17
  38. package/src/handles/HasOneHandle.ts +4 -2
  39. package/src/index.ts +6 -0
  40. package/src/store/HasManyStore.ts +312 -488
  41. package/src/store/RelationStore.ts +60 -24
  42. package/src/store/ResponseOccurrenceIndex.ts +224 -0
  43. package/src/store/SnapshotStore.ts +144 -73
  44. package/src/store/hasManyState.ts +408 -0
  45. package/src/store/relationKey.ts +17 -0
  46. package/src/undo/UndoJournal.ts +3 -1
  47. package/src/undo/UndoManager.ts +1 -1
  48. package/src/undo/rekeyJournalEntry.ts +31 -43
  49. package/src/utils/aliasGenerator.ts +1 -1
@@ -1,11 +1,15 @@
1
1
  import type { EntitySnapshot, LoadStatus } from './snapshots.js'
2
2
  import { createEntitySnapshot } from './snapshots.js'
3
3
  import type { FieldError, FieldErrorFilter } from '../errors/types.js'
4
+ import type { SelectionMeta } from '../selection/types.js'
4
5
  import { SubscriptionManager, type SnapshotVersionBumper, type SynchronousResult } from './SubscriptionManager.js'
5
6
  import { ErrorStore } from './ErrorStore.js'
6
7
  import {
7
8
  RelationStore,
9
+ type HasManyRelationProjection,
8
10
  type HasManyRemovalType,
11
+ type HasManyViewMembership,
12
+ type HasManyViewProjection,
9
13
  type RelationReconciliationResult,
10
14
  type SentHasManyDelta,
11
15
  type SentHasOneTransition,
@@ -18,6 +22,7 @@ import { generateTempId } from './entityId.js'
18
22
  import { DirtyTracker } from './DirtyTracker.js'
19
23
  import { EntitySnapshotStore } from './EntitySnapshotStore.js'
20
24
  import { RootRegistry } from './RootRegistry.js'
25
+ import { ResponseOccurrenceIndex, type RelationTargetResolver } from './ResponseOccurrenceIndex.js'
21
26
  import { ReachabilityAnalyzer } from './ReachabilityAnalyzer.js'
22
27
  import { RekeyOrchestrator } from './RekeyOrchestrator.js'
23
28
  import type { RekeyContext, Rekeyable } from './RekeyOrchestrator.js'
@@ -32,7 +37,13 @@ import type {
32
37
  } from '../undo/UndoJournal.js'
33
38
 
34
39
  export type {
40
+ HasManyAdditionKind,
41
+ HasManyRelationProjection,
35
42
  HasManyRemovalType,
43
+ HasManyView,
44
+ HasManyViewMembership,
45
+ HasManyViewProjection,
46
+ PlannedHasManyAddition,
36
47
  RelationReconciliationResult,
37
48
  SentHasManyAddition,
38
49
  SentHasManyDelta,
@@ -83,6 +94,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
83
94
  */
84
95
  private readonly lastPropagatedData = new Map<string, unknown>()
85
96
 
97
+ private readonly responseOccurrences = new ResponseOccurrenceIndex()
98
+
86
99
  /**
87
100
  * Optional write-journal. When set (by an attached UndoManager), mutating
88
101
  * methods record editable-layer pre-images of the cells they touch so a gesture
@@ -309,6 +322,23 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
309
322
  return newSnapshot as EntitySnapshot<T>
310
323
  }
311
324
 
325
+ /**
326
+ * Unites the occurrences of each entity within one server response, so the
327
+ * narrower of two occurrences cannot replace fields the wider one carried.
328
+ * Call it before any of the response is written. See {@link ResponseOccurrenceIndex}.
329
+ */
330
+ indexServerResponse(entityType: string, data: unknown, selection: SelectionMeta, schema: RelationTargetResolver): void {
331
+ this.responseOccurrences.index(entityType, data, selection, schema)
332
+ }
333
+
334
+ /**
335
+ * What to write for an entity occurrence of a server response: the union of
336
+ * its occurrences in that response, or the occurrence itself.
337
+ */
338
+ resolveServerOccurrence(occurrence: Record<string, unknown>): Record<string, unknown> {
339
+ return this.responseOccurrences.resolve(occurrence)
340
+ }
341
+
312
342
  /**
313
343
  * Refreshes server data from a revalidation read while preserving the user's
314
344
  * local dirty edits. See {@link EntitySnapshotStore.refreshServerData}.
@@ -530,25 +560,74 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
530
560
 
531
561
  // ==================== Has-Many State (delegated to RelationStore) ====================
532
562
 
563
+ /**
564
+ * Declares what a view read under {@link alias} can leave out, so the store can tell
565
+ * a filtered view from one that merely orders or renames. Only the caller that
566
+ * selected the relation knows the params; the alias is a hash of them.
567
+ */
568
+ declareHasManyViewMembership(
569
+ entityType: string,
570
+ fieldName: string,
571
+ alias: string,
572
+ membership: HasManyViewMembership,
573
+ ): void {
574
+ this.relations.declareHasManyViewMembership(entityType, fieldName, alias, membership)
575
+ }
576
+
577
+ /**
578
+ * Materializes the relation and one args-view of it, refreshing that view's
579
+ * server baseline when one is supplied.
580
+ *
581
+ * `alias` addresses a VIEW inside the relation state, never a key of its own:
582
+ * the state is always keyed by the schema field name, so everything that names
583
+ * the relation the way the schema and the server do finds it.
584
+ */
533
585
  getOrCreateHasMany(
534
586
  parentType: string,
535
587
  parentId: string,
536
588
  fieldName: string,
537
589
  serverIds?: string[],
538
590
  alias?: string,
539
- ): StoredHasManyState {
540
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
541
- return this.relations.getOrCreateHasMany(key, serverIds)
591
+ ): void {
592
+ const key = this.getRelationKey(parentType, parentId, fieldName)
593
+ this.relations.getOrCreateHasMany(key, alias ?? fieldName, serverIds)
542
594
  }
543
595
 
596
+ /** The relation as persistence and dirty tracking see it — server baseline unioned across views. */
544
597
  getHasMany(
545
598
  parentType: string,
546
599
  parentId: string,
547
600
  fieldName: string,
601
+ ): HasManyRelationProjection | undefined {
602
+ const key = this.getRelationKey(parentType, parentId, fieldName)
603
+ return this.relations.getHasManyRelation(key)
604
+ }
605
+
606
+ /** Whether the relation carries writes the next persist would send. */
607
+ hasPendingHasManyWrites(parentType: string, parentId: string, fieldName: string): boolean {
608
+ return this.relations.hasPendingHasManyWrites(this.getRelationKey(parentType, parentId, fieldName))
609
+ }
610
+
611
+ /** Whether one view carries an order its user arranged rather than the default one. */
612
+ hasExplicitHasManyOrder(
613
+ parentType: string,
614
+ parentId: string,
615
+ fieldName: string,
616
+ alias?: string,
617
+ ): boolean {
618
+ const key = this.getRelationKey(parentType, parentId, fieldName)
619
+ return this.relations.hasExplicitHasManyOrder(key, alias ?? fieldName)
620
+ }
621
+
622
+ /** The relation as ONE mounted has-many handle sees it. */
623
+ getHasManyView(
624
+ parentType: string,
625
+ parentId: string,
626
+ fieldName: string,
548
627
  alias?: string,
549
- ): StoredHasManyState | undefined {
550
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
551
- return this.relations.getHasMany(key)
628
+ ): HasManyViewProjection {
629
+ const key = this.getRelationKey(parentType, parentId, fieldName)
630
+ return this.relations.getHasManyView(key, alias ?? fieldName)
552
631
  }
553
632
 
554
633
  setHasManyServerIds(
@@ -558,8 +637,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
558
637
  serverIds: string[],
559
638
  alias?: string,
560
639
  ): void {
561
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
562
- this.relations.setHasManyServerIds(key, serverIds)
640
+ const key = this.getRelationKey(parentType, parentId, fieldName)
641
+ this.relations.setHasManyServerIds(key, alias ?? fieldName, serverIds)
563
642
  this.notifyRelationSubscribers(key)
564
643
  }
565
644
 
@@ -569,9 +648,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
569
648
  fieldName: string,
570
649
  itemId: string,
571
650
  type: HasManyRemovalType,
572
- alias?: string,
573
651
  ): void {
574
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
652
+ const key = this.getRelationKey(parentType, parentId, fieldName)
575
653
  this.journal?.recordHasMany(key)
576
654
  this.relations.planHasManyRemoval(key, itemId, type)
577
655
  this.notifyRelationSubscribers(key)
@@ -581,10 +659,9 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
581
659
  parentType: string,
582
660
  parentId: string,
583
661
  fieldName: string,
584
- alias?: string,
585
662
  ): Map<string, HasManyRemovalType> | undefined {
586
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
587
- return this.relations.getHasMany(key)?.plannedRemovals
663
+ const key = this.getRelationKey(parentType, parentId, fieldName)
664
+ return this.relations.getHasManyRelation(key)?.plannedRemovals
588
665
  }
589
666
 
590
667
  /**
@@ -603,34 +680,22 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
603
680
  itemId: string,
604
681
  alias?: string,
605
682
  ): void {
606
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
683
+ const key = this.getRelationKey(parentType, parentId, fieldName)
607
684
  this.journal?.recordHasMany(key)
608
- this.relations.planHasManyConnection(key, itemId)
685
+ this.relations.planHasManyConnection(key, alias ?? fieldName, itemId)
609
686
  this.notifyRelationSubscribers(key)
610
687
  }
611
688
 
689
+ /** The connections the next persist will send — relation-level, not per view. */
612
690
  getHasManyPlannedConnections(
613
691
  parentType: string,
614
692
  parentId: string,
615
693
  fieldName: string,
616
- alias?: string,
617
694
  ): Set<string> | undefined {
618
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
619
- const state = this.relations.getHasMany(key)
620
- if (!state) return undefined
621
- return new Set(state.plannedAdditions.keys())
622
- }
623
-
624
- commitHasMany(
625
- parentType: string,
626
- parentId: string,
627
- fieldName: string,
628
- newServerIds: string[],
629
- alias?: string,
630
- ): void {
631
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
632
- this.relations.commitHasMany(key, newServerIds)
633
- this.notifyRelationSubscribers(key)
695
+ const key = this.getRelationKey(parentType, parentId, fieldName)
696
+ const relation = this.relations.getHasManyRelation(key)
697
+ if (!relation) return undefined
698
+ return new Set(relation.plannedAdditions.keys())
634
699
  }
635
700
 
636
701
  reconcileSentHasMany(
@@ -638,9 +703,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
638
703
  parentId: string,
639
704
  fieldName: string,
640
705
  delta: SentHasManyDelta,
641
- alias?: string,
642
706
  ): RelationReconciliationResult {
643
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
707
+ const key = this.getRelationKey(parentType, parentId, fieldName)
644
708
  const result = this.relations.reconcileSentHasMany(key, delta)
645
709
  this.notifyRelationSubscribers(key)
646
710
  return result
@@ -650,9 +714,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
650
714
  parentType: string,
651
715
  parentId: string,
652
716
  fieldName: string,
653
- alias?: string,
654
717
  ): void {
655
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
718
+ const key = this.getRelationKey(parentType, parentId, fieldName)
656
719
  this.journal?.recordHasMany(key)
657
720
  this.relations.resetHasMany(key)
658
721
  this.notifyRelationSubscribers(key)
@@ -665,9 +728,9 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
665
728
  itemId: string,
666
729
  alias?: string,
667
730
  ): void {
668
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
731
+ const key = this.getRelationKey(parentType, parentId, fieldName)
669
732
  this.journal?.recordHasMany(key)
670
- this.relations.addToHasMany(key, itemId)
733
+ this.relations.addToHasMany(key, alias ?? fieldName, itemId)
671
734
  this.notifyRelationSubscribers(key)
672
735
  }
673
736
 
@@ -679,7 +742,7 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
679
742
  ): void {
680
743
  const key = this.getRelationKey(parentType, parentId, fieldName)
681
744
  this.journal?.recordHasMany(key)
682
- this.relations.connectExistingToHasMany(key, itemId)
745
+ this.relations.connectExistingToHasMany(key, fieldName, itemId)
683
746
  this.notifyRelationSubscribers(key)
684
747
  }
685
748
 
@@ -689,9 +752,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
689
752
  fieldName: string,
690
753
  itemId: string,
691
754
  removalType: HasManyRemovalType,
692
- alias?: string,
693
755
  ): void {
694
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
756
+ const key = this.getRelationKey(parentType, parentId, fieldName)
695
757
  this.journal?.recordHasMany(key)
696
758
  // Cancelling the add of a never-persisted child just removes it from the
697
759
  // list; its now-unreachable snapshot is no longer reported as a `create` and
@@ -709,9 +771,9 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
709
771
  toIndex: number,
710
772
  alias?: string,
711
773
  ): void {
712
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
774
+ const key = this.getRelationKey(parentType, parentId, fieldName)
713
775
  this.journal?.recordHasMany(key)
714
- this.relations.moveInHasMany(key, fromIndex, toIndex)
776
+ this.relations.moveInHasMany(key, alias ?? fieldName, fromIndex, toIndex)
715
777
  this.notifyRelationSubscribers(key)
716
778
  }
717
779
 
@@ -721,8 +783,8 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
721
783
  fieldName: string,
722
784
  alias?: string,
723
785
  ): string[] {
724
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
725
- return this.relations.getHasManyOrderedIds(key)
786
+ const key = this.getRelationKey(parentType, parentId, fieldName)
787
+ return this.relations.getHasManyOrderedIds(key, alias ?? fieldName)
726
788
  }
727
789
 
728
790
  getPresentationHasManyOrderedIds(
@@ -731,13 +793,13 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
731
793
  fieldName: string,
732
794
  alias?: string,
733
795
  ): string[] {
734
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
796
+ const key = this.getRelationKey(parentType, parentId, fieldName)
797
+ const view = alias ?? fieldName
735
798
  if (!this.meta.isPessimisticInFlight(this.getEntityKey(parentType, parentId))) {
736
- return this.relations.getHasManyOrderedIds(key)
799
+ return this.relations.getHasManyOrderedIds(key, view)
737
800
  }
738
801
 
739
- const state = this.relations.getHasMany(key)
740
- return state ? [...state.serverIds] : []
802
+ return [...this.relations.getHasManyView(key, view).serverIds]
741
803
  }
742
804
 
743
805
  isHasManyItemCreated(
@@ -745,24 +807,21 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
745
807
  parentId: string,
746
808
  fieldName: string,
747
809
  itemId: string,
748
- alias?: string,
749
810
  ): boolean {
750
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
751
- const state = this.relations.getHasMany(key)
752
- return state?.plannedAdditions.get(itemId) === 'created'
811
+ const key = this.getRelationKey(parentType, parentId, fieldName)
812
+ return this.relations.getHasManyRelation(key)?.plannedAdditions.get(itemId) === 'created'
753
813
  }
754
814
 
755
815
  getHasManyCreatedEntities(
756
816
  parentType: string,
757
817
  parentId: string,
758
818
  fieldName: string,
759
- alias?: string,
760
819
  ): Set<string> | undefined {
761
- const key = this.getRelationKey(parentType, parentId, alias ?? fieldName)
762
- const state = this.relations.getHasMany(key)
763
- if (!state) return undefined
820
+ const key = this.getRelationKey(parentType, parentId, fieldName)
821
+ const relation = this.relations.getHasManyRelation(key)
822
+ if (!relation) return undefined
764
823
  const created = new Set<string>()
765
- for (const [id, kind] of state.plannedAdditions) {
824
+ for (const [id, kind] of relation.plannedAdditions) {
766
825
  if (kind === 'created') created.add(id)
767
826
  }
768
827
  return created
@@ -1161,25 +1220,19 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
1161
1220
  * journal's rekey to rebase a pre-image when a just-persisted create became a
1162
1221
  * permanent member of the list (membership rebase for sealed creates).
1163
1222
  */
1164
- getLiveHasManyServerIds(relationKey: string): Set<string> {
1165
- const state = this.relations.getHasMany(relationKey)
1166
- return new Set(state?.serverIds ?? [])
1223
+ getLiveHasManyServerIds(relationKey: string): ReadonlyMap<string, ReadonlySet<string>> {
1224
+ return this.relations.collectHasManyViewServerIds(relationKey)
1167
1225
  }
1168
1226
 
1169
1227
  exportHasManyCell(key: string): HasManyCellImage {
1170
- const state = this.relations.getHasMany(key)
1228
+ const state = this.relations.getHasManyState(key)
1171
1229
  if (!state) return { kind: 'hasMany', key, present: false }
1172
1230
  return {
1173
1231
  kind: 'hasMany',
1174
1232
  key,
1175
1233
  present: true,
1176
- state: {
1177
- serverIds: new Set(state.serverIds),
1178
- orderedIds: state.orderedIds ? [...state.orderedIds] : null,
1179
- plannedRemovals: new Map(state.plannedRemovals),
1180
- plannedAdditions: new Map(state.plannedAdditions),
1181
- version: state.version,
1182
- },
1234
+ // getHasMany already returns a deep copy; the image owns it outright.
1235
+ state,
1183
1236
  }
1184
1237
  }
1185
1238
 
@@ -1218,7 +1271,9 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
1218
1271
  const image = this.exportHasManyCell(hasManyKey)
1219
1272
  images.push(image)
1220
1273
  if (image.state) {
1221
- if (image.state.orderedIds) worklist.push(...image.state.orderedIds)
1274
+ for (const view of image.state.views.values()) {
1275
+ if (view.orderedIds) worklist.push(...view.orderedIds)
1276
+ }
1222
1277
  for (const additionId of image.state.plannedAdditions.keys()) worklist.push(additionId)
1223
1278
  }
1224
1279
  }
@@ -1318,10 +1373,26 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
1318
1373
  }]]))
1319
1374
  } else {
1320
1375
  const s = img.state!
1321
- const live = this.relations.getHasMany(img.key)
1376
+ const live = this.relations.getHasManyState(img.key)
1377
+ // The editable layer (planned writes, manual ordering) comes from the image;
1378
+ // the server baseline always from the live state, which may have advanced
1379
+ // since the gesture. A view the image does not know did not exist before the
1380
+ // gesture — the gesture itself may have created it — so it had no arranged
1381
+ // order to restore and falls back to the default order of the restored writes.
1382
+ const views = new Map(live?.views ?? [])
1383
+ for (const [alias, view] of views) {
1384
+ const recorded = s.views.get(alias)
1385
+ views.set(alias, {
1386
+ serverIds: new Set(view.serverIds),
1387
+ orderedIds: recorded?.orderedIds ? [...recorded.orderedIds] : null,
1388
+ membership: view.membership,
1389
+ })
1390
+ }
1391
+ for (const [alias, recorded] of s.views) {
1392
+ if (!views.has(alias)) views.set(alias, recorded)
1393
+ }
1322
1394
  this.relations.importHasManyStates(new Map([[img.key, {
1323
- serverIds: new Set(live ? live.serverIds : s.serverIds),
1324
- orderedIds: s.orderedIds ? [...s.orderedIds] : null,
1395
+ views,
1325
1396
  plannedRemovals: new Map(s.plannedRemovals),
1326
1397
  plannedAdditions: new Map(s.plannedAdditions),
1327
1398
  version: (live?.version ?? s.version) + 1,