@statezero/core 0.2.3 → 0.2.4

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.
@@ -9,18 +9,6 @@ export const querysets = new Map(); // Map of composableId -> queryset
9
9
  function updateSyncManager() {
10
10
  // Get unique querysets from all active composables
11
11
  const uniqueQuerysets = new Set(querysets.values());
12
- // Unsubscribe from querysets no longer followed
13
- for (const [key, info] of syncManager.activeSubscriptions) {
14
- if (!uniqueQuerysets.has(info.queryset)) {
15
- syncManager.unsubscribeFromNamespace(info.queryset);
16
- }
17
- }
18
- // Subscribe to new querysets
19
- for (const queryset of uniqueQuerysets) {
20
- if (!syncManager.activeSubscriptions.has(queryset.semanticKey)) {
21
- syncManager.subscribeToNamespace(queryset);
22
- }
23
- }
24
12
  syncManager.followedQuerysets = uniqueQuerysets;
25
13
  }
26
14
  export function useQueryset(querysetFactory) {
@@ -83,21 +83,6 @@ export class PusherEventReceiver {
83
83
  clearTimeout(this.connectionTimeoutId);
84
84
  this.connectionTimeoutId = null;
85
85
  }
86
- // Notify connected event
87
- this.eventHandlers.forEach((handler) => {
88
- if (handler.onReconnected) {
89
- handler.onReconnected();
90
- }
91
- });
92
- });
93
- this.pusherClient.connection.bind("disconnected", () => {
94
- console.log(`Pusher client disconnected for backend: ${this.configKey}.`);
95
- // Notify disconnected event
96
- this.eventHandlers.forEach((handler) => {
97
- if (handler.onDisconnected) {
98
- handler.onDisconnected();
99
- }
100
- });
101
86
  });
102
87
  this.pusherClient.connection.bind("failed", () => {
103
88
  this._logConnectionError("Pusher connection explicitly failed.");
@@ -3,6 +3,7 @@ import { Model } from "./model.js";
3
3
  import { ModelSerializer, relationshipFieldSerializer, dateFieldSerializer } from "./serializers.js";
4
4
  import axios from "axios";
5
5
  import { QueryExecutor } from "./queryExecutor.js";
6
+ import { json } from "stream/consumers";
6
7
  import { v7 } from "uuid";
7
8
  import hash from "object-hash";
8
9
  import rfdc from "rfdc";
@@ -46,6 +46,6 @@ export class QuerysetStore {
46
46
  renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
47
47
  renderFromData(optimistic?: boolean): any[];
48
48
  applyOperation(operation: any, currentPks: any): any;
49
- sync(options?: {}): Promise<void>;
49
+ sync(forceFromDb?: boolean): Promise<void>;
50
50
  }
51
51
  import { Cache } from '../cache/cache.js';
@@ -269,8 +269,7 @@ export class QuerysetStore {
269
269
  }
270
270
  return currentPks;
271
271
  }
272
- async sync(options = {}) {
273
- const { canonical_id, forceFromDb = false } = typeof options === 'boolean' ? { forceFromDb: options } : options;
272
+ async sync(forceFromDb = false) {
274
273
  const id = this.modelClass.modelName;
275
274
  if (this.isSyncing) {
276
275
  console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
@@ -293,16 +292,10 @@ export class QuerysetStore {
293
292
  this.isSyncing = true;
294
293
  console.log(`[${id}] Starting sync...`);
295
294
  try {
296
- // Build request with canonical_id
297
- const requestBody = {
295
+ const response = await this.fetchFn({
298
296
  ast: this.queryset.build(),
299
297
  modelClass: this.modelClass,
300
- };
301
- // Include canonical_id if provided (for cache sharing)
302
- if (canonical_id) {
303
- requestBody.canonical_id = canonical_id;
304
- }
305
- const response = await this.fetchFn(requestBody);
298
+ });
306
299
  const { data, included } = response;
307
300
  if (isNil(data)) {
308
301
  return;
@@ -5,7 +5,6 @@ export class EventPayload {
5
5
  operation_id: any;
6
6
  pk_field_name: any;
7
7
  configKey: any;
8
- canonical_id: any;
9
8
  instances: any;
10
9
  _cachedInstances: any;
11
10
  get modelClass(): Function | null;
@@ -24,22 +23,16 @@ export class SyncManager {
24
23
  maxWaitMs: number;
25
24
  batchStartTime: number | null;
26
25
  syncQueue: PQueue<import("p-queue/dist/priority-queue").default, import("p-queue").QueueAddOptions>;
27
- activeSubscriptions: Map<any, any>;
28
- currentCanonicalId: any;
29
26
  withTimeout(promise: any, ms: any): Promise<any>;
30
27
  /**
31
28
  * Initialize event handlers for all event receivers
32
29
  */
33
30
  initialize(): void;
34
- handleDisconnect(): void;
35
- handleReconnect(): void;
36
31
  startPeriodicSync(): void;
37
32
  syncStaleQuerysets(): void;
38
33
  pruneUnreferencedModels(): void;
39
34
  isStoreFollowed(registry: any, semanticKey: any): boolean;
40
35
  cleanup(): void;
41
- subscribeToNamespace(queryset: any): Promise<void>;
42
- unsubscribeFromNamespace(queryset: any): Promise<void>;
43
36
  followModel(registry: any, modelClass: any): void;
44
37
  unfollowModel(registry: any, modelClass: any): void;
45
38
  manageRegistry(registry: any): void;
@@ -16,7 +16,6 @@ export class EventPayload {
16
16
  this.operation_id = data.operation_id;
17
17
  this.pk_field_name = data.pk_field_name;
18
18
  this.configKey = data.configKey;
19
- this.canonical_id = data.canonical_id;
20
19
  // Parse PK fields to numbers in instances
21
20
  this.instances = data.instances?.map(instance => {
22
21
  if (instance && this.pk_field_name && instance[this.pk_field_name] != null) {
@@ -51,10 +50,6 @@ export class SyncManager {
51
50
  this.handleEvent = (event) => {
52
51
  let payload = new EventPayload(event);
53
52
  let isLocalOperation = operationRegistry.has(payload.operation_id);
54
- // Store canonical_id for upcoming refetches
55
- if (event.canonical_id) {
56
- this.currentCanonicalId = event.canonical_id;
57
- }
58
53
  // Always process metrics immediately (they're lightweight)
59
54
  if (this.registries.has(MetricRegistry)) {
60
55
  this.processMetrics(payload);
@@ -102,9 +97,6 @@ export class SyncManager {
102
97
  this.batchStartTime = null;
103
98
  // SyncQueue
104
99
  this.syncQueue = new PQueue({ concurrency: 1 });
105
- // Namespace subscription tracking
106
- this.activeSubscriptions = new Map(); // semanticKey -> {queryset, subscribedAt}
107
- this.currentCanonicalId = null; // Store canonical_id from events
108
100
  }
109
101
  withTimeout(promise, ms) {
110
102
  // If no timeout specified, use 2x the periodic sync interval, or 30s as fallback
@@ -131,31 +123,15 @@ export class SyncManager {
131
123
  initializeAllEventReceivers();
132
124
  // Get all registered event receivers
133
125
  const eventReceivers = getAllEventReceivers();
134
- // Create event handler with connection lifecycle methods
135
- const eventHandlerWithLifecycle = this.handleEvent.bind(this);
136
- eventHandlerWithLifecycle.onReconnected = this.handleReconnect.bind(this);
137
- eventHandlerWithLifecycle.onDisconnected = this.handleDisconnect.bind(this);
138
126
  // Register the event handlers with each receiver
139
127
  eventReceivers.forEach((receiver, configKey) => {
140
128
  if (receiver) {
141
129
  // Model events go to handleEvent
142
- receiver.addModelEventHandler(eventHandlerWithLifecycle);
130
+ receiver.addModelEventHandler(this.handleEvent.bind(this));
143
131
  }
144
132
  });
145
133
  this.startPeriodicSync();
146
134
  }
147
- handleDisconnect() {
148
- // Clear local subscription state on disconnect
149
- this.activeSubscriptions.clear();
150
- console.log('[SyncManager] Cleared subscriptions on disconnect');
151
- }
152
- handleReconnect() {
153
- // Resubscribe to all active querysets after reconnect
154
- console.log('[SyncManager] Resubscribing to active querysets after reconnect');
155
- for (const queryset of this.followedQuerysets) {
156
- this.subscribeToNamespace(queryset);
157
- }
158
- }
159
135
  startPeriodicSync() {
160
136
  if (this.periodicSyncTimer)
161
137
  return;
@@ -231,97 +207,6 @@ export class SyncManager {
231
207
  this.maxWaitTimer = null;
232
208
  }
233
209
  }
234
- async subscribeToNamespace(queryset) {
235
- const key = queryset.semanticKey;
236
- // Skip if already subscribed
237
- if (this.activeSubscriptions.has(key))
238
- return;
239
- try {
240
- const config = getConfig();
241
- const backendConfig = config.backendConfigs[queryset.modelClass.configKey];
242
- if (!backendConfig) {
243
- console.error(`[SyncManager] Backend config not found for: ${queryset.modelClass.configKey}`);
244
- return;
245
- }
246
- const apiUrl = backendConfig.API_URL;
247
- // Get Pusher socket ID
248
- const eventReceiver = getEventReceiver(queryset.modelClass.configKey);
249
- const socketId = eventReceiver?.pusherClient?.connection?.socket_id;
250
- if (!socketId) {
251
- console.warn('[SyncManager] No socket_id available for subscription');
252
- return;
253
- }
254
- // Send subscription request to backend
255
- const response = await fetch(`${apiUrl}/namespaces/subscribe/`, {
256
- method: 'POST',
257
- headers: {
258
- 'Content-Type': 'application/json',
259
- },
260
- body: JSON.stringify({
261
- socket_id: socketId,
262
- model_name: queryset.modelClass.modelName,
263
- filter: queryset._filters // Backend extracts namespace from this
264
- })
265
- });
266
- const result = await response.json();
267
- if (!result.success) {
268
- throw new Error('Subscription failed');
269
- }
270
- // Subscribe to the namespace-specific Pusher channel
271
- const namespaceChannel = result.channel; // e.g., "django_app.Message:abc123hash"
272
- eventReceiver?.subscribe(namespaceChannel);
273
- this.activeSubscriptions.set(key, {
274
- queryset,
275
- subscribedAt: Date.now(),
276
- channel: namespaceChannel,
277
- namespace_hash: result.namespace_hash
278
- });
279
- console.log(`[SyncManager] Subscribed to namespace channel: ${namespaceChannel}`);
280
- }
281
- catch (error) {
282
- console.error('[SyncManager] Namespace subscription failed:', error);
283
- }
284
- }
285
- async unsubscribeFromNamespace(queryset) {
286
- const key = queryset.semanticKey;
287
- if (!this.activeSubscriptions.has(key))
288
- return;
289
- const subscription = this.activeSubscriptions.get(key);
290
- try {
291
- const config = getConfig();
292
- const backendConfig = config.backendConfigs[queryset.modelClass.configKey];
293
- if (!backendConfig) {
294
- console.error(`[SyncManager] Backend config not found for: ${queryset.modelClass.configKey}`);
295
- return;
296
- }
297
- const apiUrl = backendConfig.API_URL;
298
- const eventReceiver = getEventReceiver(queryset.modelClass.configKey);
299
- const socketId = eventReceiver?.pusherClient?.connection?.socket_id;
300
- if (!socketId)
301
- return;
302
- // Unsubscribe from the Pusher channel
303
- if (subscription.channel) {
304
- eventReceiver?.unsubscribe(subscription.channel);
305
- }
306
- // Notify backend to remove subscription
307
- await fetch(`${apiUrl}/namespaces/unsubscribe/`, {
308
- method: 'POST',
309
- headers: {
310
- 'Content-Type': 'application/json',
311
- },
312
- body: JSON.stringify({
313
- socket_id: socketId,
314
- model_name: queryset.modelClass.modelName,
315
- filter: queryset._filters
316
- })
317
- });
318
- this.activeSubscriptions.delete(key);
319
- console.log(`[SyncManager] Unsubscribed from namespace channel: ${subscription.channel}`);
320
- }
321
- catch (error) {
322
- console.error('[SyncManager] Namespace unsubscription failed:', error);
323
- }
324
- }
325
210
  followModel(registry, modelClass) {
326
211
  const models = this.followedModels.get(registry) || new Set();
327
212
  this.followedModels.set(registry, models);
@@ -423,9 +308,7 @@ export class SyncManager {
423
308
  // Sync all relevant stores for this model
424
309
  console.log(`[SyncManager] Syncing ${storesToSync.length} queryset stores for ${representativeEvent.model}`);
425
310
  storesToSync.forEach((store) => {
426
- this.syncQueue.add(() => this.withTimeout(store.sync({
427
- canonical_id: representativeEvent.canonical_id // Pass canonical_id
428
- })));
311
+ this.syncQueue.add(() => this.withTimeout(store.sync()));
429
312
  });
430
313
  }
431
314
  processMetrics(event) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
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",
@@ -32,7 +32,6 @@
32
32
  "test:e2e": "vitest run --config=vitest.sequential.config.ts tests/e2e",
33
33
  "generate:test-apps": "ts-node scripts/generate-test-apps.js",
34
34
  "test:adaptors": "playwright test tests/adaptors",
35
- "test:integration": "playwright test --config=playwright.integration.config.ts",
36
35
  "test:coverage": "vitest run --coverage",
37
36
  "build": "tsc",
38
37
  "parse-queries": "node scripts/perfect-query-parser.js",
@@ -1,16 +0,0 @@
1
- /**
2
- * Generate a stable hash for a namespace dictionary.
3
- * Must match the backend implementation in statezero/core/namespace_utils.py
4
- *
5
- * @param {Object} namespace - Namespace dictionary (e.g., {room_id: 5})
6
- * @returns {string} MD5 hash of the namespace
7
- */
8
- export function getNamespaceHash(namespace: Object): string;
9
- /**
10
- * Extract namespace fields from a queryset filter.
11
- * Namespace fields are those that use exact equality (not __gt, __lt, etc.)
12
- *
13
- * @param {Object} filters - Queryset filters
14
- * @returns {Object} Namespace dictionary
15
- */
16
- export function extractNamespaceFromFilter(filters: Object): Object;
@@ -1,32 +0,0 @@
1
- import crypto from 'crypto-js';
2
- /**
3
- * Generate a stable hash for a namespace dictionary.
4
- * Must match the backend implementation in statezero/core/namespace_utils.py
5
- *
6
- * @param {Object} namespace - Namespace dictionary (e.g., {room_id: 5})
7
- * @returns {string} MD5 hash of the namespace
8
- */
9
- export function getNamespaceHash(namespace) {
10
- // Sort keys to ensure stable hash
11
- const namespaceJson = JSON.stringify(namespace, Object.keys(namespace).sort());
12
- return crypto.MD5(namespaceJson).toString();
13
- }
14
- /**
15
- * Extract namespace fields from a queryset filter.
16
- * Namespace fields are those that use exact equality (not __gt, __lt, etc.)
17
- *
18
- * @param {Object} filters - Queryset filters
19
- * @returns {Object} Namespace dictionary
20
- */
21
- export function extractNamespaceFromFilter(filters) {
22
- if (!filters)
23
- return {};
24
- const namespace = {};
25
- for (const [key, value] of Object.entries(filters)) {
26
- // Only include exact matches (not lookups like __gt, __in, etc.)
27
- if (!key.includes('__')) {
28
- namespace[key] = value;
29
- }
30
- }
31
- return namespace;
32
- }