@statezero/core 0.1.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/dist/adaptors/react/composables.d.ts +1 -0
- package/dist/adaptors/react/composables.js +4 -0
- package/dist/adaptors/react/index.d.ts +1 -0
- package/dist/adaptors/react/index.js +1 -0
- package/dist/adaptors/vue/composables.d.ts +2 -0
- package/dist/adaptors/vue/composables.js +36 -0
- package/dist/adaptors/vue/index.d.ts +2 -0
- package/dist/adaptors/vue/index.js +2 -0
- package/dist/adaptors/vue/reactivity.d.ts +18 -0
- package/dist/adaptors/vue/reactivity.js +125 -0
- package/dist/cli/commands/syncModels.d.ts +132 -0
- package/dist/cli/commands/syncModels.js +1040 -0
- package/dist/cli/configFileLoader.d.ts +10 -0
- package/dist/cli/configFileLoader.js +85 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +14 -0
- package/dist/config.d.ts +52 -0
- package/dist/config.js +242 -0
- package/dist/core/eventReceivers.d.ts +179 -0
- package/dist/core/eventReceivers.js +210 -0
- package/dist/core/utils.d.ts +8 -0
- package/dist/core/utils.js +62 -0
- package/dist/filtering/localFiltering.d.ts +116 -0
- package/dist/filtering/localFiltering.js +834 -0
- package/dist/flavours/django/dates.d.ts +33 -0
- package/dist/flavours/django/dates.js +99 -0
- package/dist/flavours/django/errors.d.ts +138 -0
- package/dist/flavours/django/errors.js +187 -0
- package/dist/flavours/django/f.d.ts +6 -0
- package/dist/flavours/django/f.js +91 -0
- package/dist/flavours/django/files.d.ts +76 -0
- package/dist/flavours/django/files.js +338 -0
- package/dist/flavours/django/makeApiCall.d.ts +20 -0
- package/dist/flavours/django/makeApiCall.js +169 -0
- package/dist/flavours/django/manager.d.ts +197 -0
- package/dist/flavours/django/manager.js +222 -0
- package/dist/flavours/django/model.d.ts +112 -0
- package/dist/flavours/django/model.js +253 -0
- package/dist/flavours/django/operationFactory.d.ts +65 -0
- package/dist/flavours/django/operationFactory.js +216 -0
- package/dist/flavours/django/q.d.ts +70 -0
- package/dist/flavours/django/q.js +43 -0
- package/dist/flavours/django/queryExecutor.d.ts +131 -0
- package/dist/flavours/django/queryExecutor.js +468 -0
- package/dist/flavours/django/querySet.d.ts +412 -0
- package/dist/flavours/django/querySet.js +601 -0
- package/dist/flavours/django/tempPk.d.ts +19 -0
- package/dist/flavours/django/tempPk.js +48 -0
- package/dist/flavours/django/utils.d.ts +19 -0
- package/dist/flavours/django/utils.js +29 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +38 -0
- package/dist/react-entry.d.ts +2 -0
- package/dist/react-entry.js +2 -0
- package/dist/reactiveAdaptor.d.ts +24 -0
- package/dist/reactiveAdaptor.js +38 -0
- package/dist/setup.d.ts +15 -0
- package/dist/setup.js +22 -0
- package/dist/syncEngine/cache/cache.d.ts +75 -0
- package/dist/syncEngine/cache/cache.js +341 -0
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +79 -0
- package/dist/syncEngine/metrics/metricOptCalcs.js +284 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +53 -0
- package/dist/syncEngine/registries/metricRegistry.js +162 -0
- package/dist/syncEngine/registries/modelStoreRegistry.d.ts +11 -0
- package/dist/syncEngine/registries/modelStoreRegistry.js +56 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +55 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +244 -0
- package/dist/syncEngine/stores/metricStore.d.ts +55 -0
- package/dist/syncEngine/stores/metricStore.js +222 -0
- package/dist/syncEngine/stores/modelStore.d.ts +40 -0
- package/dist/syncEngine/stores/modelStore.js +405 -0
- package/dist/syncEngine/stores/operation.d.ts +99 -0
- package/dist/syncEngine/stores/operation.js +224 -0
- package/dist/syncEngine/stores/operationEventHandlers.d.ts +8 -0
- package/dist/syncEngine/stores/operationEventHandlers.js +239 -0
- package/dist/syncEngine/stores/querysetStore.d.ts +32 -0
- package/dist/syncEngine/stores/querysetStore.js +200 -0
- package/dist/syncEngine/stores/reactivity.d.ts +3 -0
- package/dist/syncEngine/stores/reactivity.js +4 -0
- package/dist/syncEngine/stores/utils.d.ts +14 -0
- package/dist/syncEngine/stores/utils.js +32 -0
- package/dist/syncEngine/sync.d.ts +32 -0
- package/dist/syncEngine/sync.js +169 -0
- package/dist/vue-entry.d.ts +6 -0
- package/dist/vue-entry.js +2 -0
- package/license.md +116 -0
- package/package.json +123 -0
- package/readme.md +222 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { Operation, Status, Type, operationRegistry } from './operation.js';
|
|
2
|
+
import { isNil, isEmpty, trim } from 'lodash-es';
|
|
3
|
+
import { modelEventEmitter } from './reactivity.js';
|
|
4
|
+
import { Cache } from '../cache/cache.js';
|
|
5
|
+
import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
|
|
6
|
+
const emitEvents = (modelClass, events) => {
|
|
7
|
+
// helper method to emit every event
|
|
8
|
+
events.forEach(event => {
|
|
9
|
+
modelEventEmitter.emit(`${modelClass.configKey}::${modelClass.modelName}::render`, event);
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
class EventData {
|
|
13
|
+
constructor(ModelClass, pk) {
|
|
14
|
+
this.ModelClass = ModelClass;
|
|
15
|
+
this.pk = pk;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One event per instance in a single operation
|
|
19
|
+
*/
|
|
20
|
+
static fromOperation(operation) {
|
|
21
|
+
const ModelClass = operation.queryset.ModelClass;
|
|
22
|
+
const pkField = ModelClass.primaryKeyField;
|
|
23
|
+
return operation.instances.map(instance => new EventData(ModelClass, instance[pkField]));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One event per unique PK across multiple operations
|
|
27
|
+
*/
|
|
28
|
+
static fromOperations(operations) {
|
|
29
|
+
if (!operations.length)
|
|
30
|
+
return [];
|
|
31
|
+
const ModelClass = operations[0].queryset.ModelClass;
|
|
32
|
+
const pkField = ModelClass.primaryKeyField;
|
|
33
|
+
const uniquePks = new Set();
|
|
34
|
+
for (const op of operations) {
|
|
35
|
+
for (const inst of op.instances) {
|
|
36
|
+
uniquePks.add(inst[pkField]);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Array.from(uniquePks).map(pk => new EventData(ModelClass, pk));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One event per unique PK in an array of instances
|
|
43
|
+
*/
|
|
44
|
+
static fromInstances(instances, ModelClass) {
|
|
45
|
+
const pkField = ModelClass.primaryKeyField;
|
|
46
|
+
const uniquePks = new Set(instances
|
|
47
|
+
.filter(inst => inst && inst[pkField] != null)
|
|
48
|
+
.map(inst => inst[pkField]));
|
|
49
|
+
return Array.from(uniquePks).map(pk => new EventData(ModelClass, pk));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class ModelStore {
|
|
53
|
+
constructor(modelClass, fetchFn, initialGroundTruth = null, initialOperations = null, options = {}) {
|
|
54
|
+
this.modelClass = modelClass;
|
|
55
|
+
this.fetchFn = fetchFn;
|
|
56
|
+
this.isSyncing = false;
|
|
57
|
+
this.pruneThreshold = options.pruneThreshold || 10;
|
|
58
|
+
this.groundTruthArray = initialGroundTruth || [];
|
|
59
|
+
this.operationsMap = new Map();
|
|
60
|
+
// Handle initial operations if provided
|
|
61
|
+
if (initialOperations && initialOperations.length > 0) {
|
|
62
|
+
this._loadOperations(initialOperations);
|
|
63
|
+
}
|
|
64
|
+
this.modelCache = new Cache('model-cache', {}, this.onHydrated.bind(this));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Load operations from data and add them to the operations map,
|
|
68
|
+
* reusing existing operations from the registry if they exist
|
|
69
|
+
*/
|
|
70
|
+
_loadOperations(operationsData) {
|
|
71
|
+
operationsData.forEach(opData => {
|
|
72
|
+
const existingOp = operationRegistry.get(opData.operationId);
|
|
73
|
+
if (existingOp) {
|
|
74
|
+
// If the operation exists in the registry, use it
|
|
75
|
+
this.operationsMap.set(existingOp.operationId, existingOp);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// Otherwise just use the plain object data
|
|
79
|
+
this.operationsMap.set(opData.operationId, new Operation(opData, true));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Caching
|
|
84
|
+
get cacheKey() {
|
|
85
|
+
return `${this.modelClass.configKey}::${this.modelClass.modelName}`;
|
|
86
|
+
}
|
|
87
|
+
onHydrated() {
|
|
88
|
+
if (this.groundTruthArray.length === 0 && this.operationsMap.size === 0) {
|
|
89
|
+
let cached = this.modelCache.get(this.cacheKey);
|
|
90
|
+
console.log(`[ModelStore] Hydrated ${this.modelClass.modelName} with ${(cached || []).length} items from cache`);
|
|
91
|
+
if (!isNil(cached) && !isEmpty(cached)) {
|
|
92
|
+
this.setGroundTruth(cached);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
setCache(result) {
|
|
97
|
+
const pkField = this.pkField;
|
|
98
|
+
let nonTempPkItems = [];
|
|
99
|
+
result.forEach((item) => {
|
|
100
|
+
let pk = item[pkField];
|
|
101
|
+
if (typeof pk === 'string' && containsTempPk(pk)) {
|
|
102
|
+
pk = replaceTempPks(item[pkField]);
|
|
103
|
+
if (isNil(pk) || isEmpty(trim(pk))) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
item[pkField] = pk;
|
|
108
|
+
nonTempPkItems.push(item);
|
|
109
|
+
});
|
|
110
|
+
this.modelCache.set(this.cacheKey, nonTempPkItems);
|
|
111
|
+
}
|
|
112
|
+
clearCache() {
|
|
113
|
+
this.modelCache.delete(this.cacheKey);
|
|
114
|
+
}
|
|
115
|
+
updateCache(items, requestedPks) {
|
|
116
|
+
const pkField = this.pkField;
|
|
117
|
+
let nonTempPkItems = [];
|
|
118
|
+
items.forEach((item) => {
|
|
119
|
+
let pk = item[pkField];
|
|
120
|
+
if (typeof pk === 'string' && containsTempPk(pk)) {
|
|
121
|
+
pk = replaceTempPks(item[pkField]);
|
|
122
|
+
if (isNil(pk) || isEmpty(trim(pk))) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
item[pkField] = pk;
|
|
127
|
+
nonTempPkItems.push(item);
|
|
128
|
+
});
|
|
129
|
+
// If rendering ALL items (requestedPks is null), simply replace the cache
|
|
130
|
+
if (requestedPks === null) {
|
|
131
|
+
this.setCache(nonTempPkItems);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Otherwise, we're rendering specific items - update only those items
|
|
135
|
+
const currentCache = this.modelCache.get(this.cacheKey) || [];
|
|
136
|
+
// Filter out items that were requested but not in the result (they were deleted)
|
|
137
|
+
const filteredCache = currentCache.filter(item => item &&
|
|
138
|
+
typeof item === 'object' &&
|
|
139
|
+
pkField in item &&
|
|
140
|
+
(!requestedPks.has(item[pkField]) ||
|
|
141
|
+
nonTempPkItems.some(newItem => newItem[pkField] === item[pkField])));
|
|
142
|
+
// Create a map for faster lookups
|
|
143
|
+
const cacheMap = new Map(filteredCache.map(item => [item[pkField], item]));
|
|
144
|
+
// Add or update items from the result
|
|
145
|
+
for (const item of nonTempPkItems) {
|
|
146
|
+
if (item && typeof item === 'object' && pkField in item) {
|
|
147
|
+
cacheMap.set(item[pkField], item);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Update the cache
|
|
151
|
+
const updatedCache = Array.from(cacheMap.values());
|
|
152
|
+
this.setCache(updatedCache);
|
|
153
|
+
}
|
|
154
|
+
// Main modelStore methods
|
|
155
|
+
get operations() {
|
|
156
|
+
return Array.from(this.operationsMap.values());
|
|
157
|
+
}
|
|
158
|
+
get pkField() {
|
|
159
|
+
return this.modelClass.primaryKeyField;
|
|
160
|
+
}
|
|
161
|
+
// Commit optimistic updates
|
|
162
|
+
addOperation(operation) {
|
|
163
|
+
this.operationsMap.set(operation.operationId, operation);
|
|
164
|
+
if (this.operationsMap.size > this.pruneThreshold) {
|
|
165
|
+
this.prune();
|
|
166
|
+
}
|
|
167
|
+
emitEvents(this.modelClass, EventData.fromOperation(operation));
|
|
168
|
+
}
|
|
169
|
+
updateOperation(operation) {
|
|
170
|
+
if (!this.operationsMap.has(operation.operationId))
|
|
171
|
+
return false;
|
|
172
|
+
this.operationsMap.set(operation.operationId, operation);
|
|
173
|
+
emitEvents(this.modelClass, EventData.fromOperation(operation));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
confirm(operation) {
|
|
177
|
+
if (!this.operationsMap.has(operation.operationId))
|
|
178
|
+
return;
|
|
179
|
+
this.operationsMap.set(operation.operationId, operation);
|
|
180
|
+
emitEvents(this.modelClass, EventData.fromOperation(operation));
|
|
181
|
+
}
|
|
182
|
+
reject(operation) {
|
|
183
|
+
if (!this.operationsMap.has(operation.operationId))
|
|
184
|
+
return;
|
|
185
|
+
this.operationsMap.set(operation.operationId, operation);
|
|
186
|
+
emitEvents(this.modelClass, EventData.fromOperation(operation));
|
|
187
|
+
}
|
|
188
|
+
setOperations(operations = []) {
|
|
189
|
+
const prevOps = this.operations;
|
|
190
|
+
this.operationsMap.clear();
|
|
191
|
+
operations.forEach(op => {
|
|
192
|
+
this.operationsMap.set(op.operationId, op);
|
|
193
|
+
});
|
|
194
|
+
const allOps = [...prevOps, ...this.operations];
|
|
195
|
+
emitEvents(this.modelClass, EventData.fromOperations(allOps));
|
|
196
|
+
}
|
|
197
|
+
// Ground truth data methods
|
|
198
|
+
setGroundTruth(groundTruth) {
|
|
199
|
+
let prevGroundTruth = this.groundTruthArray;
|
|
200
|
+
this.groundTruthArray = Array.isArray(groundTruth) ? groundTruth : [];
|
|
201
|
+
// reactivity - gather all ops
|
|
202
|
+
const allOps = [...prevGroundTruth, ...this.groundTruthArray];
|
|
203
|
+
emitEvents(this.modelClass, EventData.fromInstances(allOps, this.modelClass));
|
|
204
|
+
}
|
|
205
|
+
getGroundTruth() {
|
|
206
|
+
return this.groundTruthArray;
|
|
207
|
+
}
|
|
208
|
+
get groundTruthPks() {
|
|
209
|
+
const pk = this.pkField;
|
|
210
|
+
return this.groundTruthArray
|
|
211
|
+
.filter(instance => instance && typeof instance === 'object' && pk in instance)
|
|
212
|
+
.map(instance => instance[pk]);
|
|
213
|
+
}
|
|
214
|
+
addToGroundTruth(instances) {
|
|
215
|
+
if (!Array.isArray(instances) || instances.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
const pkField = this.pkField;
|
|
218
|
+
const pkMap = new Map();
|
|
219
|
+
instances.forEach(inst => {
|
|
220
|
+
if (inst && typeof inst === 'object' && pkField in inst) {
|
|
221
|
+
pkMap.set(inst[pkField], inst);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.warn(`[ModelStore ${this.modelClass.modelName}] Skipping invalid instance in addToGroundTruth:`, inst);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
if (pkMap.size === 0)
|
|
228
|
+
return;
|
|
229
|
+
const updatedGroundTruth = [];
|
|
230
|
+
const processedPks = new Set();
|
|
231
|
+
const checkpointInstances = []; // Track instances that need CHECKPOINT operations
|
|
232
|
+
for (const existingItem of this.groundTruthArray) {
|
|
233
|
+
if (!existingItem || typeof existingItem !== 'object' || !(pkField in existingItem)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const pk = existingItem[pkField];
|
|
237
|
+
if (pkMap.has(pk)) {
|
|
238
|
+
const updatedInstance = { ...existingItem, ...pkMap.get(pk) };
|
|
239
|
+
updatedGroundTruth.push(updatedInstance);
|
|
240
|
+
processedPks.add(pk);
|
|
241
|
+
// This instance already existed - add it to checkpoint list
|
|
242
|
+
checkpointInstances.push(updatedInstance);
|
|
243
|
+
pkMap.delete(pk);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
updatedGroundTruth.push(existingItem);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Add completely new instances (these don't need checkpoint operations)
|
|
250
|
+
updatedGroundTruth.push(...Array.from(pkMap.values()));
|
|
251
|
+
this.groundTruthArray = updatedGroundTruth;
|
|
252
|
+
// Create CHECKPOINT operation for instances that already existed
|
|
253
|
+
if (checkpointInstances.length > 0) {
|
|
254
|
+
const checkpointOperation = new Operation({
|
|
255
|
+
operationId: `checkpoint_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
256
|
+
type: Type.CHECKPOINT,
|
|
257
|
+
instances: checkpointInstances,
|
|
258
|
+
status: Status.CONFIRMED,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
queryset: this.modelClass.objects.all()
|
|
261
|
+
});
|
|
262
|
+
this.operationsMap.set(checkpointOperation.operationId, checkpointOperation);
|
|
263
|
+
console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
|
|
264
|
+
}
|
|
265
|
+
// reactivity - use all the newly added instances (both new and updated)
|
|
266
|
+
emitEvents(this.modelClass, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
|
|
267
|
+
}
|
|
268
|
+
_filteredOperations(pks, operations) {
|
|
269
|
+
if (!pks)
|
|
270
|
+
return operations;
|
|
271
|
+
const pkField = this.pkField;
|
|
272
|
+
let filteredOps = [];
|
|
273
|
+
for (const op of operations) {
|
|
274
|
+
let relevantInstances = op.instances.filter(instance => pks.has(instance[pkField] || instance));
|
|
275
|
+
if (relevantInstances.length > 0) {
|
|
276
|
+
filteredOps.push({
|
|
277
|
+
operationId: op.operationId,
|
|
278
|
+
instances: relevantInstances,
|
|
279
|
+
timestamp: op.timestamp,
|
|
280
|
+
queryset: op.queryset,
|
|
281
|
+
type: op.type,
|
|
282
|
+
status: op.status,
|
|
283
|
+
args: op.args
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return filteredOps;
|
|
288
|
+
}
|
|
289
|
+
_filteredGroundTruth(pks, groundTruthArray) {
|
|
290
|
+
const pkField = this.pkField;
|
|
291
|
+
let groundTruthMap = new Map();
|
|
292
|
+
for (const instance of groundTruthArray) {
|
|
293
|
+
if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const pk = instance[pkField];
|
|
297
|
+
if (!pks || pks.has(pk)) {
|
|
298
|
+
groundTruthMap.set(pk, instance);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return groundTruthMap;
|
|
302
|
+
}
|
|
303
|
+
applyOperation(operation, currentInstances) {
|
|
304
|
+
const pkField = this.pkField;
|
|
305
|
+
for (const instance of operation.instances) {
|
|
306
|
+
if (!instance || typeof instance !== 'object' || !(pkField in instance)) {
|
|
307
|
+
console.warn(`[ModelStore ${this.modelClass.modelName}] Skipping instance ${instance} in operation ${operation.operationId} during applyOperation due to missing PK field '${String(pkField)}' or invalid format.`);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
let pk = instance[pkField];
|
|
311
|
+
switch (operation.type) {
|
|
312
|
+
case Type.CREATE:
|
|
313
|
+
if (!currentInstances.has(pk)) {
|
|
314
|
+
currentInstances.set(pk, instance);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
case Type.CHECKPOINT:
|
|
318
|
+
case Type.UPDATE_INSTANCE:
|
|
319
|
+
case Type.UPDATE: {
|
|
320
|
+
const existing = currentInstances.get(pk);
|
|
321
|
+
if (existing) {
|
|
322
|
+
currentInstances.set(pk, { ...existing, ...instance });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
const wasDeletedLocally = this.operations.some(op => op.type === Type.DELETE &&
|
|
326
|
+
op.status !== Status.REJECTED &&
|
|
327
|
+
op.instances.some(inst => inst && inst[pkField] === pk));
|
|
328
|
+
if (!wasDeletedLocally) {
|
|
329
|
+
currentInstances.set(pk, instance);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case Type.DELETE_INSTANCE:
|
|
335
|
+
case Type.DELETE:
|
|
336
|
+
currentInstances.delete(pk);
|
|
337
|
+
break;
|
|
338
|
+
default:
|
|
339
|
+
console.error(`[ModelStore ${this.modelClass.modelName}] Unknown operation type: ${operation.type}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return currentInstances;
|
|
343
|
+
}
|
|
344
|
+
getTrimmedOperations() {
|
|
345
|
+
const twoMinutesAgo = Date.now() - 1000 * 60 * 2;
|
|
346
|
+
return this.operations.filter(operation => operation.timestamp > twoMinutesAgo);
|
|
347
|
+
}
|
|
348
|
+
getInflightOperations() {
|
|
349
|
+
return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
|
|
350
|
+
operation.status != Status.REJECTED);
|
|
351
|
+
}
|
|
352
|
+
// Pruning
|
|
353
|
+
prune() {
|
|
354
|
+
let renderedPks = this.render(null, false);
|
|
355
|
+
this.setGroundTruth(renderedPks);
|
|
356
|
+
this.setOperations(this.getInflightOperations());
|
|
357
|
+
this.setCache(renderedPks);
|
|
358
|
+
}
|
|
359
|
+
// Render methods
|
|
360
|
+
render(pks = null, optimistic = true) {
|
|
361
|
+
const pksSet = pks === null ? null :
|
|
362
|
+
(pks instanceof Set ? pks : new Set(Array.isArray(pks) ? pks : [pks]));
|
|
363
|
+
const renderedInstancesMap = this._filteredGroundTruth(pksSet, this.groundTruthArray);
|
|
364
|
+
const relevantOperations = this._filteredOperations(pksSet, this.operations);
|
|
365
|
+
for (const op of relevantOperations) {
|
|
366
|
+
if (op.status !== Status.REJECTED && (optimistic || op.status === Status.CONFIRMED)) {
|
|
367
|
+
this.applyOperation(op, renderedInstancesMap);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
let result = Array.from(renderedInstancesMap.values());
|
|
371
|
+
if (pks)
|
|
372
|
+
this.updateCache(result, pksSet);
|
|
373
|
+
if (isNil(pks))
|
|
374
|
+
this.setCache(result);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
async sync(pks = null) {
|
|
378
|
+
const storeIdForLog = this.modelClass.modelName;
|
|
379
|
+
if (this.isSyncing)
|
|
380
|
+
return;
|
|
381
|
+
this.isSyncing = true;
|
|
382
|
+
try {
|
|
383
|
+
const currentPks = pks || this.groundTruthPks;
|
|
384
|
+
if (currentPks.length === 0) {
|
|
385
|
+
const trimmedOps = this.getTrimmedOperations();
|
|
386
|
+
this.setOperations(trimmedOps);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const newGroundTruth = await this.fetchFn({ pks: currentPks, modelClass: this.modelClass });
|
|
390
|
+
if (pks) {
|
|
391
|
+
this.addToGroundTruth(newGroundTruth);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.setGroundTruth(newGroundTruth);
|
|
395
|
+
const trimmedOps = this.getTrimmedOperations();
|
|
396
|
+
this.setOperations(trimmedOps);
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.error(`[ModelStore ${storeIdForLog}] Failed to sync ground truth:`, error);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
this.isSyncing = false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export const operationEvents: import("mitt").Emitter<Record<import("mitt").EventType, unknown>>;
|
|
2
|
+
export namespace Status {
|
|
3
|
+
let CREATED: string;
|
|
4
|
+
let UPDATED: string;
|
|
5
|
+
let CONFIRMED: string;
|
|
6
|
+
let REJECTED: string;
|
|
7
|
+
let CLEAR: string;
|
|
8
|
+
let MUTATED: string;
|
|
9
|
+
}
|
|
10
|
+
export namespace Type {
|
|
11
|
+
let CREATE: string;
|
|
12
|
+
let UPDATE: string;
|
|
13
|
+
let DELETE: string;
|
|
14
|
+
let UPDATE_INSTANCE: string;
|
|
15
|
+
let DELETE_INSTANCE: string;
|
|
16
|
+
let GET_OR_CREATE: string;
|
|
17
|
+
let UPDATE_OR_CREATE: string;
|
|
18
|
+
let CHECKPOINT: string;
|
|
19
|
+
let COUNT: string;
|
|
20
|
+
let MIN: string;
|
|
21
|
+
let MAX: string;
|
|
22
|
+
let AVG: string;
|
|
23
|
+
let SUM: string;
|
|
24
|
+
let AGGREGATE: string;
|
|
25
|
+
}
|
|
26
|
+
export class Operation {
|
|
27
|
+
constructor(data: any, restore?: boolean);
|
|
28
|
+
operationId: any;
|
|
29
|
+
type: any;
|
|
30
|
+
status: any;
|
|
31
|
+
queryset: any;
|
|
32
|
+
args: any;
|
|
33
|
+
timestamp: any;
|
|
34
|
+
doNotPropagate: any;
|
|
35
|
+
/**
|
|
36
|
+
* Setter for instances
|
|
37
|
+
*/
|
|
38
|
+
set instances(value: any);
|
|
39
|
+
/**
|
|
40
|
+
* Getter for instances that replaces any temporary PKs with real PKs
|
|
41
|
+
*/
|
|
42
|
+
get instances(): any;
|
|
43
|
+
/**
|
|
44
|
+
* Setter for frozenInstances
|
|
45
|
+
*/
|
|
46
|
+
set frozenInstances(value: any);
|
|
47
|
+
/**
|
|
48
|
+
* Getter for frozenInstances that replaces any temporary PKs with real PKs
|
|
49
|
+
*/
|
|
50
|
+
get frozenInstances(): any;
|
|
51
|
+
/**
|
|
52
|
+
* Get primary keys of all instances in this operation
|
|
53
|
+
* Returns primary keys as simple values (not objects)
|
|
54
|
+
*/
|
|
55
|
+
get instancePks(): any;
|
|
56
|
+
/**
|
|
57
|
+
* Update this operation's status and emit an event
|
|
58
|
+
* @param {string} status - New status ('confirmed', 'rejected', etc.)
|
|
59
|
+
* @param {Array|Object|null} [instances=null] - New instances for the operation
|
|
60
|
+
*/
|
|
61
|
+
updateStatus(status: string, instances?: any[] | Object | null): void;
|
|
62
|
+
/**
|
|
63
|
+
* Updates this operation with new data and emits the appropriate event
|
|
64
|
+
* @param {Object} newData - New data to update the operation with
|
|
65
|
+
* @returns {Operation} - Returns this operation instance for chaining
|
|
66
|
+
*/
|
|
67
|
+
mutate(newData: Object): Operation;
|
|
68
|
+
#private;
|
|
69
|
+
}
|
|
70
|
+
export const operationRegistry: OperationRegistry;
|
|
71
|
+
declare class OperationRegistry {
|
|
72
|
+
_operations: Map<any, any>;
|
|
73
|
+
/**
|
|
74
|
+
* Registers a pre-constructed Operation instance in the registry.
|
|
75
|
+
* Ensures the operationId is unique within the registry.
|
|
76
|
+
* Throws an Error if the operationId already exists.
|
|
77
|
+
*
|
|
78
|
+
* @param {Operation} operation - The fully instantiated Operation object to register.
|
|
79
|
+
* @throws {Error} If the input is not a valid operation object or if an operation with the same operationId already exists.
|
|
80
|
+
*/
|
|
81
|
+
register(operation: Operation): void;
|
|
82
|
+
/**
|
|
83
|
+
* Retrieves an operation by its ID.
|
|
84
|
+
* @param {string} operationId - The ID of the operation.
|
|
85
|
+
* @returns {Operation | undefined} The operation instance or undefined if not found.
|
|
86
|
+
*/
|
|
87
|
+
get(operationId: string): Operation | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Checks if an operation with the given ID exists in the registry.
|
|
90
|
+
* @param {string} operationId - The ID of the operation to check.
|
|
91
|
+
* @returns {boolean} True if the operation exists, false otherwise.
|
|
92
|
+
*/
|
|
93
|
+
has(operationId: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Clears all operations from the registry.
|
|
96
|
+
*/
|
|
97
|
+
clear(): void;
|
|
98
|
+
}
|
|
99
|
+
export {};
|