@statezero/core 0.2.10 → 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.
@@ -16,29 +16,3 @@ export function ModelAdaptor(modelInstance: Object, reactivityFn?: Function): an
16
16
  */
17
17
  export function QuerySetAdaptor(liveQuerySet: Object, reactivityFn?: Function): any | import("vue").Ref;
18
18
  export function MetricAdaptor(metric: any): any;
19
- export const modelEventBatcher: ModelEventBatcher;
20
- export const BATCH_THRESHOLD: 50;
21
- /**
22
- * Batches model events and decides whether to use fine-grained (touch) or
23
- * coarse-grained (queryset refresh) reactivity based on batch size.
24
- */
25
- declare class ModelEventBatcher {
26
- pendingTouches: Map<any, any>;
27
- debounceTimers: Map<any, any>;
28
- maxWaitTimers: Map<any, any>;
29
- processQueue: PQueue<import("p-queue/dist/priority-queue.js").default, import("p-queue").QueueAddOptions>;
30
- /**
31
- * Queue a touch call for batching
32
- */
33
- queueTouch(configKey: any, modelName: any, pk: any, wrapper: any): void;
34
- _resetDebounce(modelKey: any): void;
35
- _ensureMaxWait(modelKey: any): void;
36
- _clearTimers(modelKey: any): void;
37
- _flush(modelKey: any): void;
38
- _processBatch(modelKey: any, touchBatch: any): void;
39
- _touchModels(touchBatch: any): void;
40
- _refreshQuerysets(modelKey: any): void;
41
- clear(): void;
42
- }
43
- import PQueue from "p-queue";
44
- export {};
@@ -4,154 +4,9 @@ import { initEventHandler } from "../../syncEngine/stores/operationEventHandlers
4
4
  import { isEqual, isNil } from 'lodash-es';
5
5
  import hash from 'object-hash';
6
6
  import { registerAdapterReset } from "../../reset.js";
7
- import PQueue from "p-queue";
8
7
  initEventHandler();
9
8
  const wrappedQuerysetCache = new Map();
10
9
  const wrappedMetricCache = new Map();
11
- // =============================================================================
12
- // Model Event Batcher - Batches rapid model events to prevent watcher overload
13
- // =============================================================================
14
- const BATCH_THRESHOLD = 50; // Above this, use queryset refresh instead of individual touch
15
- const DEBOUNCE_MS = 16; // ~1 frame, gather rapid events
16
- const MAX_WAIT_MS = 100; // Don't wait longer than this to flush
17
- /**
18
- * Batches model events and decides whether to use fine-grained (touch) or
19
- * coarse-grained (queryset refresh) reactivity based on batch size.
20
- */
21
- class ModelEventBatcher {
22
- constructor() {
23
- // Queue of pending touch calls: Map<modelKey, Map<pk, Set<wrapper>>>
24
- this.pendingTouches = new Map();
25
- // Debounce timers per model
26
- this.debounceTimers = new Map();
27
- // Max wait timers per model
28
- this.maxWaitTimers = new Map();
29
- // Rate-limited queue for processing
30
- this.processQueue = new PQueue({
31
- concurrency: 1,
32
- interval: 16, // ~1 frame
33
- intervalCap: 10 // max 10 flushes per frame
34
- });
35
- }
36
- /**
37
- * Queue a touch call for batching
38
- */
39
- queueTouch(configKey, modelName, pk, wrapper) {
40
- const modelKey = `${configKey}::${modelName}`;
41
- // Initialize maps if needed
42
- if (!this.pendingTouches.has(modelKey)) {
43
- this.pendingTouches.set(modelKey, new Map());
44
- }
45
- const pkMap = this.pendingTouches.get(modelKey);
46
- if (!pkMap.has(pk)) {
47
- pkMap.set(pk, new Set());
48
- }
49
- pkMap.get(pk).add(wrapper);
50
- // Reset debounce timer
51
- this._resetDebounce(modelKey);
52
- // Start max wait timer if not already running
53
- this._ensureMaxWait(modelKey);
54
- }
55
- _resetDebounce(modelKey) {
56
- // Clear existing debounce
57
- if (this.debounceTimers.has(modelKey)) {
58
- clearTimeout(this.debounceTimers.get(modelKey));
59
- }
60
- // Set new debounce
61
- const timer = setTimeout(() => {
62
- this._flush(modelKey);
63
- }, DEBOUNCE_MS);
64
- this.debounceTimers.set(modelKey, timer);
65
- }
66
- _ensureMaxWait(modelKey) {
67
- if (this.maxWaitTimers.has(modelKey)) {
68
- return; // Already waiting
69
- }
70
- const timer = setTimeout(() => {
71
- this._flush(modelKey);
72
- }, MAX_WAIT_MS);
73
- this.maxWaitTimers.set(modelKey, timer);
74
- }
75
- _clearTimers(modelKey) {
76
- if (this.debounceTimers.has(modelKey)) {
77
- clearTimeout(this.debounceTimers.get(modelKey));
78
- this.debounceTimers.delete(modelKey);
79
- }
80
- if (this.maxWaitTimers.has(modelKey)) {
81
- clearTimeout(this.maxWaitTimers.get(modelKey));
82
- this.maxWaitTimers.delete(modelKey);
83
- }
84
- }
85
- _flush(modelKey) {
86
- this._clearTimers(modelKey);
87
- const pkMap = this.pendingTouches.get(modelKey);
88
- if (!pkMap || pkMap.size === 0) {
89
- return;
90
- }
91
- // Take the pending touches and clear
92
- const touchBatch = new Map(pkMap);
93
- pkMap.clear();
94
- // Queue for rate-limited processing
95
- this.processQueue.add(() => this._processBatch(modelKey, touchBatch));
96
- }
97
- _processBatch(modelKey, touchBatch) {
98
- const batchSize = touchBatch.size;
99
- if (batchSize < BATCH_THRESHOLD) {
100
- // Small batch: fine-grained touch for each model
101
- this._touchModels(touchBatch);
102
- }
103
- else {
104
- // Large batch: refresh all querysets for this model instead
105
- this._refreshQuerysets(modelKey);
106
- }
107
- }
108
- _touchModels(touchBatch) {
109
- // Call touch() on each wrapper
110
- for (const [pk, wrappers] of touchBatch) {
111
- for (const wrapper of wrappers) {
112
- try {
113
- wrapper.touch();
114
- }
115
- catch (e) {
116
- console.warn('[ModelEventBatcher] Error touching model:', e);
117
- }
118
- }
119
- }
120
- }
121
- _refreshQuerysets(modelKey) {
122
- // Find all cached querysets for this model and refresh them
123
- // modelKey is "configKey::modelName"
124
- const [configKey, modelName] = modelKey.split('::');
125
- for (const [semanticKey, wrapper] of wrappedQuerysetCache) {
126
- const liveQuerySet = wrapper.original;
127
- if (!liveQuerySet)
128
- continue;
129
- const qs = liveQuerySet.queryset;
130
- if (qs?.ModelClass?.configKey === configKey &&
131
- qs?.ModelClass?.modelName === modelName) {
132
- // Refresh this queryset's reactive wrapper
133
- try {
134
- wrapper.splice(0, wrapper.length);
135
- wrapper.push(...liveQuerySet);
136
- }
137
- catch (e) {
138
- console.warn('[ModelEventBatcher] Error refreshing queryset:', e);
139
- }
140
- }
141
- }
142
- }
143
- clear() {
144
- // Clear all pending state
145
- for (const modelKey of this.pendingTouches.keys()) {
146
- this._clearTimers(modelKey);
147
- }
148
- this.pendingTouches.clear();
149
- this.processQueue.clear();
150
- }
151
- }
152
- const modelEventBatcher = new ModelEventBatcher();
153
- // Export for testing
154
- export { modelEventBatcher, BATCH_THRESHOLD };
155
10
  /**
156
11
  * Adapts a model instance to a Vue reactive object by directly wrapping
157
12
  * the instance and incrementing an internal version on relevant events.
@@ -168,15 +23,19 @@ export function ModelAdaptor(modelInstance, reactivityFn = reactive) {
168
23
  // Make the model instance reactive using the specified function
169
24
  const wrapper = reactivityFn(modelInstance);
170
25
  const eventName = `${configKey}::${modelName}::render`;
171
- // Handler queues touch calls through the batcher for optimal batching
26
+ // Handler bumps version to trigger Vue reactivity when this instance updates
172
27
  const renderHandler = (eventData) => {
173
28
  const isRef = reactivityFn === ref;
174
29
  const model = isRef ? wrapper.value : wrapper;
175
- if (eventData.pk === model[pkField]) {
176
- // Queue the touch through the batcher instead of calling directly
177
- // This allows batching of rapid events and smart routing to queryset refresh for bulk ops
178
- const targetWrapper = isRef ? wrapper.value : wrapper;
179
- modelEventBatcher.queueTouch(configKey, modelName, eventData.pk, targetWrapper);
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)) {
33
+ if (isRef) {
34
+ wrapper.value.touch();
35
+ }
36
+ else {
37
+ wrapper.touch();
38
+ }
180
39
  }
181
40
  };
182
41
  // Subscribe to model events indefinitely
@@ -270,5 +129,4 @@ export function MetricAdaptor(metric) {
270
129
  registerAdapterReset(() => {
271
130
  wrappedQuerysetCache.clear();
272
131
  wrappedMetricCache.clear();
273
- modelEventBatcher.clear();
274
132
  });
@@ -44,10 +44,9 @@ export function processIncludedEntities(modelStoreRegistry, included, ModelClass
44
44
  pksSet.add(Number(pk));
45
45
  }
46
46
  }
47
- // Register each entity in the model store
48
- for (const [pk, entity] of Object.entries(entityMap)) {
49
- modelStoreRegistry.setEntity(EntityClass, pk, entity);
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, events) => {
7
- if (!Array.isArray(events))
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
- 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
- store.renderCallbacks.forEach((callback) => {
20
- try {
21
- callback();
22
- }
23
- catch (error) {
24
- console.warn("Error in model store render callback:", error);
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, pk) {
40
+ constructor(ModelClass, pks) {
32
41
  this.ModelClass = ModelClass;
33
- this.pk = pk;
42
+ this.pks = Array.isArray(pks) ? pks : [pks];
34
43
  }
35
44
  /**
36
- * One event per instance in a single operation
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
- return operation.instances
50
+ const pks = operation.instances
42
51
  .filter(instance => instance != null && typeof instance === 'object' && pkField in instance)
43
- .map(instance => new EventData(ModelClass, instance[pkField]));
52
+ .map(instance => instance[pkField]);
53
+ return pks.length > 0 ? new EventData(ModelClass, pks) : null;
44
54
  }
45
55
  /**
46
- * One event per unique PK across multiple operations
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 Array.from(uniquePks).map(pk => new EventData(ModelClass, pk));
71
+ return uniquePks.size > 0 ? new EventData(ModelClass, Array.from(uniquePks)) : null;
62
72
  }
63
73
  /**
64
- * One event per unique PK in an array of instances
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 Array.from(uniquePks).map(pk => new EventData(ModelClass, pk));
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
- __classPrivateFieldSet(this, _Operation__frozenInstances, instances.map(i => ModelClass.fromPk(i[ModelClass.primaryKeyField]).serialize()), "f");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "type": "module",
5
5
  "module": "ESNext",
6
6
  "description": "The type-safe frontend client for StateZero - connect directly to your backend models with zero boilerplate",