@statezero/core 0.1.62 → 0.1.64

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 StateZeroError extends Error {
23
23
  */
24
24
  constructor(message, code, detail, status) {
25
25
  super(message);
26
- this.name = this.constructor.name;
26
+ this.name = "StateZeroError";
27
27
  this.code = code;
28
28
  this.detail = detail;
29
29
  this.status = status;
@@ -66,6 +66,7 @@ export class ValidationError extends StateZeroError {
66
66
  */
67
67
  constructor(detail, status = 400) {
68
68
  super("Validation error", "validation_error", detail, status);
69
+ this.name = "ValidationError";
69
70
  }
70
71
  }
71
72
  /**
@@ -80,6 +81,7 @@ export class DoesNotExist extends StateZeroError {
80
81
  */
81
82
  constructor(detail = "Does not exist", status = 404) {
82
83
  super("DoesNotExist", "does_not_exist", detail, status);
84
+ this.name = "DoesNotExist";
83
85
  }
84
86
  }
85
87
  /**
@@ -94,6 +96,7 @@ export class PermissionDenied extends StateZeroError {
94
96
  */
95
97
  constructor(detail = "Permission denied", status = 403) {
96
98
  super("Permission denied", "permission_denied", detail, status);
99
+ this.name = "PermissionDenied";
97
100
  }
98
101
  }
99
102
  /**
@@ -108,6 +111,7 @@ export class MultipleObjectsReturned extends StateZeroError {
108
111
  */
109
112
  constructor(detail = "Multiple objects returned", status = 500) {
110
113
  super("Multiple objects returned", "multiple_objects_returned", detail, status);
114
+ this.name = "MultipleObjectsReturned";
111
115
  }
112
116
  }
113
117
  /**
@@ -122,6 +126,7 @@ export class ASTValidationError extends StateZeroError {
122
126
  */
123
127
  constructor(detail, status = 400) {
124
128
  super("Query syntax error", "ast_validation_error", detail, status);
129
+ this.name = "ASTValidationError";
125
130
  }
126
131
  }
127
132
  /**
@@ -136,6 +141,7 @@ export class ConfigError extends StateZeroError {
136
141
  */
137
142
  constructor(detail, status = 500) {
138
143
  super("Configuration error", "config_error", detail, status);
144
+ this.name = "ConfigError";
139
145
  }
140
146
  }
141
147
  /**
@@ -110,19 +110,6 @@ export async function makeApiCall(querySet, operationType, args = {}, operationI
110
110
  throw new Error(`API call failed: ${error.message}`);
111
111
  }
112
112
  };
113
- /*
114
- We use p-queue for write operations to ensure that the user does not
115
- try and perform a write operation using a temporary primary key that
116
- exists on the local optimistic instance until the backend has replied with the
117
- new primary key. This would result in a 400 Bad Request error from the backend.
118
- This is effective, but does mean that write operations are queued which can lead to delays.
119
- In general, this is not a problem because the UI will optimistically update the state so the
120
- user does not perceive the delay, but it is something to be aware of.
121
-
122
- In the future, we will want to implement an approach where we specifically detect
123
- if a write operation is using a temporary pk and then await for the temporary
124
- pk to be replaced before proceeding with the write operation. But for now,
125
- this is a simple and effective solution.
126
- */
113
+ // Queue write operations, execute read operations immediately
127
114
  return isWriteOperation ? apiCallQueue.add(apiCall) : apiCall();
128
115
  }
@@ -35,38 +35,6 @@ function relatedQuerysets(queryset) {
35
35
  });
36
36
  return result;
37
37
  }
38
- /**
39
- * Returns querysets that contain any of the specified instances
40
- * @param {Operation} operation - The operation containing instances to check
41
- * @returns {Map<QuerySet, Store>}
42
- */
43
- function querysetsContainingInstances(operation) {
44
- const ModelClass = operation.queryset.ModelClass;
45
- const pkField = ModelClass.primaryKeyField;
46
- const instancePks = new Set(operation.instances
47
- .filter(instance => instance && typeof instance === 'object' && pkField in instance)
48
- .map(instance => instance[pkField]));
49
- if (instancePks.size === 0) {
50
- return new Map();
51
- }
52
- const result = new Map();
53
- Array.from(querysetStoreRegistry._stores.entries()).forEach(([queryset, store]) => {
54
- if (store.modelClass !== ModelClass)
55
- return;
56
- try {
57
- // Check if this queryset contains any of the instances
58
- const renderedPks = new Set(store.render(false)); // Get without optimistic updates
59
- const hasIntersection = [...instancePks].some(pk => renderedPks.has(pk));
60
- if (hasIntersection) {
61
- result.set(store.queryset, store);
62
- }
63
- }
64
- catch (e) {
65
- console.warn('Error checking queryset for instances', e);
66
- }
67
- });
68
- return result;
69
- }
70
38
  /**
71
39
  * Process an operation in the model store
72
40
  *
@@ -156,14 +124,9 @@ function processQuerysetStores(operation, actionType) {
156
124
  function processMetricStores(operation, actionType) {
157
125
  const queryset = operation.queryset;
158
126
  const ModelClass = queryset.ModelClass;
159
- // For metrics, we use related querysets as the base
127
+ // For metrics, we can use a similar strategy but might be more conservative
128
+ // and always use related querysets since metrics are aggregations
160
129
  const allQuerysets = Array.from(relatedQuerysets(queryset).keys());
161
- // For update/delete operations, also include querysets that contain the instances
162
- if (operation.type === Type.UPDATE || operation.type === Type.UPDATE_INSTANCE ||
163
- operation.type === Type.DELETE || operation.type === Type.DELETE_INSTANCE) {
164
- const containingQuerysets = Array.from(querysetsContainingInstances(operation).keys());
165
- allQuerysets.push(...containingQuerysets);
166
- }
167
130
  let allMetricStores = new Set();
168
131
  allQuerysets.forEach(qs => {
169
132
  const stores = metricRegistry.getAllStoresForQueryset(qs);
@@ -2,11 +2,12 @@ import { getAllEventReceivers } from "../core/eventReceivers";
2
2
  import { operationRegistry } from "./stores/operation";
3
3
  import { initializeAllEventReceivers } from "../config";
4
4
  import { getEventReceiver } from "../core/eventReceivers";
5
- import { querysetStoreRegistry } from "./registries/querysetStoreRegistry";
6
- import { modelStoreRegistry } from "./registries/modelStoreRegistry";
7
- import { metricRegistry } from "./registries/metricRegistry";
5
+ import { querysetStoreRegistry, QuerysetStoreRegistry } from "./registries/querysetStoreRegistry";
6
+ import { modelStoreRegistry, ModelStoreRegistry } from "./registries/modelStoreRegistry";
7
+ import { metricRegistry, MetricRegistry } from "./registries/metricRegistry";
8
8
  import { getModelClass, getConfig } from "../config";
9
9
  import { isNil } from "lodash-es";
10
+ import { QuerysetStore } from "./stores/querysetStore";
10
11
  export class EventPayload {
11
12
  constructor(data) {
12
13
  this.event = data.event;
@@ -38,17 +39,17 @@ export class SyncManager {
38
39
  this.handleEvent = (event) => {
39
40
  let payload = new EventPayload(event);
40
41
  let isLocalOperation = operationRegistry.has(payload.operation_id);
41
- if (this.registries.has("MetricRegistry")) {
42
+ if (this.registries.has(MetricRegistry)) {
42
43
  this.processMetrics(payload);
43
44
  }
44
45
  if (isLocalOperation) {
45
46
  // This is a local operation, so don't resync querysets or models
46
47
  return;
47
48
  }
48
- if (this.registries.has("QuerysetStoreRegistry")) {
49
+ if (this.registries.has(QuerysetStoreRegistry)) {
49
50
  this.processQuerysets(payload);
50
51
  }
51
- if (this.registries.has("ModelStoreRegistry")) {
52
+ if (this.registries.has(ModelStoreRegistry)) {
52
53
  this.processModels(payload);
53
54
  }
54
55
  };
@@ -102,7 +103,7 @@ export class SyncManager {
102
103
  syncStaleQuerysets() {
103
104
  let syncedCount = 0;
104
105
  // Sync all followed querysets - keep it simple
105
- const querysetRegistry = this.registries.get("QuerysetStoreRegistry");
106
+ const querysetRegistry = this.registries.get(QuerysetStoreRegistry);
106
107
  if (querysetRegistry) {
107
108
  for (const [semanticKey, store] of querysetRegistry._stores.entries()) {
108
109
  // Only sync if this store is actually being followed
@@ -153,11 +154,11 @@ export class SyncManager {
153
154
  }
154
155
  }
155
156
  manageRegistry(registry) {
156
- this.registries.set(registry.constructor.name, registry);
157
+ this.registries.set(registry.constructor, registry);
157
158
  registry.setSyncManager(this);
158
159
  }
159
160
  removeRegistry(registry) {
160
- this.registries.delete(registry.constructor.name);
161
+ this.registries.delete(registry.constructor);
161
162
  }
162
163
  isQuerysetFollowed(queryset) {
163
164
  const activeSemanticKeys = new Set([...this.followedQuerysets].map((qs) => qs.semanticKey));
@@ -171,7 +172,7 @@ export class SyncManager {
171
172
  return false;
172
173
  }
173
174
  processQuerysets(event) {
174
- const registry = this.registries.get("QuerysetStoreRegistry");
175
+ const registry = this.registries.get(QuerysetStoreRegistry);
175
176
  for (const [semanticKey, store] of registry._stores.entries()) {
176
177
  if (store.modelClass.modelName === event.model &&
177
178
  store.modelClass.configKey === event.configKey) {
@@ -195,7 +196,7 @@ export class SyncManager {
195
196
  }
196
197
  }
197
198
  processMetrics(event) {
198
- const registry = this.registries.get("MetricRegistry");
199
+ const registry = this.registries.get(MetricRegistry);
199
200
  for (const [key, entry] of registry._stores.entries()) {
200
201
  // Check if the store has a queryset with the model class AND correct backend
201
202
  if (entry.queryset &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statezero/core",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
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",