@statezero/core 0.2.45 → 0.2.47

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 (39) 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 +781 -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.d.ts +1 -1
  28. package/dist/syncEngine/registries/querysetStoreRegistry.js +45 -3
  29. package/dist/syncEngine/stores/metricStore.js +0 -4
  30. package/dist/syncEngine/stores/modelStore.js +0 -3
  31. package/dist/syncEngine/stores/operation.js +0 -1
  32. package/dist/syncEngine/stores/operationEventHandlers.js +87 -2
  33. package/dist/syncEngine/stores/querysetStore.d.ts +1 -1
  34. package/dist/syncEngine/stores/querysetStore.js +58 -5
  35. package/dist/syncEngine/sync.d.ts +2 -0
  36. package/dist/syncEngine/sync.js +17 -11
  37. package/dist/vue-entry.d.ts +2 -1
  38. package/dist/vue-entry.js +2 -2
  39. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ export default AlertElement;
@@ -0,0 +1 @@
1
+ export default DisplayElement;
@@ -0,0 +1 @@
1
+ export default DividerElement;
@@ -0,0 +1 @@
1
+ export default ErrorBlock;
@@ -0,0 +1 @@
1
+ export default GroupElement;
@@ -0,0 +1 @@
1
+ export default LabelElement;
@@ -0,0 +1 @@
1
+ export default TabsElement;
@@ -5,4 +5,11 @@
5
5
  * @returns {Object} Components registry for LayoutRenderer
6
6
  */
7
7
  export function createDefaultComponents(options?: Object): Object;
8
+ import AlertElement from './AlertElement.js';
9
+ import LabelElement from './LabelElement.js';
10
+ import DividerElement from './DividerElement.js';
11
+ import DisplayElement from './DisplayElement.js';
12
+ import GroupElement from './GroupElement.js';
13
+ import TabsElement from './TabsElement.js';
14
+ import ErrorBlock from './ErrorBlock.js';
8
15
  export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock };
@@ -1 +1,3 @@
1
+ export { default as LayoutRenderer } from "./LayoutRenderer.js";
2
+ export { default as StateZeroDebugPanel } from "./StateZeroDebugPanel.js";
1
3
  export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from "./defaults/index.js";
@@ -3,5 +3,6 @@
3
3
  */
4
4
  // Main layout renderer
5
5
  export { default as LayoutRenderer } from './LayoutRenderer.js';
6
+ export { default as StateZeroDebugPanel } from './StateZeroDebugPanel.js';
6
7
  // Default component implementations
7
8
  export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './defaults/index.js';
@@ -43,5 +43,4 @@ registerAdapterReset(() => {
43
43
  // Reset syncManager back to following all querysets since no composables are active
44
44
  syncManager.followAllQuerysets = true;
45
45
  syncManager.followedQuerysets.clear();
46
- console.log("Vue composables state cleared");
47
46
  });
@@ -1,3 +1,3 @@
1
1
  export { useQueryset, querysets } from "./composables.js";
2
2
  export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from "./reactivity.js";
3
- export { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from "./components/index.js";
3
+ export { LayoutRenderer, StateZeroDebugPanel, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from "./components/index.js";
@@ -1,4 +1,4 @@
1
1
  export { useQueryset, querysets } from './composables.js';
2
2
  export { ModelAdaptor, QuerySetAdaptor, MetricAdaptor } from './reactivity.js';
3
3
  // Layout components
4
- export { LayoutRenderer, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './components/index.js';
4
+ export { LayoutRenderer, StateZeroDebugPanel, AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock, createDefaultComponents } from './components/index.js';
package/dist/config.js CHANGED
@@ -159,9 +159,19 @@ export function initializeEventReceiver(backendKey = 'default') {
159
159
  if (!backendConfig.events.pusher.clientOptions.authEndpoint) {
160
160
  throw new ConfigError('Pusher auth endpoint is required for Pusher event receiver.');
161
161
  }
162
+ const baseGetAuthHeaders = backendConfig.events.pusher.clientOptions.getAuthHeaders ||
163
+ backendConfig.getAuthHeaders;
164
+ const syncToken = backendConfig.SYNC_TOKEN;
165
+ const getAuthHeaders = () => {
166
+ const headers = baseGetAuthHeaders ? baseGetAuthHeaders() : {};
167
+ if (syncToken) {
168
+ return { ...headers, "X-StateZero-Sync-Token": syncToken };
169
+ }
170
+ return headers;
171
+ };
162
172
  const clientOptions = {
163
173
  ...backendConfig.events.pusher.clientOptions,
164
- getAuthHeaders: backendConfig.events.pusher.clientOptions.getAuthHeaders || backendConfig.getAuthHeaders
174
+ getAuthHeaders
165
175
  };
166
176
  // Pass the backendKey to the constructor
167
177
  receiver = new PusherEventReceiver({ clientOptions }, backendKey);
@@ -65,9 +65,9 @@ export namespace EventType {
65
65
  */
66
66
  export class PusherEventReceiver {
67
67
  /**
68
- * @param {PusherReceiverOptions} options
69
- * @param {string} configKey - The backend configuration key
70
- */
68
+ * @param {PusherReceiverOptions} options
69
+ * @param {string} configKey - The backend configuration key
70
+ */
71
71
  constructor(options: PusherReceiverOptions, configKey: string);
72
72
  configKey: string;
73
73
  connectionTimeoutId: NodeJS.Timeout;
@@ -58,9 +58,9 @@ export const EventType = {
58
58
  */
59
59
  export class PusherEventReceiver {
60
60
  /**
61
- * @param {PusherReceiverOptions} options
62
- * @param {string} configKey - The backend configuration key
63
- */
61
+ * @param {PusherReceiverOptions} options
62
+ * @param {string} configKey - The backend configuration key
63
+ */
64
64
  constructor(options, configKey) {
65
65
  const { clientOptions, formatChannelName, namespaceResolver } = options;
66
66
  const CONNECTION_TIMEOUT = 10000; // 10 seconds
@@ -152,6 +152,9 @@ Common causes:
152
152
  });
153
153
  channel.bind("pusher:subscription_error", (status) => {
154
154
  console.error(`Subscription error for channel: ${channelName}. Status:`, status);
155
+ if (status === 409) {
156
+ console.error(`%cSync token mismatch on auth for backend ${this.configKey}. Check scenario/prod config.`, "color: red; font-weight: bold;");
157
+ }
155
158
  if (status.status === 401 || status.status === 403) {
156
159
  console.error(`%cAuthentication failed for channel ${channelName}. Check your authEndpoint and server-side permissions.`, "color: orange; font-weight: bold;");
157
160
  }
package/dist/core.css ADDED
@@ -0,0 +1 @@
1
+ .szd[data-v-6d80920c]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;color:#1f2937;background:#fff;border:1px solid #e5e7eb;border-radius:8px;display:flex;flex-direction:column;max-height:600px;overflow:hidden}.szd-header[data-v-6d80920c]{border-bottom:1px solid #e5e7eb;background:#f9fafb}.szd-header__top[data-v-6d80920c]{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;gap:12px}.szd-header__left[data-v-6d80920c]{display:flex;align-items:center;gap:8px}.szd-header__right[data-v-6d80920c]{display:flex;align-items:center;gap:6px}.szd-header__title[data-v-6d80920c]{font-weight:600;font-size:14px}.szd-header__model[data-v-6d80920c]{font-size:12px;color:#1f2937;background:#e5e7eb;padding:2px 8px;border-radius:4px}.szd-header__model--empty[data-v-6d80920c]{color:#9ca3af;background:transparent}.szd-header__badge[data-v-6d80920c]{font-size:11px;color:#6b7280;background:#f3f4f6;padding:2px 6px;border-radius:4px}.szd-header__badge--syncing[data-v-6d80920c]{color:#059669;background:#d1fae5}.szd-tabs[data-v-6d80920c]{display:flex;gap:0;padding:0 12px;border-top:1px solid #e5e7eb}.szd-tabs__tab[data-v-6d80920c]{padding:10px 16px;background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-size:13px;font-weight:500;color:#6b7280;transition:all .15s}.szd-tabs__tab[data-v-6d80920c]:hover{color:#1f2937}.szd-tabs__tab--active[data-v-6d80920c]{color:#2563eb;border-bottom-color:#2563eb}.szd-content[data-v-6d80920c]{flex:1;overflow:auto;padding:16px}.szd-panel__section[data-v-6d80920c]{margin-bottom:20px}.szd-panel__section[data-v-6d80920c]:last-child{margin-bottom:0}.szd-panel__heading[data-v-6d80920c]{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin:0 0 10px}.szd-kv__row[data-v-6d80920c]{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid #f3f4f6}.szd-kv__row[data-v-6d80920c]:last-child{border-bottom:none}.szd-kv__key[data-v-6d80920c]{font-weight:500;color:#6b7280;min-width:100px}.szd-kv__value[data-v-6d80920c]{color:#1f2937}.szd-kv__value--mono[data-v-6d80920c]{font-family:ui-monospace,monospace;font-size:12px}.szd-kv__note[data-v-6d80920c]{color:#9ca3af;font-size:11px}.szd-stats[data-v-6d80920c]{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:12px}.szd-stat[data-v-6d80920c]{background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:12px;text-align:center}.szd-stat__value[data-v-6d80920c]{font-size:24px;font-weight:600;color:#1f2937}.szd-stat__label[data-v-6d80920c]{font-size:11px;color:#6b7280;margin-top:4px}.szd-event-preview[data-v-6d80920c]{display:flex;align-items:center;gap:10px;width:100%;padding:10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;text-align:left}.szd-event-preview[data-v-6d80920c]:hover{background:#f3f4f6}.szd-event-preview__badge[data-v-6d80920c]{padding:2px 8px;border-radius:4px;color:#fff;font-size:11px;font-weight:500}.szd-event-preview__text[data-v-6d80920c]{flex:1}.szd-event-preview__time[data-v-6d80920c]{color:#9ca3af;font-size:12px}.szd-timeline-filters[data-v-6d80920c]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;align-items:center}.szd-filter-chip[data-v-6d80920c]{display:inline-flex;align-items:center;padding:4px 10px;border:1px solid #d1d5db;border-radius:999px;font-size:12px;cursor:pointer;transition:all .15s;-webkit-user-select:none;user-select:none}.szd-filter-chip--active[data-v-6d80920c]{color:#fff}.szd-filter-chip__input[data-v-6d80920c]{display:none}.szd-timeline[data-v-6d80920c]{display:flex;flex-direction:column;gap:4px;max-height:350px;overflow:auto}.szd-timeline__item[data-v-6d80920c]{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:4px;cursor:pointer;transition:background .1s}.szd-timeline__item[data-v-6d80920c]:hover{background:#f3f4f6}.szd-timeline__time[data-v-6d80920c]{font-size:11px;color:#9ca3af;min-width:70px}.szd-timeline__badge[data-v-6d80920c]{padding:2px 8px;border-radius:4px;color:#fff;font-size:10px;font-weight:500;min-width:70px;text-align:center}.szd-timeline__text[data-v-6d80920c]{flex:1;font-size:12px}.szd-preview-header[data-v-6d80920c]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.szd-preview-header .szd-panel__heading[data-v-6d80920c]{margin:0}.szd-data-grid[data-v-6d80920c]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.szd-data-card[data-v-6d80920c]{border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.szd-data-card__header[data-v-6d80920c]{display:flex;align-items:center;justify-content:space-between;background:#f9fafb;padding:8px 10px;font-size:12px;font-weight:500;border-bottom:1px solid #e5e7eb}.szd-data-card__badge[data-v-6d80920c]{font-size:10px;padding:2px 6px;border-radius:4px;background:#d1fae5;color:#059669}.szd-data-card__badge--absent[data-v-6d80920c]{background:#fee2e2;color:#dc2626}.szd-data-card__content[data-v-6d80920c]{padding:8px 10px;font-size:11px;font-family:ui-monospace,monospace;background:#1f2937;color:#f3f4f6;margin:0;max-height:150px;overflow:auto}.szd-pipeline[data-v-6d80920c]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}.szd-pipeline__stage[data-v-6d80920c]{display:flex;flex-direction:column;align-items:center;padding:12px 16px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;transition:all .15s;min-width:90px}.szd-pipeline__stage[data-v-6d80920c]:hover{background:#eff6ff;border-color:#2563eb}.szd-pipeline__label[data-v-6d80920c]{font-size:11px;color:#6b7280}.szd-pipeline__count[data-v-6d80920c]{font-size:20px;font-weight:600;color:#1f2937}.szd-pipeline__arrow[data-v-6d80920c]{color:#9ca3af;font-size:18px}.szd-ast[data-v-6d80920c]{background:#1f2937;color:#f3f4f6;padding:12px;border-radius:6px;font-size:11px;font-family:ui-monospace,monospace;margin:0;overflow:auto;max-height:400px}.szd-detail__body .szd-ast[data-v-6d80920c]{max-height:none;flex:1}.szd-detail__panel--sm[data-v-6d80920c]{max-width:440px}.szd-settings-section[data-v-6d80920c]{margin-bottom:20px}.szd-settings-section[data-v-6d80920c]:last-child{margin-bottom:0}.szd-settings-label[data-v-6d80920c]{display:block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:8px}.szd-settings-row[data-v-6d80920c]{display:flex;align-items:center;gap:8px}.szd-settings-code[data-v-6d80920c]{flex:1;font-size:11px;font-family:ui-monospace,monospace;background:#f3f4f6;padding:8px 10px;border-radius:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.szd-settings-actions[data-v-6d80920c]{display:flex;gap:8px}.szd-select--full[data-v-6d80920c]{width:100%}.szd-detail[data-v-6d80920c]{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px}.szd-detail__backdrop[data-v-6d80920c]{position:absolute;inset:0;background:#00000080}.szd-detail__panel[data-v-6d80920c]{position:relative;width:100%;max-width:700px;max-height:80vh;background:#fff;border-radius:12px;box-shadow:0 20px 50px #00000040;display:flex;flex-direction:column;overflow:hidden}.szd-detail__header[data-v-6d80920c]{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e5e7eb;background:#f9fafb;border-radius:12px 12px 0 0}.szd-detail__title[data-v-6d80920c]{margin:0;font-size:16px;font-weight:600}.szd-detail__body[data-v-6d80920c]{flex:1;padding:20px;overflow:auto}.szd-slide-enter-active[data-v-6d80920c],.szd-slide-leave-active[data-v-6d80920c]{transition:opacity .2s}.szd-slide-enter-active .szd-detail__panel[data-v-6d80920c],.szd-slide-leave-active .szd-detail__panel[data-v-6d80920c]{transition:transform .2s,opacity .2s}.szd-slide-enter-from[data-v-6d80920c],.szd-slide-leave-to[data-v-6d80920c]{opacity:0}.szd-slide-enter-from .szd-detail__panel[data-v-6d80920c],.szd-slide-leave-to .szd-detail__panel[data-v-6d80920c]{transform:scale(.95);opacity:0}.szd-select[data-v-6d80920c]{padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;background:#fff;min-width:180px}.szd-btn[data-v-6d80920c]{padding:6px 12px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:13px;cursor:pointer;transition:all .1s}.szd-btn[data-v-6d80920c]:hover:not(:disabled){background:#f3f4f6}.szd-btn[data-v-6d80920c]:disabled{opacity:.5;cursor:not-allowed}.szd-btn--sm[data-v-6d80920c]{padding:4px 8px;font-size:11px}.szd-btn--icon[data-v-6d80920c]{padding:6px;display:flex;align-items:center;justify-content:center}.szd-toggle[data-v-6d80920c]{display:flex;align-items:center;gap:4px;font-size:12px;cursor:pointer}.szd-empty[data-v-6d80920c]{padding:40px;text-align:center;color:#9ca3af}
@@ -0,0 +1,8 @@
1
+ export function recordDebugEvent(entry: any): void;
2
+ export function subscribeDebugEvents(handler: any): () => any;
3
+ export function subscribeDebugClear(handler: any): () => any;
4
+ export function getDebugEntries(): any;
5
+ export function clearDebugEntries(): void;
6
+ export function setDebugEnabled(value: any): void;
7
+ export function setDebugMaxEntries(value: any): void;
8
+ export function startStateZeroDebug(): void;
@@ -0,0 +1,118 @@
1
+ import mitt from "mitt";
2
+ import { operationEvents, Status } from "../syncEngine/stores/operation.js";
3
+ const GLOBAL_KEY = "__STATEZERO_DEBUG__";
4
+ const globalState = globalThis[GLOBAL_KEY] || {
5
+ emitter: mitt(),
6
+ buffer: [],
7
+ enabled: true,
8
+ started: false,
9
+ maxEntries: 500,
10
+ nextId: 1,
11
+ };
12
+ globalThis[GLOBAL_KEY] = globalState;
13
+ const emitter = globalState.emitter;
14
+ const buffer = globalState.buffer;
15
+ function pushEntry(entry) {
16
+ if (!globalState.enabled)
17
+ return;
18
+ const item = {
19
+ id: globalState.nextId++,
20
+ ts: Date.now(),
21
+ ...entry,
22
+ };
23
+ buffer.push(item);
24
+ if (buffer.length > globalState.maxEntries) {
25
+ buffer.shift();
26
+ }
27
+ emitter.emit("record", item);
28
+ }
29
+ export function recordDebugEvent(entry) {
30
+ pushEntry(entry);
31
+ }
32
+ export function subscribeDebugEvents(handler) {
33
+ emitter.on("record", handler);
34
+ return () => emitter.off("record", handler);
35
+ }
36
+ export function subscribeDebugClear(handler) {
37
+ emitter.on("clear", handler);
38
+ return () => emitter.off("clear", handler);
39
+ }
40
+ export function getDebugEntries() {
41
+ return buffer.slice();
42
+ }
43
+ export function clearDebugEntries() {
44
+ buffer.length = 0;
45
+ emitter.emit("clear");
46
+ }
47
+ export function setDebugEnabled(value) {
48
+ globalState.enabled = Boolean(value);
49
+ }
50
+ export function setDebugMaxEntries(value) {
51
+ const next = Number(value);
52
+ if (!Number.isFinite(next) || next <= 0)
53
+ return;
54
+ globalState.maxEntries = Math.floor(next);
55
+ if (buffer.length > globalState.maxEntries) {
56
+ buffer.splice(0, buffer.length - globalState.maxEntries);
57
+ }
58
+ }
59
+ export function startStateZeroDebug() {
60
+ if (globalState.started)
61
+ return;
62
+ globalState.started = true;
63
+ operationEvents.on(Status.CREATED, (operation) => {
64
+ pushEntry({
65
+ type: "operation",
66
+ subtype: Status.CREATED,
67
+ operationId: operation.operationId,
68
+ operationType: operation.type,
69
+ status: operation.status,
70
+ semanticKey: operation.queryset?.semanticKey,
71
+ modelName: operation.queryset?.ModelClass?.modelName,
72
+ configKey: operation.queryset?.ModelClass?.configKey,
73
+ instanceCount: operation.instances?.length ?? 0,
74
+ });
75
+ });
76
+ operationEvents.on(Status.MUTATED, (operation) => {
77
+ pushEntry({
78
+ type: "operation",
79
+ subtype: Status.MUTATED,
80
+ operationId: operation.operationId,
81
+ operationType: operation.type,
82
+ status: operation.status,
83
+ semanticKey: operation.queryset?.semanticKey,
84
+ modelName: operation.queryset?.ModelClass?.modelName,
85
+ configKey: operation.queryset?.ModelClass?.configKey,
86
+ instanceCount: operation.instances?.length ?? 0,
87
+ });
88
+ });
89
+ operationEvents.on(Status.CONFIRMED, (operation) => {
90
+ pushEntry({
91
+ type: "operation",
92
+ subtype: Status.CONFIRMED,
93
+ operationId: operation.operationId,
94
+ operationType: operation.type,
95
+ status: operation.status,
96
+ semanticKey: operation.queryset?.semanticKey,
97
+ modelName: operation.queryset?.ModelClass?.modelName,
98
+ configKey: operation.queryset?.ModelClass?.configKey,
99
+ instanceCount: operation.instances?.length ?? 0,
100
+ });
101
+ });
102
+ operationEvents.on(Status.REJECTED, (operation) => {
103
+ pushEntry({
104
+ type: "operation",
105
+ subtype: Status.REJECTED,
106
+ operationId: operation.operationId,
107
+ operationType: operation.type,
108
+ status: operation.status,
109
+ semanticKey: operation.queryset?.semanticKey,
110
+ modelName: operation.queryset?.ModelClass?.modelName,
111
+ configKey: operation.queryset?.ModelClass?.configKey,
112
+ instanceCount: operation.instances?.length ?? 0,
113
+ });
114
+ });
115
+ operationEvents.on(Status.CLEAR, () => {
116
+ pushEntry({ type: "operation", subtype: Status.CLEAR });
117
+ });
118
+ }
@@ -154,7 +154,6 @@ export class ConfigError extends StateZeroError {
154
154
  * @returns {StateZeroError} An instance of a StateZeroError subclass.
155
155
  */
156
156
  export function parseStateZeroError(errorResponse) {
157
- console.log(JSON.stringify(errorResponse));
158
157
  const { status, type, detail } = errorResponse;
159
158
  // Handle undefined type/status case (like in permission denied)
160
159
  if (type === undefined && detail === 'Invalid token.') {
@@ -5,6 +5,7 @@ import { replaceTempPks } from './tempPk.js';
5
5
  import { parseStateZeroError, MultipleObjectsReturned, DoesNotExist } from './errors.js';
6
6
  import { FileObject } from './files.js';
7
7
  import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js';
8
+ import { recordDebugEvent } from '../../debug/statezeroDebug.js';
8
9
  // Namespace-based queues: separate queues for different operation types
9
10
  // This prevents sync operations from blocking user-initiated app operations
10
11
  const queues = new Map();
@@ -130,6 +131,7 @@ export async function makeApiCall(querySet, operationType, args = {}, operationI
130
131
  const baseUrl = backend.API_URL.replace(/\/+$/, "");
131
132
  const finalUrl = `${baseUrl}/${ModelClass.modelName}/`;
132
133
  const headers = backend.getAuthHeaders ? backend.getAuthHeaders() : {};
134
+ const semanticKey = querySet.semanticKey;
133
135
  if (operationId) {
134
136
  headers["X-Operation-ID"] = operationId;
135
137
  }
@@ -139,13 +141,82 @@ export async function makeApiCall(querySet, operationType, args = {}, operationI
139
141
  // Use the queue for write operations, bypass for read operations
140
142
  const apiCall = async () => {
141
143
  try {
144
+ recordDebugEvent({
145
+ type: "request",
146
+ modelName: ModelClass.modelName,
147
+ configKey: ModelClass.configKey,
148
+ semanticKey,
149
+ operationType,
150
+ operationId,
151
+ canonicalId,
152
+ url: finalUrl,
153
+ payload,
154
+ namespace,
155
+ });
142
156
  let response = await axios.post(finalUrl, replaceTempPks(payload), { headers });
143
157
  if (typeof beforeExit === 'function' && response?.data) {
144
158
  await beforeExit(response.data);
145
159
  }
160
+ const rawData = response?.data?.data;
161
+ const rawIncluded = response?.data?.included;
162
+ const dataCount = Array.isArray(rawData)
163
+ ? rawData.length
164
+ : rawData != null
165
+ ? 1
166
+ : 0;
167
+ const includedCount = rawIncluded ? Object.keys(rawIncluded).length : 0;
168
+ const maxItems = 100;
169
+ const data = Array.isArray(rawData) && rawData.length > maxItems
170
+ ? rawData.slice(0, maxItems)
171
+ : rawData;
172
+ const dataTruncated = Array.isArray(rawData) && rawData.length > maxItems;
173
+ let included = rawIncluded;
174
+ let includedTruncated = false;
175
+ if (rawIncluded && typeof rawIncluded === "object") {
176
+ const limited = {};
177
+ let count = 0;
178
+ for (const [key, value] of Object.entries(rawIncluded)) {
179
+ limited[key] = value;
180
+ count += 1;
181
+ if (count >= maxItems) {
182
+ includedTruncated = true;
183
+ break;
184
+ }
185
+ }
186
+ included = includedTruncated ? limited : rawIncluded;
187
+ }
188
+ recordDebugEvent({
189
+ type: "response",
190
+ modelName: ModelClass.modelName,
191
+ configKey: ModelClass.configKey,
192
+ semanticKey,
193
+ operationType,
194
+ operationId,
195
+ canonicalId,
196
+ url: finalUrl,
197
+ status: response.status,
198
+ dataCount,
199
+ includedCount,
200
+ data,
201
+ included,
202
+ dataTruncated,
203
+ includedTruncated,
204
+ });
146
205
  return response.data;
147
206
  }
148
207
  catch (error) {
208
+ recordDebugEvent({
209
+ type: "error",
210
+ modelName: ModelClass.modelName,
211
+ configKey: ModelClass.configKey,
212
+ semanticKey,
213
+ operationType,
214
+ operationId,
215
+ canonicalId,
216
+ url: finalUrl,
217
+ status: error?.response?.status,
218
+ message: error?.message,
219
+ });
149
220
  if (error?.code === "ECONNREFUSED") {
150
221
  const hint = "Connection refused. If you're running tests, start the test server with `python manage.py statezero_testserver`.";
151
222
  throw new Error(`${hint} (${finalUrl})`);
package/dist/reset.js CHANGED
@@ -31,7 +31,6 @@ export function unregisterAdapterReset(resetFn) {
31
31
  * Call this after login to ensure clean authenticated state.
32
32
  */
33
33
  export async function resetStateZero() {
34
- console.log("🔄 StateZero: Resetting...");
35
34
  try {
36
35
  // 1. Clear ALL operations first
37
36
  operationRegistry.clear();
@@ -88,7 +87,6 @@ export async function resetStateZero() {
88
87
  querysetStoreRegistry.setSyncManager(syncManager);
89
88
  modelStoreRegistry.setSyncManager(syncManager);
90
89
  metricRegistry.setSyncManager(syncManager);
91
- console.log("✅ StateZero reset complete - everything cleared");
92
90
  }
93
91
  catch (error) {
94
92
  console.error("❌ StateZero reset failed:", error);
@@ -38,7 +38,6 @@ export class IndexedDBStore {
38
38
  try {
39
39
  // Delete the database
40
40
  await this._deleteDatabase();
41
- console.log(`[IndexedDBStore] Successfully deleted database "${this.dbName}", attempting to reopen...`);
42
41
  // Try to open it again at the desired version
43
42
  return await this._openDatabase();
44
43
  }
@@ -66,7 +66,7 @@ export class QuerysetStoreRegistry {
66
66
  * @param {string} operationId - Unique ID for this sync operation (for coordination)
67
67
  * @param {Set} dbSyncedKeys - Set of semanticKeys that are dbSynced (followedQuerysets)
68
68
  */
69
- groupSync(queryset: Object, operationId: string, dbSyncedKeys: Set<any>): Promise<void>;
69
+ groupSync(queryset: Object, operationId: string, dbSyncedKeys: Set<any>, canonical_id?: null): Promise<void>;
70
70
  }
71
71
  export const querysetStoreRegistry: QuerysetStoreRegistry;
72
72
  import { QuerysetStoreGraph } from './querysetStoreGraph.js';
@@ -22,6 +22,7 @@ import { isNil, pick } from 'lodash-es';
22
22
  import hash from 'object-hash';
23
23
  import { Operation } from '../stores/operation.js';
24
24
  import { Cache } from '../cache/cache.js';
25
+ import { recordDebugEvent } from '../../debug/statezeroDebug.js';
25
26
  /**
26
27
  * A dynamic wrapper that always returns the latest queryset results
27
28
  * This class proxies array operations to always reflect the current state
@@ -287,7 +288,7 @@ export class QuerysetStoreRegistry {
287
288
  * @param {string} operationId - Unique ID for this sync operation (for coordination)
288
289
  * @param {Set} dbSyncedKeys - Set of semanticKeys that are dbSynced (followedQuerysets)
289
290
  */
290
- async groupSync(queryset, operationId, dbSyncedKeys) {
291
+ async groupSync(queryset, operationId, dbSyncedKeys, canonical_id = null) {
291
292
  if (isNil(queryset))
292
293
  return;
293
294
  const semanticKey = queryset.semanticKey;
@@ -304,6 +305,16 @@ export class QuerysetStoreRegistry {
304
305
  // Find the dbSynced root
305
306
  const { isRoot, root: rootKey } = this.querysetStoreGraph.findRoot(queryset, subset);
306
307
  const iAmRoot = isRoot || rootKey === semanticKey;
308
+ recordDebugEvent({
309
+ type: "groupSync",
310
+ phase: "start",
311
+ operationId,
312
+ semanticKey,
313
+ rootKey,
314
+ iAmRoot,
315
+ modelName: ModelClass?.modelName,
316
+ configKey: ModelClass?.configKey,
317
+ });
307
318
  // Get or create cache entry - whoever arrives first creates it
308
319
  if (!this._groupSyncCache.has(operationId)) {
309
320
  let resolve;
@@ -319,9 +330,21 @@ export class QuerysetStoreRegistry {
319
330
  }
320
331
  if (iAmRoot) {
321
332
  // I'm the root - sync from DB (store handles everything)
322
- await store.sync();
333
+ await store.sync(canonical_id);
323
334
  cached.pks = store.groundTruthPks;
324
335
  cached.resolve();
336
+ recordDebugEvent({
337
+ type: "groupSync",
338
+ phase: "rootSynced",
339
+ operationId,
340
+ semanticKey,
341
+ rootKey,
342
+ modelName: ModelClass?.modelName,
343
+ configKey: ModelClass?.configKey,
344
+ dataCount: Array.isArray(store.groundTruthPks)
345
+ ? store.groundTruthPks.length
346
+ : 0,
347
+ });
325
348
  }
326
349
  else {
327
350
  // Wait for root to finish with timeout to prevent deadlocks
@@ -349,7 +372,16 @@ export class QuerysetStoreRegistry {
349
372
  // Fallback to direct sync on timeout or if root data is missing
350
373
  if (timedOut || !cached.pks) {
351
374
  console.warn(`[groupSync] Falling back to direct sync for: ${semanticKey.substring(0, 60)}`);
352
- await store.sync();
375
+ await store.sync(canonical_id);
376
+ recordDebugEvent({
377
+ type: "groupSync",
378
+ phase: "fallbackSync",
379
+ operationId,
380
+ semanticKey,
381
+ rootKey,
382
+ modelName: ModelClass?.modelName,
383
+ configKey: ModelClass?.configKey,
384
+ });
353
385
  return;
354
386
  }
355
387
  // Filter from cached root data
@@ -360,6 +392,16 @@ export class QuerysetStoreRegistry {
360
392
  store.setGroundTruth(filteredPks);
361
393
  store.setOperations(store.getInflightOperations());
362
394
  store.lastSync = Date.now();
395
+ recordDebugEvent({
396
+ type: "groupSync",
397
+ phase: "filteredFromRoot",
398
+ operationId,
399
+ semanticKey,
400
+ rootKey,
401
+ modelName: ModelClass?.modelName,
402
+ configKey: ModelClass?.configKey,
403
+ dataCount: Array.isArray(filteredPks) ? filteredPks.length : 0,
404
+ });
363
405
  }
364
406
  }
365
407
  }
@@ -124,7 +124,6 @@ export class MetricStore {
124
124
  if (this.groundTruthValue === null) {
125
125
  const cached = this.metricCache.get(this.cacheKey);
126
126
  if (!isNil(cached) && !isEmpty(cached)) {
127
- console.log(`[MetricStore] Hydrated ${this.metricType} metric for ${this.modelClass.modelName} from cache`);
128
127
  this.setGroundTruth(cached?.value);
129
128
  }
130
129
  }
@@ -164,7 +163,6 @@ export class MetricStore {
164
163
  render() {
165
164
  // Check if ground truth value is null
166
165
  if (isNil(this.groundTruthValue)) {
167
- console.log(`groundTruthValue is null, returning null`);
168
166
  return null;
169
167
  }
170
168
  // Calculate the new value using the operations-based approach
@@ -193,7 +191,6 @@ export class MetricStore {
193
191
  }
194
192
  this.isSyncing = true;
195
193
  try {
196
- console.log(`[MetricStore] Syncing ${this.metricType} metric for ${this.modelClass.modelName}`);
197
194
  // Use fetchFn to get server metric value
198
195
  const result = await this.fetchFn({
199
196
  metricType: this.metricType,
@@ -208,7 +205,6 @@ export class MetricStore {
208
205
  }
209
206
  // Update ground truth
210
207
  this.setGroundTruth(result);
211
- console.log(`[MetricStore] Synced ${this.metricType} metric with value:`, result);
212
208
  return result;
213
209
  }
214
210
  catch (error) {
@@ -137,7 +137,6 @@ export class ModelStore {
137
137
  onHydrated() {
138
138
  if (this.groundTruthArray.length === 0 && this.operationsMap.size === 0) {
139
139
  let cached = this.modelCache.get(this.cacheKey);
140
- console.log(`[ModelStore] Hydrated ${this.modelClass.modelName} with ${(cached || []).length} items from cache`);
141
140
  if (!isNil(cached) && !isEmpty(cached)) {
142
141
  this.setGroundTruth(cached);
143
142
  }
@@ -322,7 +321,6 @@ export class ModelStore {
322
321
  queryset: this.modelClass.objects.all(),
323
322
  });
324
323
  this.operationsMap.set(checkpointOperation.operationId, checkpointOperation);
325
- console.log(`[ModelStore ${this.modelClass.modelName}] Created CHECKPOINT operation for ${checkpointInstances.length} existing instances`);
326
324
  }
327
325
  // reactivity - use all the newly added instances (both new and updated)
328
326
  emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
@@ -450,7 +448,6 @@ export class ModelStore {
450
448
  });
451
449
  const removedCount = this.groundTruthArray.length - filteredGroundTruth.length;
452
450
  if (removedCount > 0) {
453
- console.log(`[ModelStore ${modelName}] Pruned ${removedCount} unreferenced instances (${filteredGroundTruth.length} remaining)`);
454
451
  this.groundTruthArray = filteredGroundTruth;
455
452
  // Update the cache to reflect the pruned data
456
453
  this.setCache(filteredGroundTruth);
@@ -270,7 +270,6 @@ class OperationRegistry {
270
270
  * Clears all operations from the registry.
271
271
  */
272
272
  clear() {
273
- console.log("OperationRegistry: Clearing all operations.");
274
273
  this._operations.clear();
275
274
  this._querysetStates.clear();
276
275
  operationEvents.emit(Status.CLEAR);