@statezero/core 0.2.46 → 0.2.48

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.
Files changed (36) hide show
  1. package/dist/adaptors/vue/components/LayoutRenderer.d.ts +1 -0
  2. package/dist/adaptors/vue/components/StateZeroDebugPanel.d.ts +1 -0
  3. package/dist/adaptors/vue/components/StateZeroDebugPanel.js +5086 -0
  4. package/dist/adaptors/vue/components/defaults/AlertElement.d.ts +1 -0
  5. package/dist/adaptors/vue/components/defaults/DisplayElement.d.ts +1 -0
  6. package/dist/adaptors/vue/components/defaults/DividerElement.d.ts +1 -0
  7. package/dist/adaptors/vue/components/defaults/ErrorBlock.d.ts +1 -0
  8. package/dist/adaptors/vue/components/defaults/GroupElement.d.ts +1 -0
  9. package/dist/adaptors/vue/components/defaults/LabelElement.d.ts +1 -0
  10. package/dist/adaptors/vue/components/defaults/TabsElement.d.ts +1 -0
  11. package/dist/adaptors/vue/components/defaults/index.d.ts +7 -0
  12. package/dist/adaptors/vue/components/index.d.ts +2 -0
  13. package/dist/adaptors/vue/components/index.js +1 -0
  14. package/dist/adaptors/vue/composables.js +0 -1
  15. package/dist/adaptors/vue/index.d.ts +1 -1
  16. package/dist/adaptors/vue/index.js +1 -1
  17. package/dist/config.js +11 -1
  18. package/dist/core/eventReceivers.d.ts +3 -3
  19. package/dist/core/eventReceivers.js +6 -3
  20. package/dist/core.css +1 -0
  21. package/dist/debug/statezeroDebug.d.ts +8 -0
  22. package/dist/debug/statezeroDebug.js +118 -0
  23. package/dist/flavours/django/errors.js +0 -1
  24. package/dist/flavours/django/makeApiCall.js +71 -0
  25. package/dist/reset.js +0 -2
  26. package/dist/syncEngine/cache/cache.js +0 -1
  27. package/dist/syncEngine/registries/querysetStoreRegistry.js +42 -0
  28. package/dist/syncEngine/stores/metricStore.js +0 -4
  29. package/dist/syncEngine/stores/modelStore.js +0 -3
  30. package/dist/syncEngine/stores/operation.js +0 -1
  31. package/dist/syncEngine/stores/operationEventHandlers.js +87 -2
  32. package/dist/syncEngine/stores/querysetStore.js +56 -4
  33. package/dist/syncEngine/sync.js +11 -10
  34. package/dist/vue-entry.d.ts +2 -1
  35. package/dist/vue-entry.js +2 -2
  36. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { QuerySet } from '../../flavours/django/querySet.js';
7
7
  import { isEqual, isNil } from 'lodash-es';
8
8
  import hash from 'object-hash';
9
9
  import { filter, applyOrderBy } from '../../filtering/localFiltering.js';
10
+ import { recordDebugEvent } from '../../debug/statezeroDebug.js';
10
11
  /**
11
12
  * Check if two model classes represent the same model.
12
13
  * Uses property comparison instead of reference equality to handle
@@ -38,11 +39,31 @@ function routeCreateOperation(operation, applyAction) {
38
39
  Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
39
40
  if (!isSameModel(store.modelClass, modelClass))
40
41
  return;
42
+ recordDebugEvent({
43
+ type: "routing",
44
+ operationId: operation.operationId,
45
+ operationType: operation.type,
46
+ semanticKey,
47
+ modelName: store.modelClass?.modelName,
48
+ configKey: store.modelClass?.configKey,
49
+ phase: "start",
50
+ });
41
51
  const serializerOptions = store.queryset?._serializerOptions || {};
42
52
  const { offset, limit } = serializerOptions;
43
53
  // Offset > 0: can't determine position based on slicing
44
54
  if (offset != null && offset > 0) {
45
55
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
56
+ recordDebugEvent({
57
+ type: "routing",
58
+ operationId: operation.operationId,
59
+ operationType: operation.type,
60
+ semanticKey,
61
+ modelName: store.modelClass?.modelName,
62
+ configKey: store.modelClass?.configKey,
63
+ decision: "offset>0",
64
+ optimistic: false,
65
+ verify: true,
66
+ });
46
67
  return;
47
68
  }
48
69
  // Evaluate if instances match this queryset's filter
@@ -53,6 +74,17 @@ function routeCreateOperation(operation, applyAction) {
53
74
  if (matchingInstances.length === 0) {
54
75
  // Local filter says no match - trust it (local filtering is robust)
55
76
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
77
+ recordDebugEvent({
78
+ type: "routing",
79
+ operationId: operation.operationId,
80
+ operationType: operation.type,
81
+ semanticKey,
82
+ modelName: store.modelClass?.modelName,
83
+ configKey: store.modelClass?.configKey,
84
+ decision: "filter:no-match",
85
+ optimistic: false,
86
+ verify: false,
87
+ });
56
88
  return;
57
89
  }
58
90
  // Item matches filter - check limit
@@ -75,6 +107,17 @@ function routeCreateOperation(operation, applyAction) {
75
107
  // If some ground truth items couldn't be rendered, we can't trust local sort
76
108
  if (currentItems.length < store.groundTruthPks.length) {
77
109
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
110
+ recordDebugEvent({
111
+ type: "routing",
112
+ operationId: operation.operationId,
113
+ operationType: operation.type,
114
+ semanticKey,
115
+ modelName: store.modelClass?.modelName,
116
+ configKey: store.modelClass?.configKey,
117
+ decision: "limit:ordering:incomplete-render",
118
+ optimistic: false,
119
+ verify: true,
120
+ });
78
121
  return;
79
122
  }
80
123
  const allItems = [...currentItems, ...matchingInstances];
@@ -88,14 +131,47 @@ function routeCreateOperation(operation, applyAction) {
88
131
  if (anyInTopN) {
89
132
  applyAction(store);
90
133
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
134
+ recordDebugEvent({
135
+ type: "routing",
136
+ operationId: operation.operationId,
137
+ operationType: operation.type,
138
+ semanticKey,
139
+ modelName: store.modelClass?.modelName,
140
+ configKey: store.modelClass?.configKey,
141
+ decision: "limit:ordering:in-top",
142
+ optimistic: true,
143
+ verify: false,
144
+ });
91
145
  }
92
146
  else {
93
147
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
148
+ recordDebugEvent({
149
+ type: "routing",
150
+ operationId: operation.operationId,
151
+ operationType: operation.type,
152
+ semanticKey,
153
+ modelName: store.modelClass?.modelName,
154
+ configKey: store.modelClass?.configKey,
155
+ decision: "limit:ordering:out",
156
+ optimistic: false,
157
+ verify: false,
158
+ });
94
159
  }
95
160
  }
96
161
  else {
97
162
  // No ordering - new items go at end, won't be in first N
98
163
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
164
+ recordDebugEvent({
165
+ type: "routing",
166
+ operationId: operation.operationId,
167
+ operationType: operation.type,
168
+ semanticKey,
169
+ modelName: store.modelClass?.modelName,
170
+ configKey: store.modelClass?.configKey,
171
+ decision: "limit:no-ordering",
172
+ optimistic: false,
173
+ verify: false,
174
+ });
99
175
  }
100
176
  return;
101
177
  }
@@ -104,6 +180,17 @@ function routeCreateOperation(operation, applyAction) {
104
180
  // Matches filter, has room or no limit - DEFINITELY_YES
105
181
  applyAction(store);
106
182
  operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
183
+ recordDebugEvent({
184
+ type: "routing",
185
+ operationId: operation.operationId,
186
+ operationType: operation.type,
187
+ semanticKey,
188
+ modelName: store.modelClass?.modelName,
189
+ configKey: store.modelClass?.configKey,
190
+ decision: "match:has-room",
191
+ optimistic: true,
192
+ verify: false,
193
+ });
107
194
  });
108
195
  }
109
196
  /**
@@ -351,7 +438,6 @@ export function initEventHandler() {
351
438
  operationEvents.on(Status.CONFIRMED, handleOperationConfirmed);
352
439
  operationEvents.on(Status.REJECTED, handleOperationRejected);
353
440
  operationEvents.on(Status.MUTATED, handleOperationMutated);
354
- console.log('Operation event handler initialized');
355
441
  }
356
442
  /**
357
443
  * Clean up by removing all event listeners
@@ -362,5 +448,4 @@ export function cleanupEventHandler() {
362
448
  operationEvents.off(Status.CONFIRMED, handleOperationConfirmed);
363
449
  operationEvents.off(Status.REJECTED, handleOperationRejected);
364
450
  operationEvents.off(Status.MUTATED, handleOperationMutated);
365
- console.log('Operation event handler cleaned up');
366
451
  }
@@ -6,6 +6,7 @@ import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
6
6
  import { processIncludedEntities } from '../../flavours/django/makeApiCall.js';
7
7
  import { Cache } from '../cache/cache.js';
8
8
  import { filter } from "../../filtering/localFiltering.js";
9
+ import { recordDebugEvent } from "../../debug/statezeroDebug.js";
9
10
  export class QuerysetStore {
10
11
  constructor(modelClass, fetchFn, queryset, initialGroundTruthPks = null, initialOperations = null, options = {}) {
11
12
  this.modelClass = modelClass;
@@ -48,7 +49,6 @@ export class QuerysetStore {
48
49
  if (this.groundTruthPks.length === 0 && this.operationsMap.size === 0) {
49
50
  const cached = this.qsCache.get(this.cacheKey);
50
51
  if (!isNil(cached) && !isEmpty(cached)) {
51
- console.log(`[QuerysetStore] Hydrated ${this.modelClass.modelName} queryset with ${cached.length} items from cache`);
52
52
  this.setGroundTruth(cached);
53
53
  }
54
54
  }
@@ -186,6 +186,17 @@ export class QuerysetStore {
186
186
  if (fromCache) {
187
187
  const cachedResult = this.qsCache.get(this.cacheKey);
188
188
  if (Array.isArray(cachedResult)) {
189
+ recordDebugEvent({
190
+ type: "render",
191
+ source: "cache",
192
+ semanticKey: this.queryset.semanticKey,
193
+ modelName: this.modelClass.modelName,
194
+ configKey: this.modelClass.configKey,
195
+ optimistic,
196
+ groundTruthCount: this.groundTruthPks.length,
197
+ operationsCount: this.operationsMap.size,
198
+ resultCount: cachedResult.length,
199
+ });
189
200
  return cachedResult;
190
201
  }
191
202
  }
@@ -197,12 +208,29 @@ export class QuerysetStore {
197
208
  : this.renderFromData(optimistic);
198
209
  // Validate against model store and apply local filtering/sorting
199
210
  let result = this._getValidatedAndFilteredPks(pks);
211
+ const preLimitCount = result.length;
200
212
  // Apply pagination limit
201
213
  const limit = this.queryset.build().serializerOptions?.limit;
202
214
  if (limit) {
203
215
  result = result.slice(0, limit);
204
216
  }
205
217
  this.setCache(result);
218
+ recordDebugEvent({
219
+ type: "render",
220
+ source: this.groundTruthPks.length === 0 && this.lastSync === null
221
+ ? "modelStore"
222
+ : "groundTruth",
223
+ semanticKey: this.queryset.semanticKey,
224
+ modelName: this.modelClass.modelName,
225
+ configKey: this.modelClass.configKey,
226
+ optimistic,
227
+ groundTruthCount: this.groundTruthPks.length,
228
+ operationsCount: this.operationsMap.size,
229
+ rawCount: Array.isArray(pks) ? pks.length : 0,
230
+ filteredCount: preLimitCount,
231
+ resultCount: result.length,
232
+ limit,
233
+ });
206
234
  return result;
207
235
  }
208
236
  renderFromData(optimistic = true) {
@@ -265,7 +293,14 @@ export class QuerysetStore {
265
293
  return;
266
294
  }
267
295
  this.isSyncing = true;
268
- console.log(`[${id}] Starting sync...`);
296
+ recordDebugEvent({
297
+ type: "sync",
298
+ phase: "start",
299
+ semanticKey: this.queryset.semanticKey,
300
+ modelName: this.modelClass.modelName,
301
+ configKey: this.modelClass.configKey,
302
+ canonicalId: canonical_id,
303
+ });
269
304
  try {
270
305
  const response = await this.fetchFn({
271
306
  ast: this.queryset.build(),
@@ -274,7 +309,6 @@ export class QuerysetStore {
274
309
  });
275
310
  const { data, included } = response;
276
311
  if (!isNil(data)) {
277
- console.log(`[${id}] Sync fetch completed. Received: ${JSON.stringify(data)}.`);
278
312
  // Clear previous included PKs tracking before processing new data
279
313
  this.includedPks.clear();
280
314
  // Persist all instances (including nested) to the model store
@@ -283,10 +317,28 @@ export class QuerysetStore {
283
317
  }
284
318
  this.setOperations(this.getInflightOperations());
285
319
  this.lastSync = Date.now();
286
- console.log(`[${id}] Sync completed.`);
320
+ recordDebugEvent({
321
+ type: "sync",
322
+ phase: "success",
323
+ semanticKey: this.queryset.semanticKey,
324
+ modelName: this.modelClass.modelName,
325
+ configKey: this.modelClass.configKey,
326
+ canonicalId: canonical_id,
327
+ dataCount: Array.isArray(data) ? data.length : 0,
328
+ includedCount: included ? Object.keys(included).length : 0,
329
+ });
287
330
  }
288
331
  catch (e) {
289
332
  console.error(`[${id}] Failed to sync ground truth:`, e);
333
+ recordDebugEvent({
334
+ type: "sync",
335
+ phase: "error",
336
+ semanticKey: this.queryset.semanticKey,
337
+ modelName: this.modelClass.modelName,
338
+ configKey: this.modelClass.configKey,
339
+ canonicalId: canonical_id,
340
+ message: e?.message,
341
+ });
290
342
  }
291
343
  finally {
292
344
  this.isSyncing = false;
@@ -2,6 +2,7 @@ import { getAllEventReceivers } from "../core/eventReceivers.js";
2
2
  import { operationRegistry } from "./stores/operation.js";
3
3
  import { initializeAllEventReceivers } from "../config.js";
4
4
  import { getEventReceiver } from "../core/eventReceivers.js";
5
+ import { recordDebugEvent } from "../debug/statezeroDebug.js";
5
6
  import { querysetStoreRegistry, QuerysetStoreRegistry, } from "./registries/querysetStoreRegistry.js";
6
7
  import { modelStoreRegistry, ModelStoreRegistry, } from "./registries/modelStoreRegistry.js";
7
8
  import { metricRegistry, MetricRegistry } from "./registries/metricRegistry.js";
@@ -50,6 +51,16 @@ export class EventPayload {
50
51
  export class SyncManager {
51
52
  constructor() {
52
53
  this.handleEvent = (event) => {
54
+ recordDebugEvent({
55
+ type: "event",
56
+ modelName: event.model,
57
+ configKey: event.configKey,
58
+ semanticKey: `${event.configKey || "default"}::${event.model}`,
59
+ eventType: event.type || event.event,
60
+ instanceCount: Array.isArray(event.instances) ? event.instances.length : 0,
61
+ operationId: event.operation_id,
62
+ socketId: event.socket_id,
63
+ });
53
64
  let payload = new EventPayload(event);
54
65
  let isLocalOperation = operationRegistry.has(payload.operation_id);
55
66
  // Always process metrics immediately (they're lightweight)
@@ -143,18 +154,15 @@ export class SyncManager {
143
154
  const intervalSeconds = config.periodicSyncIntervalSeconds;
144
155
  // If null or undefined, don't start periodic sync
145
156
  if (!intervalSeconds) {
146
- console.log("[SyncManager] Periodic sync disabled (set to null)");
147
157
  return;
148
158
  }
149
159
  const intervalMs = intervalSeconds * 1000;
150
160
  this.periodicSyncTimer = setInterval(() => {
151
161
  this.syncStaleQuerysets();
152
162
  }, intervalMs);
153
- console.log(`[SyncManager] Periodic sync started: ${intervalSeconds}s intervals`);
154
163
  }
155
164
  catch (error) {
156
165
  // If no config, don't start periodic sync by default
157
- console.log("[SyncManager] No config found, periodic sync disabled by default");
158
166
  }
159
167
  }
160
168
  async syncStaleQuerysets() {
@@ -176,7 +184,6 @@ export class SyncManager {
176
184
  }
177
185
  }
178
186
  if (storesToSync.length > 0) {
179
- console.log(`[SyncManager] Periodic sync: syncing ${storesToSync.length} stores`);
180
187
  // Run all groupSync calls in parallel - they coordinate via shared promise cache
181
188
  await Promise.all(storesToSync.map(store => querysetRegistry.groupSync(store.queryset, operationId, dbSyncedKeys)));
182
189
  }
@@ -268,7 +275,6 @@ export class SyncManager {
268
275
  }
269
276
  if (storesToSync.length === 0)
270
277
  return;
271
- console.log(`[SyncManager] Syncing ${storesToSync.length} querysets needing verification for operation ${operationId}`);
272
278
  const syncOperationId = `maybe-sync-${uuidv7()}`;
273
279
  const dbSyncedKeys = new Set([...this.followedQuerysets]
274
280
  .map(qs => qs.semanticKey)
@@ -291,7 +297,6 @@ export class SyncManager {
291
297
  const waitTime = Date.now() - this.batchStartTime;
292
298
  this.eventBatch.clear();
293
299
  this.batchStartTime = null;
294
- console.log(`[SyncManager] Processing batch of ${events.length} events (reason: ${reason}, waited: ${waitTime}ms)`);
295
300
  // Group events by model for efficient processing
296
301
  const eventsByModel = new Map();
297
302
  events.forEach((event) => {
@@ -350,8 +355,6 @@ export class SyncManager {
350
355
  // Pick canonical_id from the latest event by server_ts_ms
351
356
  const latestEvent = allEvents.reduce((latest, evt) => (evt.server_ts_ms || 0) > (latest.server_ts_ms || 0) ? evt : latest, allEvents[0]);
352
357
  const canonical_id = latestEvent.canonical_id;
353
- // Sync all relevant stores for this model
354
- console.log(`[SyncManager] Syncing ${storesToSync.length} queryset stores for ${representativeEvent.model}`);
355
358
  // Generate operationId for this batch - querysets in same chain will coordinate
356
359
  const operationId = `remote-event-${uuidv7()}`;
357
360
  // Get dbSynced keys (followed querysets that have stores)
@@ -376,7 +379,6 @@ export class SyncManager {
376
379
  }
377
380
  // Check if this queryset (or any parent) is being followed
378
381
  if (this.isQuerysetFollowed(entry.queryset)) {
379
- console.log(`syncing metric store for key: ${key}`);
380
382
  entry.store.sync();
381
383
  }
382
384
  }
@@ -422,7 +424,6 @@ export class SyncManager {
422
424
  }
423
425
  });
424
426
  if (pksToSync.length > 0) {
425
- console.log(`[SyncManager] Syncing ${pksToSync.length} nested-only PKs for ${event.model}: ${pksToSync}`);
426
427
  modelStore.sync(pksToSync);
427
428
  }
428
429
  }
@@ -4,6 +4,7 @@ import { MetricAdaptor } from './adaptors/vue/index.js';
4
4
  import { useQueryset } from './adaptors/vue/index.js';
5
5
  import { querysets } from './adaptors/vue/index.js';
6
6
  import { LayoutRenderer } from './adaptors/vue/index.js';
7
+ import { StateZeroDebugPanel } from './adaptors/vue/index.js';
7
8
  import { AlertElement } from './adaptors/vue/index.js';
8
9
  import { LabelElement } from './adaptors/vue/index.js';
9
10
  import { DividerElement } from './adaptors/vue/index.js';
@@ -12,4 +13,4 @@ import { GroupElement } from './adaptors/vue/index.js';
12
13
  import { TabsElement } from './adaptors/vue/index.js';
13
14
  import { ErrorBlock } from './adaptors/vue/index.js';
14
15
  import { createDefaultComponents } from './adaptors/vue/index.js';
15
- export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets, LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
16
+ export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets, LayoutRenderer, StateZeroDebugPanel, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
package/dist/vue-entry.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets } from './adaptors/vue/index.js';
2
- import { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './adaptors/vue/index.js';
2
+ import { LayoutRenderer, StateZeroDebugPanel, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './adaptors/vue/index.js';
3
3
  export {
4
4
  // Reactivity
5
5
  ModelAdaptor, QuerySetAdaptor, MetricAdaptor, useQueryset, querysets,
6
6
  // Layout components
7
- LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
7
+ LayoutRenderer, StateZeroDebugPanel, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.46",
3
+ "version": "0.2.48",
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",