zerodrift 1.0.0
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/LICENSE +21 -0
- package/README.md +358 -0
- package/dist/core/BaseModel.d.ts +76 -0
- package/dist/core/BaseModel.js +505 -0
- package/dist/core/BaseSSEConnection.d.ts +31 -0
- package/dist/core/BaseSSEConnection.js +91 -0
- package/dist/core/BatchModelLoader.d.ts +27 -0
- package/dist/core/BatchModelLoader.js +70 -0
- package/dist/core/CompoundIndexFetcher.d.ts +46 -0
- package/dist/core/CompoundIndexFetcher.js +177 -0
- package/dist/core/Database.d.ts +303 -0
- package/dist/core/Database.js +837 -0
- package/dist/core/LazyCollection.d.ts +168 -0
- package/dist/core/LazyCollection.js +403 -0
- package/dist/core/LazyOwnedCollection.d.ts +35 -0
- package/dist/core/LazyOwnedCollection.js +66 -0
- package/dist/core/MemoryAdapter.d.ts +67 -0
- package/dist/core/MemoryAdapter.js +243 -0
- package/dist/core/ModelRegistry.d.ts +64 -0
- package/dist/core/ModelRegistry.js +217 -0
- package/dist/core/ModelStream.d.ts +33 -0
- package/dist/core/ModelStream.js +68 -0
- package/dist/core/ObjectPool.d.ts +113 -0
- package/dist/core/ObjectPool.js +339 -0
- package/dist/core/Store.d.ts +40 -0
- package/dist/core/Store.js +73 -0
- package/dist/core/StoreManager.d.ts +839 -0
- package/dist/core/StoreManager.js +2034 -0
- package/dist/core/SyncConnection.d.ts +105 -0
- package/dist/core/SyncConnection.js +348 -0
- package/dist/core/Transaction.d.ts +114 -0
- package/dist/core/Transaction.js +147 -0
- package/dist/core/TransactionQueue.d.ts +110 -0
- package/dist/core/TransactionQueue.js +601 -0
- package/dist/core/decorators.d.ts +66 -0
- package/dist/core/decorators.js +278 -0
- package/dist/core/hash.d.ts +6 -0
- package/dist/core/hash.js +12 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +18 -0
- package/dist/core/internal.d.ts +27 -0
- package/dist/core/internal.js +25 -0
- package/dist/core/observability.d.ts +21 -0
- package/dist/core/observability.js +66 -0
- package/dist/core/refAccessors.d.ts +43 -0
- package/dist/core/refAccessors.js +80 -0
- package/dist/core/serializers.d.ts +2 -0
- package/dist/core/serializers.js +2 -0
- package/dist/core/types.d.ts +320 -0
- package/dist/core/types.js +84 -0
- package/dist/react/index.d.ts +82 -0
- package/dist/react/index.js +373 -0
- package/dist/schema/builders.d.ts +29 -0
- package/dist/schema/builders.js +81 -0
- package/dist/schema/compile.d.ts +28 -0
- package/dist/schema/compile.js +334 -0
- package/dist/schema/createStore.d.ts +235 -0
- package/dist/schema/createStore.js +264 -0
- package/dist/schema/extend.d.ts +46 -0
- package/dist/schema/extend.js +6 -0
- package/dist/schema/index.d.ts +13 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/infer.d.ts +102 -0
- package/dist/schema/infer.js +1 -0
- package/dist/schema/types.d.ts +76 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/zod.d.ts +90 -0
- package/dist/schema/zod.js +101 -0
- package/package.json +99 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectPool — the in-memory cache of all hydrated model instances.
|
|
3
|
+
*
|
|
4
|
+
* Structure: Map<modelName, Map<uuid, modelInstance>>
|
|
5
|
+
*
|
|
6
|
+
* This is what @Reference getters resolve against when you access
|
|
7
|
+
* `issue.assignee` — it looks up the User by ID in this pool.
|
|
8
|
+
*
|
|
9
|
+
* Subscription system:
|
|
10
|
+
* React hooks subscribe to specific model types. When a delta packet
|
|
11
|
+
* adds, updates, or removes an instance of that type, all subscribers
|
|
12
|
+
* are notified and their components re-render.
|
|
13
|
+
*/
|
|
14
|
+
import type { BaseModel } from "./BaseModel";
|
|
15
|
+
import { type ModelMeta } from "./types";
|
|
16
|
+
type Listener = () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Read a dynamic property off a model instance. The single bridge between
|
|
19
|
+
* the typed `BaseModel` shape and the index-string access we need for
|
|
20
|
+
* runtime field reflection (covering paths, predicate filters, etc.).
|
|
21
|
+
*/
|
|
22
|
+
export declare function prop(model: BaseModel, key: string): unknown;
|
|
23
|
+
/** Read a dynamic FK property off a model instance, or null if missing/empty. */
|
|
24
|
+
export declare function readFk(instance: BaseModel, key: string): string | null;
|
|
25
|
+
export declare class ObjectPool {
|
|
26
|
+
private pool;
|
|
27
|
+
private snapshotCache;
|
|
28
|
+
/**
|
|
29
|
+
* Subscribers per model type. When the pool changes for a given type,
|
|
30
|
+
* all listeners for that type are called, triggering React re-renders.
|
|
31
|
+
*/
|
|
32
|
+
private listeners;
|
|
33
|
+
/**
|
|
34
|
+
* Per-`(modelName, id)` MobX atoms. Each atom is bumped when the entry
|
|
35
|
+
* is added, removed, or replaced — bridging pool identity changes into
|
|
36
|
+
* the MobX dependency graph. The `@Reference` getter calls `trackModel`
|
|
37
|
+
* to register a tracked read, so observers wake when the underlying
|
|
38
|
+
* pool entry transitions even if the holder's foreign key didn't change.
|
|
39
|
+
* Atoms are created lazily on first observation and dropped automatically
|
|
40
|
+
* when no observer remains.
|
|
41
|
+
*/
|
|
42
|
+
private modelAtoms;
|
|
43
|
+
/**
|
|
44
|
+
* Register a tracked MobX dependency on the pool entry for `(modelName, id)`.
|
|
45
|
+
* Bumped from `put` (when the entry is new) and `remove`, so observers
|
|
46
|
+
* reading the entry through `@Reference` re-run on identity changes — not
|
|
47
|
+
* just on FK changes.
|
|
48
|
+
*/
|
|
49
|
+
trackModel(modelName: string, id: string): void;
|
|
50
|
+
/** Bump the atom for `(modelName, id)` if any observer is currently tracking it. */
|
|
51
|
+
private notifyModelChanged;
|
|
52
|
+
/**
|
|
53
|
+
* Subscribe to changes for a model type. The optional `predicate` runs
|
|
54
|
+
* against the affected record on `put` / `remove` and the listener only
|
|
55
|
+
* fires when it returns true; `clear` always fires every listener since
|
|
56
|
+
* the affected record is gone by definition.
|
|
57
|
+
*
|
|
58
|
+
* Predicate filtering covers **set-membership changes** (a record was
|
|
59
|
+
* added or removed). It does NOT see field-level reassignments — a child
|
|
60
|
+
* moving between FK buckets via `child.teamId = "..."` goes through MobX
|
|
61
|
+
* boxes, not `notify`. Pair with `record.watch` if you need to react to
|
|
62
|
+
* field changes that cross a filter boundary.
|
|
63
|
+
*
|
|
64
|
+
* Returns an unsubscribe function.
|
|
65
|
+
*/
|
|
66
|
+
subscribe(modelName: string, listener: Listener): () => void;
|
|
67
|
+
subscribe(modelName: string, predicate: (model: BaseModel) => boolean, listener: Listener): () => void;
|
|
68
|
+
private notify;
|
|
69
|
+
getById<T extends BaseModel = BaseModel>(modelName: string, id: string): T | undefined;
|
|
70
|
+
/** Store a model instance. Notifies subscribers. */
|
|
71
|
+
put(modelName: string, instance: BaseModel): void;
|
|
72
|
+
/** Remove a model. Notifies subscribers. */
|
|
73
|
+
remove(modelName: string, id: string): void;
|
|
74
|
+
getAll<T extends BaseModel = BaseModel>(modelName: string): T[];
|
|
75
|
+
get size(): number;
|
|
76
|
+
counts(): Record<string, number>;
|
|
77
|
+
/**
|
|
78
|
+
* Create an instance from raw data, hydrate it, make it observable,
|
|
79
|
+
* and add it to the pool. Used everywhere a new model arrives from
|
|
80
|
+
* the server or IDB.
|
|
81
|
+
*/
|
|
82
|
+
hydrateAndPut(modelName: string, meta: ModelMeta, data: Record<string, unknown>): BaseModel;
|
|
83
|
+
clear(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Memoized parent-side declarations targeting a given child model. The
|
|
86
|
+
* registry is decorator-load-time only, so the cache lives for the pool's
|
|
87
|
+
* lifetime.
|
|
88
|
+
*/
|
|
89
|
+
private inverseDeclCache;
|
|
90
|
+
/** Set of FK property names that are an `inverseOf` on some parent's decl.
|
|
91
|
+
* Lets `notifyReferenceChange` skip the work for plain property writes
|
|
92
|
+
* (`title`, `done`, ...) without iterating decls. */
|
|
93
|
+
private inverseFkCache;
|
|
94
|
+
private inverseDeclarations;
|
|
95
|
+
private inverseFkNames;
|
|
96
|
+
/** Resolve the parent's runtime collection / back-ref for an inverse decl. */
|
|
97
|
+
private inverseTarget;
|
|
98
|
+
private attachInverseLinks;
|
|
99
|
+
private detachInverseLinks;
|
|
100
|
+
/**
|
|
101
|
+
* When a parent enters the pool after its children, seed each declared
|
|
102
|
+
* collection / back-ref from children already in the pool. Counterpart
|
|
103
|
+
* to `attachInverseLinks` on the parent side.
|
|
104
|
+
*/
|
|
105
|
+
private populateOwnedCollectionsFromPool;
|
|
106
|
+
/**
|
|
107
|
+
* Called by BaseModel when a child's foreign-key property changes — either
|
|
108
|
+
* via the prototype setter or via `box.set` in hydrate. Detaches from the
|
|
109
|
+
* old parent and attaches to the new one in a single batched action.
|
|
110
|
+
*/
|
|
111
|
+
notifyReferenceChange(child: BaseModel, childModelName: string, fkName: string, oldId: string | null, newId: string | null): void;
|
|
112
|
+
}
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectPool — the in-memory cache of all hydrated model instances.
|
|
3
|
+
*
|
|
4
|
+
* Structure: Map<modelName, Map<uuid, modelInstance>>
|
|
5
|
+
*
|
|
6
|
+
* This is what @Reference getters resolve against when you access
|
|
7
|
+
* `issue.assignee` — it looks up the User by ID in this pool.
|
|
8
|
+
*
|
|
9
|
+
* Subscription system:
|
|
10
|
+
* React hooks subscribe to specific model types. When a delta packet
|
|
11
|
+
* adds, updates, or removes an instance of that type, all subscribers
|
|
12
|
+
* are notified and their components re-render.
|
|
13
|
+
*/
|
|
14
|
+
import { createAtom, runInAction } from "mobx";
|
|
15
|
+
import { ModelRegistry } from "./ModelRegistry";
|
|
16
|
+
import { PropertyType } from "./types";
|
|
17
|
+
/**
|
|
18
|
+
* Read a dynamic property off a model instance. The single bridge between
|
|
19
|
+
* the typed `BaseModel` shape and the index-string access we need for
|
|
20
|
+
* runtime field reflection (covering paths, predicate filters, etc.).
|
|
21
|
+
*/
|
|
22
|
+
export function prop(model, key) {
|
|
23
|
+
return model[key];
|
|
24
|
+
}
|
|
25
|
+
/** Read a dynamic FK property off a model instance, or null if missing/empty. */
|
|
26
|
+
export function readFk(instance, key) {
|
|
27
|
+
const v = prop(instance, key);
|
|
28
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
29
|
+
}
|
|
30
|
+
export class ObjectPool {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.pool = new Map();
|
|
33
|
+
this.snapshotCache = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Subscribers per model type. When the pool changes for a given type,
|
|
36
|
+
* all listeners for that type are called, triggering React re-renders.
|
|
37
|
+
*/
|
|
38
|
+
this.listeners = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Per-`(modelName, id)` MobX atoms. Each atom is bumped when the entry
|
|
41
|
+
* is added, removed, or replaced — bridging pool identity changes into
|
|
42
|
+
* the MobX dependency graph. The `@Reference` getter calls `trackModel`
|
|
43
|
+
* to register a tracked read, so observers wake when the underlying
|
|
44
|
+
* pool entry transitions even if the holder's foreign key didn't change.
|
|
45
|
+
* Atoms are created lazily on first observation and dropped automatically
|
|
46
|
+
* when no observer remains.
|
|
47
|
+
*/
|
|
48
|
+
this.modelAtoms = new Map();
|
|
49
|
+
// ── Inverse link maintenance ──────────────────────────────────────────────
|
|
50
|
+
//
|
|
51
|
+
// Children push themselves into their parents' RefCollection / BackRef as
|
|
52
|
+
// they enter and leave the pool — no manual invalidation, no re-query.
|
|
53
|
+
// Walked from the parent side so plain @Property foreign keys work the
|
|
54
|
+
// same as @Reference-typed ones. Mirrors how Linear's framework does
|
|
55
|
+
// inverse attachment from the child's setter path.
|
|
56
|
+
/**
|
|
57
|
+
* Memoized parent-side declarations targeting a given child model. The
|
|
58
|
+
* registry is decorator-load-time only, so the cache lives for the pool's
|
|
59
|
+
* lifetime.
|
|
60
|
+
*/
|
|
61
|
+
this.inverseDeclCache = new Map();
|
|
62
|
+
/** Set of FK property names that are an `inverseOf` on some parent's decl.
|
|
63
|
+
* Lets `notifyReferenceChange` skip the work for plain property writes
|
|
64
|
+
* (`title`, `done`, ...) without iterating decls. */
|
|
65
|
+
this.inverseFkCache = new Map();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Register a tracked MobX dependency on the pool entry for `(modelName, id)`.
|
|
69
|
+
* Bumped from `put` (when the entry is new) and `remove`, so observers
|
|
70
|
+
* reading the entry through `@Reference` re-run on identity changes — not
|
|
71
|
+
* just on FK changes.
|
|
72
|
+
*/
|
|
73
|
+
trackModel(modelName, id) {
|
|
74
|
+
const key = `${modelName}:${id}`;
|
|
75
|
+
let atom = this.modelAtoms.get(key);
|
|
76
|
+
const wasJustCreated = atom == null;
|
|
77
|
+
if (atom == null) {
|
|
78
|
+
atom = createAtom(key, undefined, () => this.modelAtoms.delete(key));
|
|
79
|
+
this.modelAtoms.set(key, atom);
|
|
80
|
+
}
|
|
81
|
+
// reportObserved returns true iff a derivation is currently tracking. If
|
|
82
|
+
// we created the atom for a non-reactive read (event handler, JSON walk,
|
|
83
|
+
// etc.) the onUnobserved callback won't fire later, so drop the entry now
|
|
84
|
+
// to keep modelAtoms bounded.
|
|
85
|
+
if (!atom.reportObserved() && wasJustCreated) {
|
|
86
|
+
this.modelAtoms.delete(key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Bump the atom for `(modelName, id)` if any observer is currently tracking it. */
|
|
90
|
+
notifyModelChanged(modelName, id) {
|
|
91
|
+
this.modelAtoms.get(`${modelName}:${id}`)?.reportChanged();
|
|
92
|
+
}
|
|
93
|
+
subscribe(modelName, a, b) {
|
|
94
|
+
const sub = b != null
|
|
95
|
+
? { predicate: a, listener: b }
|
|
96
|
+
: { listener: a };
|
|
97
|
+
if (!this.listeners.has(modelName)) {
|
|
98
|
+
this.listeners.set(modelName, new Set());
|
|
99
|
+
}
|
|
100
|
+
this.listeners.get(modelName).add(sub);
|
|
101
|
+
return () => {
|
|
102
|
+
this.listeners.get(modelName)?.delete(sub);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
notify(modelName, affected) {
|
|
106
|
+
const subs = this.listeners.get(modelName);
|
|
107
|
+
if (subs == null) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const sub of subs) {
|
|
111
|
+
if (sub.predicate != null && affected != null && !sub.predicate(affected)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
sub.listener();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ── Core operations (notify on mutation) ──────────────────────────────────
|
|
118
|
+
getById(modelName, id) {
|
|
119
|
+
return this.pool.get(modelName)?.get(id);
|
|
120
|
+
}
|
|
121
|
+
/** Store a model instance. Notifies subscribers. */
|
|
122
|
+
put(modelName, instance) {
|
|
123
|
+
if (!this.pool.has(modelName)) {
|
|
124
|
+
this.pool.set(modelName, new Map());
|
|
125
|
+
}
|
|
126
|
+
const bucket = this.pool.get(modelName);
|
|
127
|
+
const wasNew = !bucket.has(instance.id);
|
|
128
|
+
bucket.set(instance.id, instance);
|
|
129
|
+
instance.store = this;
|
|
130
|
+
this.snapshotCache.delete(modelName);
|
|
131
|
+
// First-time entry: wire inverse links and bump the per-id atom. Re-puts
|
|
132
|
+
// for an in-place hydrate skip this — the model's own MobX boxes already
|
|
133
|
+
// report their property changes, so observers don't need a duplicate poke.
|
|
134
|
+
if (wasNew) {
|
|
135
|
+
runInAction(() => {
|
|
136
|
+
this.attachInverseLinks(modelName, instance);
|
|
137
|
+
this.populateOwnedCollectionsFromPool(modelName, instance);
|
|
138
|
+
this.notifyModelChanged(modelName, instance.id);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
this.notify(modelName, instance);
|
|
142
|
+
}
|
|
143
|
+
/** Remove a model. Notifies subscribers. */
|
|
144
|
+
remove(modelName, id) {
|
|
145
|
+
const instance = this.pool.get(modelName)?.get(id);
|
|
146
|
+
runInAction(() => {
|
|
147
|
+
if (instance != null) {
|
|
148
|
+
this.detachInverseLinks(modelName, instance);
|
|
149
|
+
}
|
|
150
|
+
this.pool.get(modelName)?.delete(id);
|
|
151
|
+
this.snapshotCache.delete(modelName);
|
|
152
|
+
this.notifyModelChanged(modelName, id);
|
|
153
|
+
});
|
|
154
|
+
this.notify(modelName, instance);
|
|
155
|
+
}
|
|
156
|
+
getAll(modelName) {
|
|
157
|
+
let snapshot = this.snapshotCache.get(modelName);
|
|
158
|
+
if (snapshot === undefined) {
|
|
159
|
+
const bucket = this.pool.get(modelName);
|
|
160
|
+
snapshot = bucket != null ? [...bucket.values()] : [];
|
|
161
|
+
this.snapshotCache.set(modelName, snapshot);
|
|
162
|
+
}
|
|
163
|
+
return snapshot;
|
|
164
|
+
}
|
|
165
|
+
get size() {
|
|
166
|
+
let total = 0;
|
|
167
|
+
for (const bucket of this.pool.values()) {
|
|
168
|
+
total += bucket.size;
|
|
169
|
+
}
|
|
170
|
+
return total;
|
|
171
|
+
}
|
|
172
|
+
counts() {
|
|
173
|
+
const out = {};
|
|
174
|
+
for (const [name, bucket] of this.pool) {
|
|
175
|
+
out[name] = bucket.size;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Create an instance from raw data, hydrate it, make it observable,
|
|
181
|
+
* and add it to the pool. Used everywhere a new model arrives from
|
|
182
|
+
* the server or IDB.
|
|
183
|
+
*/
|
|
184
|
+
hydrateAndPut(modelName, meta, data) {
|
|
185
|
+
const id = data.id;
|
|
186
|
+
if (id != null) {
|
|
187
|
+
const existing = this.getById(modelName, id);
|
|
188
|
+
if (existing != null) {
|
|
189
|
+
existing.hydrate(data);
|
|
190
|
+
return existing;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const inst = new meta.ctor();
|
|
194
|
+
inst.hydrate(data);
|
|
195
|
+
inst.makeModelObservable();
|
|
196
|
+
this.put(modelName, inst);
|
|
197
|
+
return inst;
|
|
198
|
+
}
|
|
199
|
+
clear() {
|
|
200
|
+
const names = [...this.pool.keys()];
|
|
201
|
+
// Snapshot atoms before iterating: reportChanged can disturb the map via
|
|
202
|
+
// onUnobserved as observers detach.
|
|
203
|
+
const atoms = [...this.modelAtoms.values()];
|
|
204
|
+
this.pool.clear();
|
|
205
|
+
this.snapshotCache.clear();
|
|
206
|
+
for (const atom of atoms) {
|
|
207
|
+
atom.reportChanged();
|
|
208
|
+
}
|
|
209
|
+
names.forEach((n) => this.notify(n));
|
|
210
|
+
}
|
|
211
|
+
inverseDeclarations(childModelName) {
|
|
212
|
+
let cached = this.inverseDeclCache.get(childModelName);
|
|
213
|
+
if (cached != null) {
|
|
214
|
+
return cached;
|
|
215
|
+
}
|
|
216
|
+
cached = [];
|
|
217
|
+
for (const parentMeta of ModelRegistry.allModels()) {
|
|
218
|
+
for (const [propName, propMeta] of parentMeta.properties) {
|
|
219
|
+
if (propMeta.referenceTo !== childModelName) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (propMeta.inverseOf == null) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (propMeta.type !== PropertyType.ReferenceCollection &&
|
|
226
|
+
propMeta.type !== PropertyType.BackReference) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
cached.push({
|
|
230
|
+
parentModelName: parentMeta.name,
|
|
231
|
+
parentPropName: propName,
|
|
232
|
+
fkName: propMeta.inverseOf,
|
|
233
|
+
kind: propMeta.type,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.inverseDeclCache.set(childModelName, cached);
|
|
238
|
+
return cached;
|
|
239
|
+
}
|
|
240
|
+
inverseFkNames(childModelName) {
|
|
241
|
+
let cached = this.inverseFkCache.get(childModelName);
|
|
242
|
+
if (cached != null) {
|
|
243
|
+
return cached;
|
|
244
|
+
}
|
|
245
|
+
cached = new Set(this.inverseDeclarations(childModelName).map((d) => d.fkName));
|
|
246
|
+
this.inverseFkCache.set(childModelName, cached);
|
|
247
|
+
return cached;
|
|
248
|
+
}
|
|
249
|
+
/** Resolve the parent's runtime collection / back-ref for an inverse decl. */
|
|
250
|
+
inverseTarget(decl, parentId) {
|
|
251
|
+
const parent = this.getById(decl.parentModelName, parentId);
|
|
252
|
+
if (parent == null) {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
if (decl.kind === PropertyType.ReferenceCollection) {
|
|
256
|
+
return parent.__collections[decl.parentPropName];
|
|
257
|
+
}
|
|
258
|
+
return parent.__backRefs[decl.parentPropName];
|
|
259
|
+
}
|
|
260
|
+
attachInverseLinks(modelName, instance) {
|
|
261
|
+
for (const decl of this.inverseDeclarations(modelName)) {
|
|
262
|
+
const fk = readFk(instance, decl.fkName);
|
|
263
|
+
if (fk != null) {
|
|
264
|
+
this.inverseTarget(decl, fk)?.attach(instance);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
detachInverseLinks(modelName, instance) {
|
|
269
|
+
for (const decl of this.inverseDeclarations(modelName)) {
|
|
270
|
+
const fk = readFk(instance, decl.fkName);
|
|
271
|
+
if (fk != null) {
|
|
272
|
+
this.inverseTarget(decl, fk)?.detach(instance.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* When a parent enters the pool after its children, seed each declared
|
|
278
|
+
* collection / back-ref from children already in the pool. Counterpart
|
|
279
|
+
* to `attachInverseLinks` on the parent side.
|
|
280
|
+
*/
|
|
281
|
+
populateOwnedCollectionsFromPool(modelName, instance) {
|
|
282
|
+
const meta = ModelRegistry.getModelMeta(modelName);
|
|
283
|
+
if (meta == null) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const [propName, propMeta] of meta.properties) {
|
|
287
|
+
if (propMeta.type === PropertyType.ReferenceCollection) {
|
|
288
|
+
const collection = instance.__collections[propName];
|
|
289
|
+
if (collection == null) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const matches = collection.resolveFromPool(this);
|
|
293
|
+
if (matches.length > 0) {
|
|
294
|
+
collection.setItems(matches);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (propMeta.type === PropertyType.BackReference) {
|
|
298
|
+
const backRef = instance.__backRefs[propName];
|
|
299
|
+
if (backRef == null) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const match = backRef.resolveFromPool(this);
|
|
303
|
+
if (match != null) {
|
|
304
|
+
backRef.attach(match);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Called by BaseModel when a child's foreign-key property changes — either
|
|
311
|
+
* via the prototype setter or via `box.set` in hydrate. Detaches from the
|
|
312
|
+
* old parent and attaches to the new one in a single batched action.
|
|
313
|
+
*/
|
|
314
|
+
notifyReferenceChange(child, childModelName, fkName, oldId, newId) {
|
|
315
|
+
if (oldId === newId) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
// Hot-path gate: skip the runInAction frame and the loop entirely when the
|
|
319
|
+
// changed property isn't an `inverseOf` for any parent. propertyChanged
|
|
320
|
+
// calls this for every tracked write (title, done, updatedAt, ...) — most
|
|
321
|
+
// of which aren't FKs.
|
|
322
|
+
if (!this.inverseFkNames(childModelName).has(fkName)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
runInAction(() => {
|
|
326
|
+
for (const decl of this.inverseDeclarations(childModelName)) {
|
|
327
|
+
if (decl.fkName !== fkName) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (oldId != null) {
|
|
331
|
+
this.inverseTarget(decl, oldId)?.detach(child.id);
|
|
332
|
+
}
|
|
333
|
+
if (newId != null) {
|
|
334
|
+
this.inverseTarget(decl, newId)?.attach(child);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FullStore and PartialStore — per-model stores that sync memory ↔ IndexedDB.
|
|
3
|
+
*
|
|
4
|
+
* StoreManager creates one of these for each registered model:
|
|
5
|
+
* - FullStore for eager/lazy/localOnly models (all instances at once)
|
|
6
|
+
* - PartialStore for partial models (on demand)
|
|
7
|
+
* - EphemeralStore for ephemeral models (pool-only, never persisted)
|
|
8
|
+
*/
|
|
9
|
+
import type { BaseModel } from "./BaseModel";
|
|
10
|
+
import type { StorageAdapter } from "./Database";
|
|
11
|
+
import { ObjectPool } from "./ObjectPool";
|
|
12
|
+
import { type ModelMeta } from "./types";
|
|
13
|
+
export declare abstract class ModelStore {
|
|
14
|
+
protected meta: ModelMeta;
|
|
15
|
+
protected database: StorageAdapter;
|
|
16
|
+
protected pool: ObjectPool;
|
|
17
|
+
constructor(meta: ModelMeta, database: StorageAdapter, pool: ObjectPool);
|
|
18
|
+
get modelName(): string;
|
|
19
|
+
protected hydrateRecord(record: Record<string, unknown>): BaseModel;
|
|
20
|
+
abstract loadFromDatabase(): Promise<void>;
|
|
21
|
+
abstract loadFromServer(records: Record<string, unknown>[]): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
/** FullStore — loads ALL instances of a model at once. */
|
|
24
|
+
export declare class FullStore extends ModelStore {
|
|
25
|
+
loadFromDatabase(): Promise<void>;
|
|
26
|
+
loadFromServer(records: Record<string, unknown>[]): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/** EphemeralStore — pool-only, no IDB reads or writes. */
|
|
29
|
+
export declare class EphemeralStore extends ModelStore {
|
|
30
|
+
loadFromDatabase(): Promise<void>;
|
|
31
|
+
loadFromServer(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
/** PartialStore — loads a subset of instances on demand. */
|
|
34
|
+
export declare class PartialStore extends ModelStore {
|
|
35
|
+
private loadedIds;
|
|
36
|
+
loadFromDatabase(): Promise<void>;
|
|
37
|
+
loadFromServer(records: Record<string, unknown>[]): Promise<void>;
|
|
38
|
+
/** Load a single instance by ID from IDB. Returns null if not found. */
|
|
39
|
+
loadById(id: string): Promise<BaseModel | null>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FullStore and PartialStore — per-model stores that sync memory ↔ IndexedDB.
|
|
3
|
+
*
|
|
4
|
+
* StoreManager creates one of these for each registered model:
|
|
5
|
+
* - FullStore for eager/lazy/localOnly models (all instances at once)
|
|
6
|
+
* - PartialStore for partial models (on demand)
|
|
7
|
+
* - EphemeralStore for ephemeral models (pool-only, never persisted)
|
|
8
|
+
*/
|
|
9
|
+
import { LoadStrategy } from "./types";
|
|
10
|
+
export class ModelStore {
|
|
11
|
+
constructor(meta, database, pool) {
|
|
12
|
+
this.meta = meta;
|
|
13
|
+
this.database = database;
|
|
14
|
+
this.pool = pool;
|
|
15
|
+
}
|
|
16
|
+
get modelName() {
|
|
17
|
+
return this.meta.name;
|
|
18
|
+
}
|
|
19
|
+
hydrateRecord(record) {
|
|
20
|
+
return this.pool.hydrateAndPut(this.modelName, this.meta, record);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** FullStore — loads ALL instances of a model at once. */
|
|
24
|
+
export class FullStore extends ModelStore {
|
|
25
|
+
async loadFromDatabase() {
|
|
26
|
+
const records = await this.database.readAllModels(this.modelName);
|
|
27
|
+
for (const record of records) {
|
|
28
|
+
this.hydrateRecord(record);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async loadFromServer(records) {
|
|
32
|
+
await this.database.clearModelStore(this.modelName);
|
|
33
|
+
await this.database.writeModels(this.modelName, records);
|
|
34
|
+
// Only hydrate into memory if this model loads at bootstrap time
|
|
35
|
+
if (this.meta.loadStrategy === LoadStrategy.Eager) {
|
|
36
|
+
for (const record of records) {
|
|
37
|
+
this.hydrateRecord(record);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** EphemeralStore — pool-only, no IDB reads or writes. */
|
|
43
|
+
export class EphemeralStore extends ModelStore {
|
|
44
|
+
async loadFromDatabase() { }
|
|
45
|
+
async loadFromServer() { }
|
|
46
|
+
}
|
|
47
|
+
/** PartialStore — loads a subset of instances on demand. */
|
|
48
|
+
export class PartialStore extends ModelStore {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(...arguments);
|
|
51
|
+
this.loadedIds = new Set();
|
|
52
|
+
}
|
|
53
|
+
async loadFromDatabase() {
|
|
54
|
+
/* no-op — partial models load on demand */
|
|
55
|
+
}
|
|
56
|
+
async loadFromServer(records) {
|
|
57
|
+
await this.database.clearModelStore(this.modelName);
|
|
58
|
+
await this.database.writeModels(this.modelName, records);
|
|
59
|
+
// NOT hydrated — will be loaded individually when requested
|
|
60
|
+
}
|
|
61
|
+
/** Load a single instance by ID from IDB. Returns null if not found. */
|
|
62
|
+
async loadById(id) {
|
|
63
|
+
if (this.loadedIds.has(id)) {
|
|
64
|
+
return this.pool.getById(this.modelName, id) ?? null;
|
|
65
|
+
}
|
|
66
|
+
const record = await this.database.readModel(this.modelName, id);
|
|
67
|
+
if (record == null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
this.loadedIds.add(id);
|
|
71
|
+
return this.hydrateRecord(record);
|
|
72
|
+
}
|
|
73
|
+
}
|