@statezero/core 0.1.1 → 0.1.3-9.1
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 +3 -2
- package/dist/cli/commands/sync.d.ts +6 -0
- package/dist/cli/commands/sync.js +30 -0
- package/dist/cli/commands/syncActions.d.ts +46 -0
- package/dist/cli/commands/syncActions.js +623 -0
- package/dist/cli/commands/syncModels.js +61 -29
- package/dist/cli/index.js +18 -10
- package/dist/config.d.ts +5 -0
- package/dist/config.js +40 -10
- package/dist/flavours/django/dates.js +3 -3
- package/dist/flavours/django/files.d.ts +8 -7
- package/dist/flavours/django/files.js +36 -2
- package/dist/flavours/django/model.d.ts +15 -0
- package/dist/flavours/django/model.js +143 -24
- package/dist/setup.js +11 -0
- package/dist/syncEngine/registries/metricRegistry.d.ts +5 -0
- package/dist/syncEngine/registries/metricRegistry.js +8 -0
- package/dist/syncEngine/registries/querysetStoreGraph.d.ts +21 -0
- package/dist/syncEngine/registries/querysetStoreGraph.js +95 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.d.ts +14 -0
- package/dist/syncEngine/registries/querysetStoreRegistry.js +64 -16
- package/dist/syncEngine/stores/modelStore.d.ts +1 -0
- package/dist/syncEngine/stores/modelStore.js +23 -12
- package/dist/syncEngine/stores/querysetStore.d.ts +18 -0
- package/dist/syncEngine/stores/querysetStore.js +129 -18
- package/dist/syncEngine/sync.d.ts +5 -0
- package/dist/syncEngine/sync.js +61 -5
- package/package.json +126 -123
- package/readme.md +1 -1
|
@@ -7,6 +7,7 @@ export class ModelStore {
|
|
|
7
7
|
isSyncing: boolean;
|
|
8
8
|
pruneThreshold: any;
|
|
9
9
|
modelCache: Cache;
|
|
10
|
+
_lastRenderedData: Map<any, any>;
|
|
10
11
|
/**
|
|
11
12
|
* Load operations from data and add them to the operations map,
|
|
12
13
|
* reusing existing operations from the registry if they exist
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { Operation, Status, Type, operationRegistry } from './operation.js';
|
|
2
|
-
import { isNil, isEmpty, trim } from 'lodash-es';
|
|
2
|
+
import { isNil, isEmpty, trim, isEqual } from 'lodash-es';
|
|
3
3
|
import { modelEventEmitter } from './reactivity.js';
|
|
4
4
|
import { Cache } from '../cache/cache.js';
|
|
5
5
|
import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
|
|
6
|
-
const emitEvents = (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const emitEvents = (store, events) => {
|
|
7
|
+
if (!Array.isArray(events))
|
|
8
|
+
return;
|
|
9
|
+
events.forEach((event) => {
|
|
10
|
+
const pk = event.pk;
|
|
11
|
+
if (isNil(pk))
|
|
12
|
+
return;
|
|
13
|
+
const newRenderedDataArray = store.render([pk], true);
|
|
14
|
+
const newRenderedData = newRenderedDataArray.length > 0 ? newRenderedDataArray[0] : null;
|
|
15
|
+
const lastRenderedData = store._lastRenderedData.get(pk);
|
|
16
|
+
if (!isEqual(newRenderedData, lastRenderedData)) {
|
|
17
|
+
store._lastRenderedData.set(pk, newRenderedData);
|
|
18
|
+
modelEventEmitter.emit(`${store.modelClass.configKey}::${store.modelClass.modelName}::render`, event);
|
|
19
|
+
}
|
|
10
20
|
});
|
|
11
21
|
};
|
|
12
22
|
class EventData {
|
|
@@ -62,6 +72,7 @@ export class ModelStore {
|
|
|
62
72
|
this._loadOperations(initialOperations);
|
|
63
73
|
}
|
|
64
74
|
this.modelCache = new Cache('model-cache', {}, this.onHydrated.bind(this));
|
|
75
|
+
this._lastRenderedData = new Map();
|
|
65
76
|
}
|
|
66
77
|
/**
|
|
67
78
|
* Load operations from data and add them to the operations map,
|
|
@@ -164,26 +175,26 @@ export class ModelStore {
|
|
|
164
175
|
if (this.operationsMap.size > this.pruneThreshold) {
|
|
165
176
|
this.prune();
|
|
166
177
|
}
|
|
167
|
-
emitEvents(this
|
|
178
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
168
179
|
}
|
|
169
180
|
updateOperation(operation) {
|
|
170
181
|
if (!this.operationsMap.has(operation.operationId))
|
|
171
182
|
return false;
|
|
172
183
|
this.operationsMap.set(operation.operationId, operation);
|
|
173
|
-
emitEvents(this
|
|
184
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
174
185
|
return true;
|
|
175
186
|
}
|
|
176
187
|
confirm(operation) {
|
|
177
188
|
if (!this.operationsMap.has(operation.operationId))
|
|
178
189
|
return;
|
|
179
190
|
this.operationsMap.set(operation.operationId, operation);
|
|
180
|
-
emitEvents(this
|
|
191
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
181
192
|
}
|
|
182
193
|
reject(operation) {
|
|
183
194
|
if (!this.operationsMap.has(operation.operationId))
|
|
184
195
|
return;
|
|
185
196
|
this.operationsMap.set(operation.operationId, operation);
|
|
186
|
-
emitEvents(this
|
|
197
|
+
emitEvents(this, EventData.fromOperation(operation));
|
|
187
198
|
}
|
|
188
199
|
setOperations(operations = []) {
|
|
189
200
|
const prevOps = this.operations;
|
|
@@ -192,7 +203,7 @@ export class ModelStore {
|
|
|
192
203
|
this.operationsMap.set(op.operationId, op);
|
|
193
204
|
});
|
|
194
205
|
const allOps = [...prevOps, ...this.operations];
|
|
195
|
-
emitEvents(this
|
|
206
|
+
emitEvents(this, EventData.fromOperations(allOps));
|
|
196
207
|
}
|
|
197
208
|
// Ground truth data methods
|
|
198
209
|
setGroundTruth(groundTruth) {
|
|
@@ -200,7 +211,7 @@ export class ModelStore {
|
|
|
200
211
|
this.groundTruthArray = Array.isArray(groundTruth) ? groundTruth : [];
|
|
201
212
|
// reactivity - gather all ops
|
|
202
213
|
const allOps = [...prevGroundTruth, ...this.groundTruthArray];
|
|
203
|
-
emitEvents(this
|
|
214
|
+
emitEvents(this, EventData.fromInstances(allOps, this.modelClass));
|
|
204
215
|
}
|
|
205
216
|
getGroundTruth() {
|
|
206
217
|
return this.groundTruthArray;
|
|
@@ -263,7 +274,7 @@ export class ModelStore {
|
|
|
263
274
|
console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
|
|
264
275
|
}
|
|
265
276
|
// reactivity - use all the newly added instances (both new and updated)
|
|
266
|
-
emitEvents(this
|
|
277
|
+
emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
|
|
267
278
|
}
|
|
268
279
|
_filteredOperations(pks, operations) {
|
|
269
280
|
if (!pks)
|
|
@@ -6,8 +6,16 @@ export class QuerysetStore {
|
|
|
6
6
|
operationsMap: Map<any, any>;
|
|
7
7
|
groundTruthPks: never[];
|
|
8
8
|
isSyncing: boolean;
|
|
9
|
+
lastSync: number | null;
|
|
10
|
+
needsSync: boolean;
|
|
11
|
+
isTemp: any;
|
|
9
12
|
pruneThreshold: any;
|
|
13
|
+
getRootStore: any;
|
|
10
14
|
qsCache: Cache;
|
|
15
|
+
_lastRenderedPks: any[] | null;
|
|
16
|
+
renderCallbacks: Set<any>;
|
|
17
|
+
_rootUnregister: any;
|
|
18
|
+
_currentRootStore: any;
|
|
11
19
|
get cacheKey(): any;
|
|
12
20
|
onHydrated(hydratedData: any): void;
|
|
13
21
|
setCache(result: any): void;
|
|
@@ -25,7 +33,17 @@ export class QuerysetStore {
|
|
|
25
33
|
getTrimmedOperations(): any[];
|
|
26
34
|
getInflightOperations(): any[];
|
|
27
35
|
prune(): void;
|
|
36
|
+
registerRenderCallback(callback: any): () => boolean;
|
|
37
|
+
_ensureRootRegistration(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Helper to validate PKs against the model store and apply local filtering/sorting.
|
|
40
|
+
* This is the core of the rendering logic.
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
private _getValidatedAndFilteredPks;
|
|
28
44
|
render(optimistic?: boolean, fromCache?: boolean): any[];
|
|
45
|
+
renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
|
|
46
|
+
renderFromData(optimistic?: boolean): any[];
|
|
29
47
|
applyOperation(operation: any, currentPks: any): any;
|
|
30
48
|
sync(): Promise<void>;
|
|
31
49
|
}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { Operation, Status, Type, operationRegistry } from './operation.js';
|
|
2
2
|
import { querysetEventEmitter } from './reactivity.js';
|
|
3
|
-
import { isNil, isEmpty, trim } from 'lodash-es';
|
|
3
|
+
import { isNil, isEmpty, trim, isEqual } from 'lodash-es';
|
|
4
4
|
import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
|
|
5
5
|
import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
|
|
6
6
|
import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
|
|
7
7
|
import hash from 'object-hash';
|
|
8
8
|
import { Cache } from '../cache/cache.js';
|
|
9
|
+
import { filter } from "../../filtering/localFiltering.js";
|
|
10
|
+
import { mod } from 'mathjs';
|
|
9
11
|
export class QuerysetStore {
|
|
10
12
|
constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
|
|
11
13
|
this.modelClass = modelClass;
|
|
12
14
|
this.fetchFn = fetchFn;
|
|
13
15
|
this.queryset = queryset;
|
|
14
16
|
this.isSyncing = false;
|
|
17
|
+
this.lastSync = null;
|
|
18
|
+
this.needsSync = false;
|
|
19
|
+
this.isTemp = options.isTemp || false;
|
|
15
20
|
this.pruneThreshold = options.pruneThreshold || 10;
|
|
21
|
+
this.getRootStore = options.getRootStore || null;
|
|
16
22
|
this.groundTruthPks = initialGroundTruthPks || [];
|
|
17
23
|
this.operationsMap = new Map();
|
|
18
24
|
if (Array.isArray(initialOperations)) {
|
|
@@ -22,7 +28,12 @@ export class QuerysetStore {
|
|
|
22
28
|
this.operationsMap.set(op.operationId, op);
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
|
-
this.qsCache = new Cache(
|
|
31
|
+
this.qsCache = new Cache("queryset-cache", {}, this.onHydrated.bind(this));
|
|
32
|
+
this._lastRenderedPks = null;
|
|
33
|
+
this.renderCallbacks = new Set();
|
|
34
|
+
this._rootUnregister = null;
|
|
35
|
+
this._currentRootStore = null;
|
|
36
|
+
this._ensureRootRegistration();
|
|
26
37
|
}
|
|
27
38
|
// Caching
|
|
28
39
|
get cacheKey() {
|
|
@@ -40,7 +51,7 @@ export class QuerysetStore {
|
|
|
40
51
|
setCache(result) {
|
|
41
52
|
let nonTempPks = [];
|
|
42
53
|
result.forEach((pk) => {
|
|
43
|
-
if (typeof pk ===
|
|
54
|
+
if (typeof pk === "string" && containsTempPk(pk)) {
|
|
44
55
|
pk = replaceTempPks(pk);
|
|
45
56
|
if (isNil(pk) || isEmpty(trim(pk))) {
|
|
46
57
|
return;
|
|
@@ -64,7 +75,22 @@ export class QuerysetStore {
|
|
|
64
75
|
return new Set(this.groundTruthPks);
|
|
65
76
|
}
|
|
66
77
|
_emitRenderEvent() {
|
|
67
|
-
|
|
78
|
+
const newPks = this.render(true, false);
|
|
79
|
+
// 1. Always notify direct child stores to trigger their own re-evaluation.
|
|
80
|
+
// They will perform their own check to see if their own results have changed.
|
|
81
|
+
this.renderCallbacks.forEach((callback) => {
|
|
82
|
+
try {
|
|
83
|
+
callback();
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn("Error in render callback:", error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
// 2. Only emit the global event for UI components if the final list of PKs has actually changed.
|
|
90
|
+
if (!isEqual(newPks, this._lastRenderedPks)) {
|
|
91
|
+
this._lastRenderedPks = newPks; // Update the cache with the new state
|
|
92
|
+
querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
|
|
93
|
+
}
|
|
68
94
|
}
|
|
69
95
|
async addOperation(operation) {
|
|
70
96
|
this.operationsMap.set(operation.operationId, operation);
|
|
@@ -93,9 +119,7 @@ export class QuerysetStore {
|
|
|
93
119
|
this._emitRenderEvent();
|
|
94
120
|
}
|
|
95
121
|
async setGroundTruth(groundTruthPks) {
|
|
96
|
-
this.groundTruthPks = Array.isArray(groundTruthPks)
|
|
97
|
-
? groundTruthPks
|
|
98
|
-
: [];
|
|
122
|
+
this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
|
|
99
123
|
this._emitRenderEvent();
|
|
100
124
|
}
|
|
101
125
|
async setOperations(operations) {
|
|
@@ -109,10 +133,10 @@ export class QuerysetStore {
|
|
|
109
133
|
}
|
|
110
134
|
getTrimmedOperations() {
|
|
111
135
|
const cutoff = Date.now() - 1000 * 60 * 2;
|
|
112
|
-
return this.operations.filter(op => op.timestamp > cutoff);
|
|
136
|
+
return this.operations.filter((op) => op.timestamp > cutoff);
|
|
113
137
|
}
|
|
114
138
|
getInflightOperations() {
|
|
115
|
-
return this.operations.filter(operation => operation.status != Status.CONFIRMED &&
|
|
139
|
+
return this.operations.filter((operation) => operation.status != Status.CONFIRMED &&
|
|
116
140
|
operation.status != Status.REJECTED);
|
|
117
141
|
}
|
|
118
142
|
prune() {
|
|
@@ -120,20 +144,68 @@ export class QuerysetStore {
|
|
|
120
144
|
this.setGroundTruth(renderedPks);
|
|
121
145
|
this.setOperations(this.getInflightOperations());
|
|
122
146
|
}
|
|
147
|
+
registerRenderCallback(callback) {
|
|
148
|
+
this.renderCallbacks.add(callback);
|
|
149
|
+
return () => this.renderCallbacks.delete(callback);
|
|
150
|
+
}
|
|
151
|
+
_ensureRootRegistration() {
|
|
152
|
+
if (this.isTemp)
|
|
153
|
+
return;
|
|
154
|
+
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
155
|
+
// If the root store hasn't changed, nothing to do
|
|
156
|
+
if (this._currentRootStore === rootStore) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Root store changed - clean up old registration if it exists
|
|
160
|
+
if (this._rootUnregister) {
|
|
161
|
+
this._rootUnregister();
|
|
162
|
+
this._rootUnregister = null;
|
|
163
|
+
}
|
|
164
|
+
// Set up new registration if we're derived and have a root store
|
|
165
|
+
if (!isRoot && rootStore) {
|
|
166
|
+
this._rootUnregister = rootStore.registerRenderCallback(() => {
|
|
167
|
+
this._emitRenderEvent();
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Update current root store reference (could be null now)
|
|
171
|
+
this._currentRootStore = rootStore;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Helper to validate PKs against the model store and apply local filtering/sorting.
|
|
175
|
+
* This is the core of the rendering logic.
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
_getValidatedAndFilteredPks(pks) {
|
|
179
|
+
// 1. Convert PKs to instances, filtering out any that are null (deleted).
|
|
180
|
+
const instances = Array.from(pks)
|
|
181
|
+
.map((pk) => this.modelClass.fromPk(pk, this.queryset))
|
|
182
|
+
.filter((instance) => modelStoreRegistry.getEntity(this.modelClass, instance.pk) !== null);
|
|
183
|
+
// 2. Apply the queryset's AST (filters, ordering) to the validated instances.
|
|
184
|
+
const ast = this.queryset.build();
|
|
185
|
+
const finalPks = filter(instances, ast, this.modelClass, false); // false = return PKs
|
|
186
|
+
return finalPks;
|
|
187
|
+
}
|
|
123
188
|
render(optimistic = true, fromCache = false) {
|
|
189
|
+
this._ensureRootRegistration();
|
|
124
190
|
if (fromCache) {
|
|
125
191
|
const cachedResult = this.qsCache.get(this.cacheKey);
|
|
126
192
|
if (Array.isArray(cachedResult)) {
|
|
127
193
|
return cachedResult;
|
|
128
194
|
}
|
|
129
195
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
let pks;
|
|
197
|
+
if (this.getRootStore &&
|
|
198
|
+
typeof this.getRootStore === "function" &&
|
|
199
|
+
!this.isTemp) {
|
|
200
|
+
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
201
|
+
if (!isRoot && rootStore) {
|
|
202
|
+
pks = this.renderFromRoot(optimistic, rootStore);
|
|
134
203
|
}
|
|
135
204
|
}
|
|
136
|
-
|
|
205
|
+
if (isNil(pks)) {
|
|
206
|
+
pks = this.renderFromData(optimistic);
|
|
207
|
+
}
|
|
208
|
+
let result = this._getValidatedAndFilteredPks(pks);
|
|
137
209
|
let limit = this.queryset.build().serializerOptions?.limit;
|
|
138
210
|
if (limit) {
|
|
139
211
|
result = result.slice(0, limit);
|
|
@@ -141,12 +213,30 @@ export class QuerysetStore {
|
|
|
141
213
|
this.setCache(result);
|
|
142
214
|
return result;
|
|
143
215
|
}
|
|
216
|
+
renderFromRoot(optimistic = true, rootStore) {
|
|
217
|
+
let renderedPks = rootStore.render(optimistic);
|
|
218
|
+
let renderedData = renderedPks.map((pk) => {
|
|
219
|
+
return this.modelClass.fromPk(pk, this.queryset);
|
|
220
|
+
});
|
|
221
|
+
let ast = this.queryset.build();
|
|
222
|
+
let result = filter(renderedData, ast, this.modelClass, false);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
renderFromData(optimistic = true) {
|
|
226
|
+
const renderedPks = this.groundTruthSet;
|
|
227
|
+
for (const op of this.operations) {
|
|
228
|
+
if (op.status !== Status.REJECTED &&
|
|
229
|
+
(optimistic || op.status === Status.CONFIRMED)) {
|
|
230
|
+
this.applyOperation(op, renderedPks);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
let result = Array.from(renderedPks);
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
144
236
|
applyOperation(operation, currentPks) {
|
|
145
237
|
const pkField = this.pkField;
|
|
146
238
|
for (const instance of operation.instances) {
|
|
147
|
-
if (!instance ||
|
|
148
|
-
typeof instance !== 'object' ||
|
|
149
|
-
!(pkField in instance)) {
|
|
239
|
+
if (!instance || typeof instance !== "object" || !(pkField in instance)) {
|
|
150
240
|
console.warn(`[QuerysetStore ${this.modelClass.modelName}] Skipping instance in operation ${operation.operationId} due to missing PK '${String(pkField)}' or invalid format.`);
|
|
151
241
|
continue;
|
|
152
242
|
}
|
|
@@ -175,23 +265,44 @@ export class QuerysetStore {
|
|
|
175
265
|
console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
|
|
176
266
|
return;
|
|
177
267
|
}
|
|
268
|
+
// Check if we're delegating to a root store
|
|
269
|
+
if (this.getRootStore &&
|
|
270
|
+
typeof this.getRootStore === "function" &&
|
|
271
|
+
!this.isTemp) {
|
|
272
|
+
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
273
|
+
if (!isRoot && rootStore) {
|
|
274
|
+
// We're delegating to a root store - don't sync, just mark as needing sync
|
|
275
|
+
console.log(`[${id}] Delegating to root store, marking sync needed.`);
|
|
276
|
+
this.needsSync = true;
|
|
277
|
+
this.lastSync = null; // Clear last sync since we're not actually syncing
|
|
278
|
+
this.setOperations(this.getInflightOperations());
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// We're in independent mode - proceed with normal sync
|
|
178
283
|
this.isSyncing = true;
|
|
179
284
|
console.log(`[${id}] Starting sync...`);
|
|
180
285
|
try {
|
|
181
286
|
const response = await this.fetchFn({
|
|
182
287
|
ast: this.queryset.build(),
|
|
183
|
-
modelClass: this.modelClass
|
|
288
|
+
modelClass: this.modelClass,
|
|
184
289
|
});
|
|
185
290
|
const { data, included } = response;
|
|
291
|
+
if (isNil(data)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
186
294
|
console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
|
|
187
295
|
// Persists all the instances (including nested instances) to the model store
|
|
188
296
|
processIncludedEntities(modelStoreRegistry, included, this.modelClass);
|
|
189
297
|
this.setGroundTruth(data);
|
|
190
298
|
this.setOperations(this.getInflightOperations());
|
|
299
|
+
this.lastSync = Date.now();
|
|
300
|
+
this.needsSync = false;
|
|
191
301
|
console.log(`[${id}] Sync completed.`);
|
|
192
302
|
}
|
|
193
303
|
catch (e) {
|
|
194
304
|
console.error(`[${id}] Failed to sync ground truth:`, e);
|
|
305
|
+
this.needsSync = true; // Mark as needing sync on error
|
|
195
306
|
}
|
|
196
307
|
finally {
|
|
197
308
|
this.isSyncing = false;
|
|
@@ -15,10 +15,15 @@ export class SyncManager {
|
|
|
15
15
|
followedModels: Map<any, any>;
|
|
16
16
|
followAllQuerysets: boolean;
|
|
17
17
|
followedQuerysets: Map<any, any>;
|
|
18
|
+
periodicSyncTimer: NodeJS.Timeout | null;
|
|
18
19
|
/**
|
|
19
20
|
* Initialize event handlers for all event receivers
|
|
20
21
|
*/
|
|
21
22
|
initialize(): void;
|
|
23
|
+
startPeriodicSync(): void;
|
|
24
|
+
syncStaleQuerysets(): void;
|
|
25
|
+
isStoreFollowed(registry: any, semanticKey: any): boolean;
|
|
26
|
+
cleanup(): void;
|
|
22
27
|
followModel(registry: any, modelClass: any): void;
|
|
23
28
|
unfollowModel(registry: any, modelClass: any): void;
|
|
24
29
|
manageRegistry(registry: any): void;
|
package/dist/syncEngine/sync.js
CHANGED
|
@@ -58,6 +58,7 @@ export class SyncManager {
|
|
|
58
58
|
// Map of querysets to keep synced
|
|
59
59
|
this.followAllQuerysets = true;
|
|
60
60
|
this.followedQuerysets = new Map();
|
|
61
|
+
this.periodicSyncTimer = null;
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
63
64
|
* Initialize event handlers for all event receivers
|
|
@@ -74,13 +75,68 @@ export class SyncManager {
|
|
|
74
75
|
receiver.addModelEventHandler(this.handleEvent.bind(this));
|
|
75
76
|
}
|
|
76
77
|
});
|
|
78
|
+
this.startPeriodicSync();
|
|
79
|
+
}
|
|
80
|
+
startPeriodicSync() {
|
|
81
|
+
if (this.periodicSyncTimer)
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
const config = getConfig();
|
|
85
|
+
const intervalSeconds = config.periodicSyncIntervalSeconds;
|
|
86
|
+
// If null or undefined, don't start periodic sync
|
|
87
|
+
if (!intervalSeconds) {
|
|
88
|
+
console.log("[SyncManager] Periodic sync disabled (set to null)");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const intervalMs = intervalSeconds * 1000;
|
|
92
|
+
this.periodicSyncTimer = setInterval(() => {
|
|
93
|
+
this.syncStaleQuerysets();
|
|
94
|
+
}, intervalMs);
|
|
95
|
+
console.log(`[SyncManager] Periodic sync started: ${intervalSeconds}s intervals`);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
// If no config, don't start periodic sync by default
|
|
99
|
+
console.log("[SyncManager] No config found, periodic sync disabled by default");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
syncStaleQuerysets() {
|
|
103
|
+
let syncedCount = 0;
|
|
104
|
+
// Sync all followed querysets - keep it simple
|
|
105
|
+
const querysetRegistry = this.registries.get("QuerysetStoreRegistry");
|
|
106
|
+
if (querysetRegistry) {
|
|
107
|
+
for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
|
|
108
|
+
// Only sync if this store is actually being followed
|
|
109
|
+
const isFollowed = this.isStoreFollowed(querysetRegistry, semanticKey);
|
|
110
|
+
if (this.followAllQuerysets || isFollowed) {
|
|
111
|
+
store.sync();
|
|
112
|
+
syncedCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (syncedCount > 0) {
|
|
117
|
+
console.log(`[SyncManager] Periodic sync: ${syncedCount} stores synced`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
isStoreFollowed(registry, semanticKey) {
|
|
121
|
+
const followingQuerysets = registry.followingQuerysets.get(semanticKey);
|
|
122
|
+
if (!followingQuerysets)
|
|
123
|
+
return false;
|
|
124
|
+
return [...followingQuerysets].some((queryset) => {
|
|
125
|
+
return this.isQuerysetFollowed(queryset);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
cleanup() {
|
|
129
|
+
if (this.periodicSyncTimer) {
|
|
130
|
+
clearInterval(this.periodicSyncTimer);
|
|
131
|
+
this.periodicSyncTimer = null;
|
|
132
|
+
}
|
|
77
133
|
}
|
|
78
134
|
followModel(registry, modelClass) {
|
|
79
135
|
const models = this.followedModels.get(registry) || new Set();
|
|
80
136
|
this.followedModels.set(registry, models);
|
|
81
137
|
if (models.has(modelClass))
|
|
82
138
|
return;
|
|
83
|
-
const alreadyFollowed = [...this.followedModels.values()].some(set => set.has(modelClass));
|
|
139
|
+
const alreadyFollowed = [...this.followedModels.values()].some((set) => set.has(modelClass));
|
|
84
140
|
models.add(modelClass);
|
|
85
141
|
if (!alreadyFollowed) {
|
|
86
142
|
getEventReceiver(modelClass.configKey)?.subscribe(modelClass.modelName, this.handleEvent);
|
|
@@ -91,7 +147,7 @@ export class SyncManager {
|
|
|
91
147
|
if (!models)
|
|
92
148
|
return;
|
|
93
149
|
models.delete(modelClass);
|
|
94
|
-
const stillFollowed = [...this.followedModels.values()].some(set => set.has(modelClass));
|
|
150
|
+
const stillFollowed = [...this.followedModels.values()].some((set) => set.has(modelClass));
|
|
95
151
|
if (!stillFollowed) {
|
|
96
152
|
getEventReceiver(modelClass.configKey)?.unsubscribe(modelClass.modelName, this.handleEvent);
|
|
97
153
|
}
|
|
@@ -104,10 +160,10 @@ export class SyncManager {
|
|
|
104
160
|
this.registries.delete(registry.constructor.name);
|
|
105
161
|
}
|
|
106
162
|
isQuerysetFollowed(queryset) {
|
|
163
|
+
const activeSemanticKeys = new Set([...this.followedQuerysets].map((qs) => qs.semanticKey));
|
|
107
164
|
let current = queryset;
|
|
108
|
-
// All followed querysets and their descendents get updated
|
|
109
165
|
while (current) {
|
|
110
|
-
if (
|
|
166
|
+
if (activeSemanticKeys.has(current.semanticKey)) {
|
|
111
167
|
return true;
|
|
112
168
|
}
|
|
113
169
|
current = current.__parent;
|
|
@@ -127,7 +183,7 @@ export class SyncManager {
|
|
|
127
183
|
const followingQuerysets = registry.followingQuerysets.get(semanticKey);
|
|
128
184
|
if (followingQuerysets) {
|
|
129
185
|
// Use some() to break early when we find a match
|
|
130
|
-
const shouldSync = [...followingQuerysets].some(queryset => {
|
|
186
|
+
const shouldSync = [...followingQuerysets].some((queryset) => {
|
|
131
187
|
return this.isQuerysetFollowed(queryset);
|
|
132
188
|
});
|
|
133
189
|
if (shouldSync) {
|