@statezero/core 0.2.9 → 0.2.11
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/vue/reactivity.js +3 -1
- package/dist/flavours/django/makeApiCall.js +3 -4
- package/dist/flavours/django/tempPk.d.ts +12 -0
- package/dist/flavours/django/tempPk.js +43 -0
- package/dist/syncEngine/registries/modelStoreRegistry.d.ts +1 -0
- package/dist/syncEngine/registries/modelStoreRegistry.js +8 -1
- package/dist/syncEngine/stores/modelStore.d.ts +4 -1
- package/dist/syncEngine/stores/modelStore.js +108 -33
- package/dist/syncEngine/stores/operation.js +6 -1
- package/package.json +1 -1
|
@@ -27,7 +27,9 @@ export function ModelAdaptor(modelInstance, reactivityFn = reactive) {
|
|
|
27
27
|
const renderHandler = (eventData) => {
|
|
28
28
|
const isRef = reactivityFn === ref;
|
|
29
29
|
const model = isRef ? wrapper.value : wrapper;
|
|
30
|
-
|
|
30
|
+
const modelPk = model[pkField];
|
|
31
|
+
// Check if this model's pk is in the event's pks array
|
|
32
|
+
if (eventData.pks && eventData.pks.includes(modelPk)) {
|
|
31
33
|
if (isRef) {
|
|
32
34
|
wrapper.value.touch();
|
|
33
35
|
}
|
|
@@ -44,10 +44,9 @@ export function processIncludedEntities(modelStoreRegistry, included, ModelClass
|
|
|
44
44
|
pksSet.add(Number(pk));
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
// Register
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
47
|
+
// Register all entities in the model store in a single batch call
|
|
48
|
+
const entities = Object.values(entityMap);
|
|
49
|
+
modelStoreRegistry.setEntities(EntityClass, entities);
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
catch (error) {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register a callback to be notified when a temp pk is resolved to a real pk
|
|
3
|
+
* @param {Function} callback - Called with (tempPk, realPk) when resolved
|
|
4
|
+
* @returns {Function} - Unsubscribe function
|
|
5
|
+
*/
|
|
6
|
+
export function onTempPkResolved(callback: Function): Function;
|
|
1
7
|
/**
|
|
2
8
|
* Check if a string contains a temporary PK template
|
|
3
9
|
* @param {string} str - The string to check
|
|
@@ -12,6 +18,12 @@ export function createTempPk(uuid: any): string;
|
|
|
12
18
|
* Register a real PK for a temporary PK
|
|
13
19
|
*/
|
|
14
20
|
export function setRealPk(uuid: any, realPk: any): void;
|
|
21
|
+
/**
|
|
22
|
+
* Get the real PK for a temp PK if it exists in the registry
|
|
23
|
+
* @param {any} pk - The pk to resolve (may be temp or real)
|
|
24
|
+
* @returns {any} - The real pk if found, otherwise the original pk
|
|
25
|
+
*/
|
|
26
|
+
export function resolveToRealPk(pk: any): any;
|
|
15
27
|
/**
|
|
16
28
|
* Replace all temporary PKs in an object (or string) with their real values
|
|
17
29
|
*/
|
|
@@ -2,6 +2,17 @@ import Handlebars from 'handlebars';
|
|
|
2
2
|
import superjson from 'superjson';
|
|
3
3
|
// Simple map to store temporary PK to real PK mappings
|
|
4
4
|
export const tempPkMap = new Map();
|
|
5
|
+
// Callbacks to notify when a temp pk is resolved to a real pk
|
|
6
|
+
const onResolveCallbacks = new Set();
|
|
7
|
+
/**
|
|
8
|
+
* Register a callback to be notified when a temp pk is resolved to a real pk
|
|
9
|
+
* @param {Function} callback - Called with (tempPk, realPk) when resolved
|
|
10
|
+
* @returns {Function} - Unsubscribe function
|
|
11
|
+
*/
|
|
12
|
+
export function onTempPkResolved(callback) {
|
|
13
|
+
onResolveCallbacks.add(callback);
|
|
14
|
+
return () => onResolveCallbacks.delete(callback);
|
|
15
|
+
}
|
|
5
16
|
/**
|
|
6
17
|
* Check if a string contains a temporary PK template
|
|
7
18
|
* @param {string} str - The string to check
|
|
@@ -22,9 +33,41 @@ export function createTempPk(uuid) {
|
|
|
22
33
|
* Register a real PK for a temporary PK
|
|
23
34
|
*/
|
|
24
35
|
export function setRealPk(uuid, realPk) {
|
|
36
|
+
const tempPk = `{{TempPK_${uuid}}}`;
|
|
25
37
|
const key = `"TempPK_${uuid}"`;
|
|
26
38
|
const value = typeof realPk === 'string' ? `"${realPk}"` : String(realPk);
|
|
27
39
|
tempPkMap.set(key, value);
|
|
40
|
+
// Notify listeners so they can migrate cache entries
|
|
41
|
+
for (const cb of onResolveCallbacks) {
|
|
42
|
+
try {
|
|
43
|
+
cb(tempPk, realPk);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
console.warn('[tempPk] onResolve callback error:', e);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get the real PK for a temp PK if it exists in the registry
|
|
52
|
+
* @param {any} pk - The pk to resolve (may be temp or real)
|
|
53
|
+
* @returns {any} - The real pk if found, otherwise the original pk
|
|
54
|
+
*/
|
|
55
|
+
export function resolveToRealPk(pk) {
|
|
56
|
+
if (!containsTempPk(String(pk)))
|
|
57
|
+
return pk;
|
|
58
|
+
// Extract the uuid from {{TempPK_uuid}}
|
|
59
|
+
const match = String(pk).match(/\{\{\s*TempPK_([^}\s]+)\s*\}\}/);
|
|
60
|
+
if (!match)
|
|
61
|
+
return pk;
|
|
62
|
+
const key = `"TempPK_${match[1]}"`;
|
|
63
|
+
const realPkStr = tempPkMap.get(key);
|
|
64
|
+
if (!realPkStr)
|
|
65
|
+
return pk;
|
|
66
|
+
// Parse the real pk (remove quotes if string, parse if number)
|
|
67
|
+
if (realPkStr.startsWith('"') && realPkStr.endsWith('"')) {
|
|
68
|
+
return realPkStr.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
return parseInt(realPkStr, 10);
|
|
28
71
|
}
|
|
29
72
|
/**
|
|
30
73
|
* Replace all temporary PKs in an object (or string) with their real values
|
|
@@ -6,5 +6,6 @@ export class ModelStoreRegistry {
|
|
|
6
6
|
getStore(modelClass: any): any;
|
|
7
7
|
getEntity(modelClass: any, pk: any): any;
|
|
8
8
|
setEntity(modelClass: any, pk: any, data: any): any;
|
|
9
|
+
setEntities(modelClass: any, entities: any): void;
|
|
9
10
|
}
|
|
10
11
|
export const modelStoreRegistry: ModelStoreRegistry;
|
|
@@ -20,7 +20,7 @@ export class ModelStoreRegistry {
|
|
|
20
20
|
const fetchModels = async ({ pks, modelClass }) => {
|
|
21
21
|
return await modelClass.objects.filter({
|
|
22
22
|
[`${modelClass.primaryKeyField}__in`]: pks
|
|
23
|
-
}).fetch();
|
|
23
|
+
}).fetch().serialize();
|
|
24
24
|
};
|
|
25
25
|
this._stores.set(modelClass, new ModelStore(modelClass, fetchModels, null, null));
|
|
26
26
|
this.syncManager.followModel(this, modelClass);
|
|
@@ -51,6 +51,13 @@ export class ModelStoreRegistry {
|
|
|
51
51
|
store.addToGroundTruth([data]);
|
|
52
52
|
return data;
|
|
53
53
|
}
|
|
54
|
+
// Batch add or update entities in the store ground truth
|
|
55
|
+
setEntities(modelClass, entities) {
|
|
56
|
+
if (isNil(modelClass) || !entities?.length)
|
|
57
|
+
return;
|
|
58
|
+
const store = this.getStore(modelClass);
|
|
59
|
+
store.addToGroundTruth(entities);
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
62
|
// Export singleton instance
|
|
56
63
|
export const modelStoreRegistry = new ModelStoreRegistry();
|
|
@@ -9,6 +9,7 @@ export class ModelStore {
|
|
|
9
9
|
modelCache: Cache;
|
|
10
10
|
_lastRenderedData: Map<any, any>;
|
|
11
11
|
renderCallbacks: Set<any>;
|
|
12
|
+
_unsubscribeTempPk: Function;
|
|
12
13
|
registerRenderCallback(callback: any): () => boolean;
|
|
13
14
|
/**
|
|
14
15
|
* Load operations from data and add them to the operations map,
|
|
@@ -44,7 +45,9 @@ export class ModelStore {
|
|
|
44
45
|
* @param {QuerysetStoreRegistry} querysetStoreRegistry - The registry to check for queryset references
|
|
45
46
|
*/
|
|
46
47
|
pruneUnreferencedInstances(querysetStoreRegistry: QuerysetStoreRegistry): void;
|
|
47
|
-
render(pks?: null, optimistic?: boolean): any
|
|
48
|
+
render(pks?: null, optimistic?: boolean, useCache?: boolean): any;
|
|
49
|
+
_renderFromCache(pks: any): any;
|
|
50
|
+
_renderFresh(pks?: null, optimistic?: boolean): any[];
|
|
48
51
|
sync(pks?: null): Promise<void>;
|
|
49
52
|
}
|
|
50
53
|
import { Cache } from '../cache/cache.js';
|
|
@@ -2,52 +2,62 @@ import { Operation, Status, Type, operationRegistry } from './operation.js';
|
|
|
2
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
|
-
import { replaceTempPks, containsTempPk } from '../../flavours/django/tempPk.js';
|
|
6
|
-
const emitEvents = (store,
|
|
7
|
-
if (!
|
|
5
|
+
import { replaceTempPks, containsTempPk, resolveToRealPk, onTempPkResolved } from '../../flavours/django/tempPk.js';
|
|
6
|
+
const emitEvents = (store, event) => {
|
|
7
|
+
if (!event || !event.pks || event.pks.length === 0)
|
|
8
8
|
return;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
9
|
+
// Get prior values for these pks in one pass (before computing new)
|
|
10
|
+
const lastRenderedDataArray = event.pks.map(pk => store._lastRenderedData.get(pk) ?? null);
|
|
11
|
+
// Batch render all pks - useCache=false to get fresh data for comparison
|
|
12
|
+
const newRenderedDataArray = store.render(event.pks, true, false);
|
|
13
|
+
// Single equality check on the whole batch
|
|
14
|
+
if (!isEqual(newRenderedDataArray, lastRenderedDataArray)) {
|
|
15
|
+
// Update cache for all pks
|
|
16
|
+
const pkField = store.pkField;
|
|
17
|
+
for (const item of newRenderedDataArray) {
|
|
18
|
+
if (item && item[pkField] != null) {
|
|
19
|
+
store._lastRenderedData.set(item[pkField], item);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Also mark any pks that are now null (deleted)
|
|
23
|
+
for (const pk of event.pks) {
|
|
24
|
+
if (!newRenderedDataArray.some(item => item && item[pkField] === pk)) {
|
|
25
|
+
store._lastRenderedData.set(pk, null);
|
|
26
|
+
}
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
modelEventEmitter.emit(`${store.modelClass.configKey}::${store.modelClass.modelName}::render`, event);
|
|
29
|
+
store.renderCallbacks.forEach((callback) => {
|
|
30
|
+
try {
|
|
31
|
+
callback();
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.warn("Error in model store render callback:", error);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
29
38
|
};
|
|
30
39
|
class EventData {
|
|
31
|
-
constructor(ModelClass,
|
|
40
|
+
constructor(ModelClass, pks) {
|
|
32
41
|
this.ModelClass = ModelClass;
|
|
33
|
-
this.
|
|
42
|
+
this.pks = Array.isArray(pks) ? pks : [pks];
|
|
34
43
|
}
|
|
35
44
|
/**
|
|
36
|
-
*
|
|
45
|
+
* Single event containing all PKs from an operation
|
|
37
46
|
*/
|
|
38
47
|
static fromOperation(operation) {
|
|
39
48
|
const ModelClass = operation.queryset.ModelClass;
|
|
40
49
|
const pkField = ModelClass.primaryKeyField;
|
|
41
|
-
|
|
50
|
+
const pks = operation.instances
|
|
42
51
|
.filter(instance => instance != null && typeof instance === 'object' && pkField in instance)
|
|
43
|
-
.map(instance =>
|
|
52
|
+
.map(instance => instance[pkField]);
|
|
53
|
+
return pks.length > 0 ? new EventData(ModelClass, pks) : null;
|
|
44
54
|
}
|
|
45
55
|
/**
|
|
46
|
-
*
|
|
56
|
+
* Single event containing all unique PKs across multiple operations
|
|
47
57
|
*/
|
|
48
58
|
static fromOperations(operations) {
|
|
49
59
|
if (!operations.length)
|
|
50
|
-
return
|
|
60
|
+
return null;
|
|
51
61
|
const ModelClass = operations[0].queryset.ModelClass;
|
|
52
62
|
const pkField = ModelClass.primaryKeyField;
|
|
53
63
|
const uniquePks = new Set();
|
|
@@ -58,17 +68,17 @@ class EventData {
|
|
|
58
68
|
}
|
|
59
69
|
}
|
|
60
70
|
}
|
|
61
|
-
return
|
|
71
|
+
return uniquePks.size > 0 ? new EventData(ModelClass, Array.from(uniquePks)) : null;
|
|
62
72
|
}
|
|
63
73
|
/**
|
|
64
|
-
*
|
|
74
|
+
* Single event containing all unique PKs from an array of instances
|
|
65
75
|
*/
|
|
66
76
|
static fromInstances(instances, ModelClass) {
|
|
67
77
|
const pkField = ModelClass.primaryKeyField;
|
|
68
78
|
const uniquePks = new Set(instances
|
|
69
79
|
.filter(inst => inst && inst[pkField] != null)
|
|
70
80
|
.map(inst => inst[pkField]));
|
|
71
|
-
return
|
|
81
|
+
return uniquePks.size > 0 ? new EventData(ModelClass, Array.from(uniquePks)) : null;
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
84
|
export class ModelStore {
|
|
@@ -86,6 +96,18 @@ export class ModelStore {
|
|
|
86
96
|
this.modelCache = new Cache("model-cache", {}, this.onHydrated.bind(this));
|
|
87
97
|
this._lastRenderedData = new Map();
|
|
88
98
|
this.renderCallbacks = new Set();
|
|
99
|
+
// Migrate cache entries when temp pks are resolved to real pks
|
|
100
|
+
this._unsubscribeTempPk = onTempPkResolved((tempPk, realPk) => {
|
|
101
|
+
if (this._lastRenderedData.has(tempPk)) {
|
|
102
|
+
const data = this._lastRenderedData.get(tempPk);
|
|
103
|
+
// Update the pk field in the cached data
|
|
104
|
+
if (data && typeof data === 'object') {
|
|
105
|
+
data[this.pkField] = realPk;
|
|
106
|
+
}
|
|
107
|
+
this._lastRenderedData.set(realPk, data);
|
|
108
|
+
this._lastRenderedData.delete(tempPk);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
89
111
|
}
|
|
90
112
|
registerRenderCallback(callback) {
|
|
91
113
|
this.renderCallbacks.add(callback);
|
|
@@ -435,7 +457,60 @@ export class ModelStore {
|
|
|
435
457
|
}
|
|
436
458
|
}
|
|
437
459
|
// Render methods
|
|
438
|
-
render(pks = null, optimistic = true) {
|
|
460
|
+
render(pks = null, optimistic = true, useCache = true) {
|
|
461
|
+
// When rendering specific pks, check cache first
|
|
462
|
+
if (useCache && pks !== null) {
|
|
463
|
+
const pksArray = Array.isArray(pks) ? pks : [pks];
|
|
464
|
+
// Resolve temp pks to real pks for cache lookup
|
|
465
|
+
const resolvedPks = pksArray.map(pk => resolveToRealPk(pk));
|
|
466
|
+
const uncachedPks = [];
|
|
467
|
+
for (const pk of resolvedPks) {
|
|
468
|
+
if (!this._lastRenderedData.has(pk)) {
|
|
469
|
+
uncachedPks.push(pk);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// All pks were cached - return from cache
|
|
473
|
+
if (uncachedPks.length === 0) {
|
|
474
|
+
return this._renderFromCache(resolvedPks);
|
|
475
|
+
}
|
|
476
|
+
// Some or all pks need fresh render
|
|
477
|
+
const pkField = this.pkField;
|
|
478
|
+
if (uncachedPks.length < resolvedPks.length) {
|
|
479
|
+
// Partial cache hit - get cached and render uncached
|
|
480
|
+
const cachedPks = resolvedPks.filter(pk => this._lastRenderedData.has(pk));
|
|
481
|
+
const cached = this._renderFromCache(cachedPks);
|
|
482
|
+
const fresh = this._renderFresh(uncachedPks, optimistic);
|
|
483
|
+
// Update cache for freshly rendered items
|
|
484
|
+
for (const item of fresh) {
|
|
485
|
+
if (item && item[pkField] != null) {
|
|
486
|
+
this._lastRenderedData.set(item[pkField], item);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return [...cached, ...fresh];
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
// All pks uncached - render fresh and update cache
|
|
493
|
+
const fresh = this._renderFresh(resolvedPks, optimistic);
|
|
494
|
+
// Update cache for freshly rendered items
|
|
495
|
+
for (const item of fresh) {
|
|
496
|
+
if (item && item[pkField] != null) {
|
|
497
|
+
this._lastRenderedData.set(item[pkField], item);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return fresh;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Full render (no cache, useCache=false, or pks=null for all)
|
|
504
|
+
return this._renderFresh(pks, optimistic);
|
|
505
|
+
}
|
|
506
|
+
_renderFromCache(pks) {
|
|
507
|
+
// Returns cached rendered data for the given pks
|
|
508
|
+
// This method can be spied on in tests to track cache hits
|
|
509
|
+
return pks
|
|
510
|
+
.map(pk => this._lastRenderedData.get(pk))
|
|
511
|
+
.filter(item => item !== null && item !== undefined);
|
|
512
|
+
}
|
|
513
|
+
_renderFresh(pks = null, optimistic = true) {
|
|
439
514
|
const pksSet = pks === null
|
|
440
515
|
? null
|
|
441
516
|
: pks instanceof Set
|
|
@@ -14,6 +14,7 @@ import { MAX, v7 as uuidv7 } from "uuid";
|
|
|
14
14
|
import { isNil } from 'lodash-es';
|
|
15
15
|
import mitt from 'mitt';
|
|
16
16
|
import { tempPkMap } from "../../flavours/django/tempPk";
|
|
17
|
+
import { modelStoreRegistry } from "../registries/modelStoreRegistry.js";
|
|
17
18
|
export const operationEvents = mitt();
|
|
18
19
|
export const Status = {
|
|
19
20
|
CREATED: 'operation:created',
|
|
@@ -73,7 +74,11 @@ export class Operation {
|
|
|
73
74
|
throw new Error(`All operation instances must be objects with the '${pkField}' field`);
|
|
74
75
|
}
|
|
75
76
|
__classPrivateFieldSet(this, _Operation__instances, instances, "f");
|
|
76
|
-
|
|
77
|
+
// Batch render to warm the cache, then serialize each instance to get plain objects
|
|
78
|
+
const pks = instances.map(i => i[pkField]);
|
|
79
|
+
const store = modelStoreRegistry.getStore(ModelClass);
|
|
80
|
+
store.render(pks, true, false);
|
|
81
|
+
__classPrivateFieldSet(this, _Operation__frozenInstances, instances.map(i => ModelClass.fromPk(i[pkField]).serialize()), "f");
|
|
77
82
|
this.timestamp = data.timestamp || Date.now();
|
|
78
83
|
if (restore)
|
|
79
84
|
return;
|
package/package.json
CHANGED