@contember/bindx 0.1.49 → 0.1.51
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/actions.d.ts +3 -3
- package/dist/core/actions.d.ts.map +1 -1
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/types.d.ts +11 -0
- package/dist/errors/types.d.ts.map +1 -1
- package/dist/handles/FieldHandle.d.ts +3 -3
- package/dist/handles/FieldHandle.d.ts.map +1 -1
- package/dist/handles/HasManyListHandle.d.ts +1 -0
- package/dist/handles/HasManyListHandle.d.ts.map +1 -1
- package/dist/handles/types.d.ts +9 -8
- package/dist/handles/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/BatchPersister.d.ts +6 -0
- package/dist/persistence/BatchPersister.d.ts.map +1 -1
- package/dist/store/ErrorStore.d.ts +3 -3
- package/dist/store/ErrorStore.d.ts.map +1 -1
- package/dist/store/HasManyStore.d.ts +10 -0
- package/dist/store/HasManyStore.d.ts.map +1 -1
- package/dist/store/HasOneStore.d.ts +10 -0
- package/dist/store/HasOneStore.d.ts.map +1 -1
- package/dist/store/PlannedDeleteIndex.d.ts +19 -0
- package/dist/store/PlannedDeleteIndex.d.ts.map +1 -0
- package/dist/store/RekeyOrchestrator.d.ts +2 -0
- package/dist/store/RekeyOrchestrator.d.ts.map +1 -1
- package/dist/store/RelationOwnerIndex.d.ts +8 -0
- package/dist/store/RelationOwnerIndex.d.ts.map +1 -0
- package/dist/store/RelationStore.d.ts +7 -0
- package/dist/store/RelationStore.d.ts.map +1 -1
- package/dist/store/SnapshotStore.d.ts +10 -2
- package/dist/store/SnapshotStore.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/core/ActionDispatcher.ts +1 -1
- package/src/core/actions.ts +4 -4
- package/src/errors/index.ts +2 -0
- package/src/errors/types.ts +17 -0
- package/src/handles/FieldHandle.ts +4 -4
- package/src/handles/HasManyListHandle.ts +5 -0
- package/src/handles/types.ts +18 -15
- package/src/index.ts +2 -0
- package/src/persistence/BatchPersister.ts +48 -0
- package/src/store/ErrorStore.ts +15 -12
- package/src/store/HasManyStore.ts +39 -3
- package/src/store/HasOneStore.ts +41 -3
- package/src/store/PlannedDeleteIndex.ts +33 -0
- package/src/store/RekeyOrchestrator.ts +7 -0
- package/src/store/RelationOwnerIndex.ts +31 -0
- package/src/store/RelationStore.ts +10 -0
- package/src/store/SnapshotStore.ts +17 -3
package/src/store/HasOneStore.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { HasOneRelationState } from '../handles/types.js'
|
|
2
2
|
import type { EntitySnapshot } from './snapshots.js'
|
|
3
3
|
import { parentKeyFromOwnerPrefix, parentKeyFromRelationKey } from './relationKey.js'
|
|
4
|
+
import { PlannedDeleteIndex } from './PlannedDeleteIndex.js'
|
|
4
5
|
import { RelationEdgeIndex } from './RelationEdgeIndex.js'
|
|
6
|
+
import { RelationOwnerIndex } from './RelationOwnerIndex.js'
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Relation state stored in SnapshotStore
|
|
@@ -40,6 +42,7 @@ function cloneRelationState(state: StoredRelationState): StoredRelationState {
|
|
|
40
42
|
export class HasOneStore {
|
|
41
43
|
/** Relation states keyed by "parentType:parentId:fieldName" */
|
|
42
44
|
private readonly relationStates = new Map<string, StoredRelationState>()
|
|
45
|
+
private readonly owners = new RelationOwnerIndex()
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Bidirectional live-edge index, maintained by {@link writeRelation} /
|
|
@@ -48,6 +51,12 @@ export class HasOneStore {
|
|
|
48
51
|
*/
|
|
49
52
|
private readonly edges = new RelationEdgeIndex()
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Targets this store plans to delete through their parent's mutation, maintained
|
|
56
|
+
* by {@link writeRelation} / {@link deleteRelation}, like {@link edges}.
|
|
57
|
+
*/
|
|
58
|
+
private readonly plannedDeletes = new PlannedDeleteIndex()
|
|
59
|
+
|
|
51
60
|
private mutationVersion = 0
|
|
52
61
|
|
|
53
62
|
/**
|
|
@@ -73,14 +82,17 @@ export class HasOneStore {
|
|
|
73
82
|
* tracking the reverse direction itself.
|
|
74
83
|
*/
|
|
75
84
|
private writeRelation(key: string, state: StoredRelationState): void {
|
|
76
|
-
const
|
|
85
|
+
const previous = this.relationStates.get(key)
|
|
86
|
+
const oldChild = liveHasOneChildId(previous)
|
|
77
87
|
const newChild = liveHasOneChildId(state)
|
|
78
88
|
this.relationStates.set(key, state)
|
|
89
|
+
if (!previous) this.owners.add(key)
|
|
79
90
|
if (oldChild !== newChild) {
|
|
80
91
|
const parentKey = parentKeyFromRelationKey(key)
|
|
81
92
|
if (oldChild !== null) this.edges.removeEdge(parentKey, oldChild)
|
|
82
93
|
if (newChild !== null) this.edges.addEdge(parentKey, newChild)
|
|
83
94
|
}
|
|
95
|
+
this.reconcilePlannedDelete(plannedDeleteChildId(previous), plannedDeleteChildId(state))
|
|
84
96
|
this.mutationVersion++
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -94,9 +106,23 @@ export class HasOneStore {
|
|
|
94
106
|
const child = liveHasOneChildId(existing)
|
|
95
107
|
if (child !== null) this.edges.removeEdge(parentKeyFromRelationKey(key), child)
|
|
96
108
|
this.relationStates.delete(key)
|
|
109
|
+
this.owners.delete(key)
|
|
110
|
+
this.reconcilePlannedDelete(plannedDeleteChildId(existing), null)
|
|
97
111
|
this.mutationVersion++
|
|
98
112
|
}
|
|
99
113
|
|
|
114
|
+
/** Applies one relation's planned-delete diff to the refcounted index. */
|
|
115
|
+
private reconcilePlannedDelete(previous: string | null, next: string | null): void {
|
|
116
|
+
if (previous === next) return
|
|
117
|
+
if (previous !== null) this.plannedDeletes.release(previous)
|
|
118
|
+
if (next !== null) this.plannedDeletes.retain(next)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether this store plans to delete {@link childId} through its parent. */
|
|
122
|
+
isPlannedForDelete(childId: string): boolean {
|
|
123
|
+
return this.plannedDeletes.has(childId)
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
/**
|
|
101
127
|
* Gets or creates relation state.
|
|
102
128
|
*/
|
|
@@ -289,8 +315,8 @@ export class HasOneStore {
|
|
|
289
315
|
* Collects the field names of dirty has-one relations for an entity.
|
|
290
316
|
*/
|
|
291
317
|
collectDirtyRelations(keyPrefix: string, dirtyRelations: string[]): void {
|
|
292
|
-
for (const
|
|
293
|
-
|
|
318
|
+
for (const key of this.owners.get(parentKeyFromOwnerPrefix(keyPrefix))) {
|
|
319
|
+
const state = this.relationStates.get(key)!
|
|
294
320
|
const fieldName = key.slice(keyPrefix.length)
|
|
295
321
|
|
|
296
322
|
if (
|
|
@@ -375,7 +401,9 @@ export class HasOneStore {
|
|
|
375
401
|
*/
|
|
376
402
|
clear(): void {
|
|
377
403
|
this.relationStates.clear()
|
|
404
|
+
this.owners.clear()
|
|
378
405
|
this.edges.clear()
|
|
406
|
+
this.plannedDeletes.clear()
|
|
379
407
|
this.mutationVersion++
|
|
380
408
|
}
|
|
381
409
|
}
|
|
@@ -390,6 +418,16 @@ function liveHasOneChildId(state: StoredRelationState | undefined): string | nul
|
|
|
390
418
|
return state.currentId !== null && state.state !== 'deleted' ? state.currentId : null
|
|
391
419
|
}
|
|
392
420
|
|
|
421
|
+
/**
|
|
422
|
+
* The id the parent's `{ delete: true }` removes — `serverId`, not `currentId`,
|
|
423
|
+
* because that is the row MutationCollector deletes for a `deleted` relation, and a
|
|
424
|
+
* null serverId emits no delete at all.
|
|
425
|
+
*/
|
|
426
|
+
function plannedDeleteChildId(state: StoredRelationState | undefined): string | null {
|
|
427
|
+
if (!state || state.state !== 'deleted') return null
|
|
428
|
+
return state.serverId
|
|
429
|
+
}
|
|
430
|
+
|
|
393
431
|
interface HasOneReconciliation {
|
|
394
432
|
state: StoredRelationState
|
|
395
433
|
result: ReconciliationResult
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refcounted multiset of entity ids whose row a parent relation plans to delete
|
|
3
|
+
* through its own mutation ({@link HasManyStore} `plannedRemovals` of type `delete`,
|
|
4
|
+
* {@link HasOneStore} state `deleted`).
|
|
5
|
+
*
|
|
6
|
+
* Refcounted because more than one relation may plan the same child's deletion, so
|
|
7
|
+
* the id stays planned until the last of them drops it. Like {@link RelationEdgeIndex},
|
|
8
|
+
* the index knows nothing about the rules: turning relation state into a planned-delete
|
|
9
|
+
* id is the owning sub-store's job, done once per write in its chokepoint by diffing
|
|
10
|
+
* the previous state against the next.
|
|
11
|
+
*/
|
|
12
|
+
export class PlannedDeleteIndex {
|
|
13
|
+
private readonly counts = new Map<string, number>()
|
|
14
|
+
|
|
15
|
+
retain(childId: string): void {
|
|
16
|
+
this.counts.set(childId, (this.counts.get(childId) ?? 0) + 1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
release(childId: string): void {
|
|
20
|
+
const count = this.counts.get(childId)
|
|
21
|
+
if (count === undefined) return
|
|
22
|
+
if (count > 1) this.counts.set(childId, count - 1)
|
|
23
|
+
else this.counts.delete(childId)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
has(childId: string): boolean {
|
|
27
|
+
return this.counts.has(childId)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
clear(): void {
|
|
31
|
+
this.counts.clear()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -42,6 +42,7 @@ export interface Rekeyable {
|
|
|
42
42
|
export class RekeyOrchestrator {
|
|
43
43
|
/** "entityType:tempId" → persistedId. The single identity-redirect map. */
|
|
44
44
|
private readonly tempToPersisted = new Map<string, string>()
|
|
45
|
+
private mutationVersion = 0
|
|
45
46
|
private readonly participants: Rekeyable[] = []
|
|
46
47
|
private readonly participantSet = new Set<Rekeyable>()
|
|
47
48
|
|
|
@@ -66,6 +67,10 @@ export class RekeyOrchestrator {
|
|
|
66
67
|
return persisted !== undefined ? `${entityType}:${persisted}` : `${entityType}:${id}`
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
getMutationVersion(): number {
|
|
71
|
+
return this.mutationVersion
|
|
72
|
+
}
|
|
73
|
+
|
|
69
74
|
/** Resolves an entity id to its persisted id if it has been rekeyed. */
|
|
70
75
|
resolveId(entityType: string, id: string): string {
|
|
71
76
|
return this.tempToPersisted.get(`${entityType}:${id}`) ?? id
|
|
@@ -111,6 +116,7 @@ export class RekeyOrchestrator {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
this.tempToPersisted.set(ctx.oldKey, persistedId)
|
|
119
|
+
this.mutationVersion++
|
|
114
120
|
|
|
115
121
|
for (const participant of this.participants) {
|
|
116
122
|
participant.rekey(ctx)
|
|
@@ -119,5 +125,6 @@ export class RekeyOrchestrator {
|
|
|
119
125
|
|
|
120
126
|
clear(): void {
|
|
121
127
|
this.tempToPersisted.clear()
|
|
128
|
+
this.mutationVersion++
|
|
122
129
|
}
|
|
123
130
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parentKeyFromRelationKey } from './relationKey.js'
|
|
2
|
+
|
|
3
|
+
export class RelationOwnerIndex {
|
|
4
|
+
private readonly keysByOwner = new Map<string, Set<string>>()
|
|
5
|
+
|
|
6
|
+
add(key: string): void {
|
|
7
|
+
const owner = parentKeyFromRelationKey(key)
|
|
8
|
+
let keys = this.keysByOwner.get(owner)
|
|
9
|
+
if (!keys) {
|
|
10
|
+
keys = new Set()
|
|
11
|
+
this.keysByOwner.set(owner, keys)
|
|
12
|
+
}
|
|
13
|
+
keys.add(key)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
delete(key: string): void {
|
|
17
|
+
const owner = parentKeyFromRelationKey(key)
|
|
18
|
+
const keys = this.keysByOwner.get(owner)
|
|
19
|
+
if (!keys) return
|
|
20
|
+
keys.delete(key)
|
|
21
|
+
if (keys.size === 0) this.keysByOwner.delete(owner)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get(owner: string): Iterable<string> {
|
|
25
|
+
return this.keysByOwner.get(owner) ?? []
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
clear(): void {
|
|
29
|
+
this.keysByOwner.clear()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -158,6 +158,16 @@ export class RelationStore implements Rekeyable {
|
|
|
158
158
|
return this.hasMany.getHasManyOrderedIds(key)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Whether some relation plans to delete {@link childId} through its parent's
|
|
163
|
+
* mutation — a has-many `delete` removal or a has-one target marked `deleted`.
|
|
164
|
+
* Such a row goes away with the parent's update, so the child must not be
|
|
165
|
+
* written on its own (see issue #91).
|
|
166
|
+
*/
|
|
167
|
+
isPlannedForDeleteByParent(childId: string): boolean {
|
|
168
|
+
return this.hasOne.isPlannedForDelete(childId) || this.hasMany.isPlannedForDelete(childId)
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
// ==================== Reachability / Reverse Lookup ====================
|
|
162
172
|
|
|
163
173
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntitySnapshot, LoadStatus } from './snapshots.js'
|
|
2
2
|
import { createEntitySnapshot } from './snapshots.js'
|
|
3
|
-
import type { FieldError } from '../errors/types.js'
|
|
3
|
+
import type { FieldError, FieldErrorFilter } from '../errors/types.js'
|
|
4
4
|
import { SubscriptionManager, type SnapshotVersionBumper } from './SubscriptionManager.js'
|
|
5
5
|
import { ErrorStore } from './ErrorStore.js'
|
|
6
6
|
import {
|
|
@@ -194,6 +194,11 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
|
|
|
194
194
|
return this.rekeyOrchestrator.resolveId(entityType, id)
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/** Identity caches only need reconciliation when redirects change, including on clear. */
|
|
198
|
+
getRekeyVersion(): number {
|
|
199
|
+
return this.rekeyOrchestrator.getMutationVersion()
|
|
200
|
+
}
|
|
201
|
+
|
|
197
202
|
/** Attaches identity-keyed state to the store's single rekey fan-out. */
|
|
198
203
|
attachRekeyParticipant(participant: Rekeyable): void {
|
|
199
204
|
this.rekeyOrchestrator.registerParticipant(participant)
|
|
@@ -577,6 +582,15 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
|
|
|
577
582
|
return this.relations.getHasMany(key)?.plannedRemovals
|
|
578
583
|
}
|
|
579
584
|
|
|
585
|
+
/**
|
|
586
|
+
* Whether some relation plans to delete {@link entityId} through its parent, i.e.
|
|
587
|
+
* the row goes away with the parent's update rather than with a mutation of its
|
|
588
|
+
* own (see issue #91).
|
|
589
|
+
*/
|
|
590
|
+
isPlannedForDeleteByParent(entityId: string): boolean {
|
|
591
|
+
return this.relations.isPlannedForDeleteByParent(entityId)
|
|
592
|
+
}
|
|
593
|
+
|
|
580
594
|
planHasManyConnection(
|
|
581
595
|
parentType: string,
|
|
582
596
|
parentId: string,
|
|
@@ -814,10 +828,10 @@ export class SnapshotStore implements SnapshotVersionBumper, JournalTarget {
|
|
|
814
828
|
entityType: string,
|
|
815
829
|
id: string,
|
|
816
830
|
fieldName: string,
|
|
817
|
-
|
|
831
|
+
filter?: FieldErrorFilter,
|
|
818
832
|
): void {
|
|
819
833
|
const key = this.getRelationKey(entityType, id, fieldName)
|
|
820
|
-
this.errors.clearFieldErrors(key,
|
|
834
|
+
this.errors.clearFieldErrors(key, filter)
|
|
821
835
|
this.notifyEntitySubscribers(this.getEntityKey(entityType, id))
|
|
822
836
|
}
|
|
823
837
|
|