@statezero/core 0.2.43 → 0.2.45

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.
@@ -5,3 +5,4 @@
5
5
  * @returns {Object} Components registry for LayoutRenderer
6
6
  */
7
7
  export function createDefaultComponents(options?: Object): Object;
8
+ export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock };
@@ -4,13 +4,14 @@
4
4
  * These are minimal, unstyled implementations that follow the contracts.
5
5
  * Users can use these as-is or as reference for custom implementations.
6
6
  */
7
- export { default as AlertElement } from './AlertElement.js';
8
- export { default as LabelElement } from './LabelElement.js';
9
- export { default as DividerElement } from './DividerElement.js';
10
- export { default as DisplayElement } from './DisplayElement.js';
11
- export { default as GroupElement } from './GroupElement.js';
12
- export { default as TabsElement } from './TabsElement.js';
13
- export { default as ErrorBlock } from './ErrorBlock.js';
7
+ import AlertElement from './AlertElement.js';
8
+ import LabelElement from './LabelElement.js';
9
+ import DividerElement from './DividerElement.js';
10
+ import DisplayElement from './DisplayElement.js';
11
+ import GroupElement from './GroupElement.js';
12
+ import TabsElement from './TabsElement.js';
13
+ import ErrorBlock from './ErrorBlock.js';
14
+ export { AlertElement, LabelElement, DividerElement, DisplayElement, GroupElement, TabsElement, ErrorBlock };
14
15
  /**
15
16
  * Create a default components registry (without Control - user must provide)
16
17
  *
@@ -17,6 +17,7 @@ import { processQuery, getRequiredFields, pickRequiredFields } from '../../filte
17
17
  import { filter } from '../../filtering/localFiltering.js';
18
18
  import { makeApiCall } from '../../flavours/django/makeApiCall.js';
19
19
  import { QuerysetStoreGraph } from './querysetStoreGraph.js';
20
+ import { getConfig } from '../../config.js';
20
21
  import { isNil, pick } from 'lodash-es';
21
22
  import hash from 'object-hash';
22
23
  import { Operation } from '../stores/operation.js';
@@ -291,10 +292,14 @@ export class QuerysetStoreRegistry {
291
292
  return;
292
293
  const semanticKey = queryset.semanticKey;
293
294
  const ModelClass = queryset.ModelClass;
294
- // Convert dbSyncedKeys to semanticKeys if needed
295
+ // Convert dbSyncedKeys to semanticKeys, filtering to only keys that have stores
296
+ // A key without a store can never be a valid root (nobody would sync it)
295
297
  const subset = new Set();
296
298
  for (const item of dbSyncedKeys) {
297
- subset.add(typeof item === 'string' ? item : item?.semanticKey);
299
+ const key = typeof item === 'string' ? item : item?.semanticKey;
300
+ if (key && this._stores.has(key)) {
301
+ subset.add(key);
302
+ }
298
303
  }
299
304
  // Find the dbSynced root
300
305
  const { isRoot, root: rootKey } = this.querysetStoreGraph.findRoot(queryset, subset);
@@ -319,8 +324,34 @@ export class QuerysetStoreRegistry {
319
324
  cached.resolve();
320
325
  }
321
326
  else {
322
- // Wait for root to finish
323
- await cached.promise;
327
+ // Wait for root to finish with timeout to prevent deadlocks
328
+ // Use 2x periodic sync interval (same as SyncManager.withTimeout) or 30s fallback
329
+ let syncTimeoutMs = 30000;
330
+ try {
331
+ const config = getConfig();
332
+ if (config.periodicSyncIntervalSeconds) {
333
+ syncTimeoutMs = config.periodicSyncIntervalSeconds * 2000;
334
+ }
335
+ }
336
+ catch {
337
+ // Use default if no config
338
+ }
339
+ const timeoutPromise = new Promise((_, reject) => {
340
+ setTimeout(() => reject(new Error('timeout')), syncTimeoutMs);
341
+ });
342
+ let timedOut = false;
343
+ try {
344
+ await Promise.race([cached.promise, timeoutPromise]);
345
+ }
346
+ catch {
347
+ timedOut = true;
348
+ }
349
+ // Fallback to direct sync on timeout or if root data is missing
350
+ if (timedOut || !cached.pks) {
351
+ console.warn(`[groupSync] Falling back to direct sync for: ${semanticKey.substring(0, 60)}`);
352
+ await store.sync();
353
+ return;
354
+ }
324
355
  // Filter from cached root data
325
356
  const rootInstances = cached.pks.map(pk => ModelClass.fromPk(pk, queryset));
326
357
  const ast = queryset.build();
@@ -161,8 +161,10 @@ export class SyncManager {
161
161
  return;
162
162
  // Generate operationId for this sync batch - querysets in same chain will coordinate
163
163
  const operationId = `periodic-sync-${uuidv7()}`;
164
- // Get dbSynced keys (followed querysets)
165
- const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
164
+ // Get dbSynced keys (followed querysets that have stores)
165
+ const dbSyncedKeys = new Set([...this.followedQuerysets]
166
+ .map(qs => qs.semanticKey)
167
+ .filter(key => querysetRegistry._stores.has(key)));
166
168
  // Collect all stores to sync
167
169
  const storesToSync = [];
168
170
  for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
@@ -266,7 +268,9 @@ export class SyncManager {
266
268
  return;
267
269
  console.log(`[SyncManager] Syncing ${storesToSync.length} querysets needing verification for operation ${operationId}`);
268
270
  const syncOperationId = `maybe-sync-${uuidv7()}`;
269
- const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
271
+ const dbSyncedKeys = new Set([...this.followedQuerysets]
272
+ .map(qs => qs.semanticKey)
273
+ .filter(key => registry._stores.has(key)));
270
274
  Promise.all(storesToSync.map(store => registry.groupSync(store.queryset, syncOperationId, dbSyncedKeys)));
271
275
  }
272
276
  processBatch(reason = "unknown") {
@@ -345,8 +349,12 @@ export class SyncManager {
345
349
  console.log(`[SyncManager] Syncing ${storesToSync.length} queryset stores for ${representativeEvent.model}`);
346
350
  // Generate operationId for this batch - querysets in same chain will coordinate
347
351
  const operationId = `remote-event-${uuidv7()}`;
348
- // Get dbSynced keys (followed querysets)
349
- const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
352
+ // Get dbSynced keys (followed querysets that have stores)
353
+ // Filter to only include keys with stores - a queryset can be followed but not have
354
+ // a store if useQueryset doesn't call .fetch() (e.g., base queryset in a chain)
355
+ const dbSyncedKeys = new Set([...this.followedQuerysets]
356
+ .map(qs => qs.semanticKey)
357
+ .filter(key => registry._stores.has(key)));
350
358
  // Run all groupSync calls in parallel - they coordinate via shared promise cache
351
359
  Promise.all(storesToSync.map(store => registry.groupSync(store.queryset, operationId, dbSyncedKeys)));
352
360
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.2.43",
3
+ "version": "0.2.45",
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",