@statezero/core 0.1.24 → 0.1.26

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.
@@ -23,7 +23,7 @@ export class DateParsingHelpers {
23
23
  // Get field-specific format from schema
24
24
  const formatStrings = {
25
25
  'date': schema.date_format,
26
- 'datetime': schema.datetime_format
26
+ 'date-time': schema.datetime_format
27
27
  };
28
28
  const dateFormat = formatStrings[fieldFormat];
29
29
  // Check if format is supported
@@ -56,13 +56,13 @@ export class DateParsingHelpers {
56
56
  return date;
57
57
  }
58
58
  const fieldFormat = schema.properties[fieldName].format;
59
- if (!["date", "datetime"].includes(fieldFormat)) {
59
+ if (!["date", "date-time"].includes(fieldFormat)) {
60
60
  throw new Error(`Only date and date-time fields can be processed to JS date objects. ${fieldName} has format ${fieldFormat}`);
61
61
  }
62
62
  // Get field-specific format from schema
63
63
  const formatStrings = {
64
64
  'date': schema.date_format,
65
- 'datetime': schema.datetime_format
65
+ 'date-time': schema.datetime_format
66
66
  };
67
67
  const dateFormat = formatStrings[fieldFormat];
68
68
  // Check if format is supported
@@ -89,6 +89,14 @@ export class Model {
89
89
  if (storedValue)
90
90
  value = storedValue[field]; // if stops null -> undefined
91
91
  }
92
+ // Date/DateTime fields need special handling - convert to Date objects
93
+ const dateFormats = ["date", "datetime", "date-time"];
94
+ if (ModelClass.schema &&
95
+ dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
96
+ value) {
97
+ // Let DateParsingHelpers.parseDate throw if it fails
98
+ return DateParsingHelpers.parseDate(value, field, ModelClass.schema);
99
+ }
92
100
  // File/Image fields need special handling - wrap as FileObject
93
101
  const fileFormats = ["file-path", "image-path"];
94
102
  if (ModelClass.schema &&
@@ -191,6 +199,14 @@ export class Model {
191
199
  if (storedValue)
192
200
  value = storedValue[field];
193
201
  }
202
+ // Date/DateTime fields need special handling - convert Date objects to strings for API
203
+ const dateFormats = ["date", "date-time"];
204
+ if (ModelClass.schema &&
205
+ dateFormats.includes(ModelClass.schema.properties[field]?.format) &&
206
+ value instanceof Date) {
207
+ // Let DateParsingHelpers.serializeDate throw if it fails
208
+ return DateParsingHelpers.serializeDate(value, field, ModelClass.schema);
209
+ }
194
210
  return value;
195
211
  }
196
212
  /**
@@ -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 = (modelClass, events) => {
7
- // helper method to emit every event
8
- events.forEach(event => {
9
- modelEventEmitter.emit(`${modelClass.configKey}::${modelClass.modelName}::render`, event);
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.modelClass, EventData.fromOperation(operation));
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.modelClass, EventData.fromOperation(operation));
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.modelClass, EventData.fromOperation(operation));
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.modelClass, EventData.fromOperation(operation));
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.modelClass, EventData.fromOperations(allOps));
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.modelClass, EventData.fromInstances(allOps, this.modelClass));
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.modelClass, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
277
+ emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
267
278
  }
268
279
  _filteredOperations(pks, operations) {
269
280
  if (!pks)
@@ -12,6 +12,10 @@ export class QuerysetStore {
12
12
  pruneThreshold: any;
13
13
  getRootStore: any;
14
14
  qsCache: Cache;
15
+ _lastRenderedPks: any[] | null;
16
+ renderCallbacks: Set<any>;
17
+ _rootUnregister: any;
18
+ _currentRootStore: any;
15
19
  get cacheKey(): any;
16
20
  onHydrated(hydratedData: any): void;
17
21
  setCache(result: any): void;
@@ -29,6 +33,8 @@ export class QuerysetStore {
29
33
  getTrimmedOperations(): any[];
30
34
  getInflightOperations(): any[];
31
35
  prune(): void;
36
+ registerRenderCallback(callback: any): () => boolean;
37
+ _ensureRootRegistration(): void;
32
38
  render(optimistic?: boolean, fromCache?: boolean): any[];
33
39
  renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
34
40
  renderFromData(optimistic?: boolean): any[];
@@ -1,6 +1,6 @@
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';
@@ -29,6 +29,11 @@ export class QuerysetStore {
29
29
  }
30
30
  }
31
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();
32
37
  }
33
38
  // Caching
34
39
  get cacheKey() {
@@ -70,7 +75,20 @@ export class QuerysetStore {
70
75
  return new Set(this.groundTruthPks);
71
76
  }
72
77
  _emitRenderEvent() {
73
- querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
78
+ const newPks = this.render(true, false); // Get current state without using cache
79
+ // Directly compare PK lists. isEqual performs a deep, order-sensitive comparison.
80
+ if (!isEqual(newPks, this._lastRenderedPks)) {
81
+ this._lastRenderedPks = newPks; // Update the cache with the new state
82
+ querysetEventEmitter.emit(`${this.modelClass.configKey}::${this.modelClass.modelName}::queryset::render`, { ast: this.queryset.build(), ModelClass: this.modelClass });
83
+ this.renderCallbacks.forEach((callback) => {
84
+ try {
85
+ callback();
86
+ }
87
+ catch (error) {
88
+ console.warn("Error in render callback:", error);
89
+ }
90
+ });
91
+ }
74
92
  }
75
93
  async addOperation(operation) {
76
94
  this.operationsMap.set(operation.operationId, operation);
@@ -124,7 +142,34 @@ export class QuerysetStore {
124
142
  this.setGroundTruth(renderedPks);
125
143
  this.setOperations(this.getInflightOperations());
126
144
  }
145
+ registerRenderCallback(callback) {
146
+ this.renderCallbacks.add(callback);
147
+ return () => this.renderCallbacks.delete(callback);
148
+ }
149
+ _ensureRootRegistration() {
150
+ if (this.isTemp)
151
+ return;
152
+ const { isRoot, rootStore } = this.getRootStore(this.queryset);
153
+ // If the root store hasn't changed, nothing to do
154
+ if (this._currentRootStore === rootStore) {
155
+ return;
156
+ }
157
+ // Root store changed - clean up old registration if it exists
158
+ if (this._rootUnregister) {
159
+ this._rootUnregister();
160
+ this._rootUnregister = null;
161
+ }
162
+ // Set up new registration if we're derived and have a root store
163
+ if (!isRoot && rootStore) {
164
+ this._rootUnregister = rootStore.registerRenderCallback(() => {
165
+ this._emitRenderEvent();
166
+ });
167
+ }
168
+ // Update current root store reference (could be null now)
169
+ this._currentRootStore = rootStore;
170
+ }
127
171
  render(optimistic = true, fromCache = false) {
172
+ this._ensureRootRegistration();
128
173
  if (fromCache) {
129
174
  const cachedResult = this.qsCache.get(this.cacheKey);
130
175
  if (Array.isArray(cachedResult)) {
@@ -132,7 +177,9 @@ export class QuerysetStore {
132
177
  }
133
178
  }
134
179
  let result;
135
- if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
180
+ if (this.getRootStore &&
181
+ typeof this.getRootStore === "function" &&
182
+ !this.isTemp) {
136
183
  const { isRoot, rootStore } = this.getRootStore(this.queryset);
137
184
  if (!isRoot && rootStore && rootStore.lastSync) {
138
185
  result = this.renderFromRoot(optimistic, rootStore);
@@ -201,7 +248,9 @@ export class QuerysetStore {
201
248
  return;
202
249
  }
203
250
  // Check if we're delegating to a root store
204
- if (this.getRootStore && typeof this.getRootStore === "function" && !this.isTemp) {
251
+ if (this.getRootStore &&
252
+ typeof this.getRootStore === "function" &&
253
+ !this.isTemp) {
205
254
  const { isRoot, rootStore } = this.getRootStore(this.queryset);
206
255
  if (!isRoot && rootStore && rootStore.lastSync) {
207
256
  // We're delegating to a root store - don't sync, just mark as needing sync
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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",