@statezero/core 0.2.41 → 0.2.43
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.
- package/dist/adaptors/vue/composables.js +3 -0
- package/dist/adaptors/vue/reactivity.js +56 -0
- package/dist/syncEngine/stores/operation.d.ts +21 -7
- package/dist/syncEngine/stores/operation.js +15 -9
- package/dist/syncEngine/stores/operationEventHandlers.js +52 -34
- package/dist/syncEngine/sync.d.ts +3 -2
- package/dist/syncEngine/sync.js +8 -6
- package/package.json +3 -1
|
@@ -32,6 +32,9 @@ export function useQueryset(querysetFactory) {
|
|
|
32
32
|
updateSyncManager();
|
|
33
33
|
lastQueryset = queryset;
|
|
34
34
|
}
|
|
35
|
+
// Access __version to establish Vue dependency tracking for watch()
|
|
36
|
+
// This makes the computed re-evaluate when queryset data changes
|
|
37
|
+
const _ = result?.__version;
|
|
35
38
|
return result;
|
|
36
39
|
});
|
|
37
40
|
}
|
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue Reactivity Adapters for StateZero
|
|
3
|
+
*
|
|
4
|
+
* This module bridges StateZero's event-based reactivity (mitt) with Vue's reactivity system.
|
|
5
|
+
*
|
|
6
|
+
* ## How It Works
|
|
7
|
+
*
|
|
8
|
+
* StateZero emits events via mitt when data changes. These adapters:
|
|
9
|
+
* 1. Wrap data in Vue's reactive() or ref()
|
|
10
|
+
* 2. Listen for mitt events
|
|
11
|
+
* 3. Update the reactive wrapper when events fire
|
|
12
|
+
*
|
|
13
|
+
* ## Queryset Reactivity
|
|
14
|
+
*
|
|
15
|
+
* Querysets are wrapped as **stable reactive arrays**. The same object reference is
|
|
16
|
+
* maintained across updates - data is mutated in place via splice/push. This is
|
|
17
|
+
* intentional:
|
|
18
|
+
*
|
|
19
|
+
* - Templates automatically re-render (Vue tracks array mutations)
|
|
20
|
+
* - Object identity stays stable (no stale references in UI code)
|
|
21
|
+
* - Cached wrappers are reused for the same queryset
|
|
22
|
+
*
|
|
23
|
+
* ### Watching Querysets
|
|
24
|
+
*
|
|
25
|
+
* Because the object reference is stable, Vue's shallow watch won't detect changes.
|
|
26
|
+
* Use one of these patterns:
|
|
27
|
+
*
|
|
28
|
+
* ```js
|
|
29
|
+
* const messages = useQueryset(() => baseQs.value.fetch())
|
|
30
|
+
*
|
|
31
|
+
* // Option 1: Deep watch (recommended)
|
|
32
|
+
* watch(messages, (newMessages) => {
|
|
33
|
+
* scrollToBottom()
|
|
34
|
+
* }, { deep: true })
|
|
35
|
+
*
|
|
36
|
+
* // Option 2: Watch a derived value
|
|
37
|
+
* watch(() => messages.value.length, (newLen) => {
|
|
38
|
+
* scrollToBottom()
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* ### The __version Mechanism
|
|
43
|
+
*
|
|
44
|
+
* Each queryset wrapper has a `__version` counter that increments on every update.
|
|
45
|
+
* The `useQueryset` composable accesses this to establish Vue dependency tracking,
|
|
46
|
+
* ensuring the computed re-evaluates when data changes. This makes `{ deep: true }`
|
|
47
|
+
* watches work correctly.
|
|
48
|
+
*
|
|
49
|
+
* ## Model Reactivity
|
|
50
|
+
*
|
|
51
|
+
* Models use a similar `__version` / `touch()` mechanism. When a model is updated,
|
|
52
|
+
* `touch()` increments the version, triggering Vue to re-render dependent components.
|
|
53
|
+
*/
|
|
1
54
|
import { reactive, ref, nextTick } from "vue";
|
|
2
55
|
import { modelEventEmitter, querysetEventEmitter, metricEventEmitter } from "../../syncEngine/stores/reactivity.js";
|
|
3
56
|
import { initEventHandler } from "../../syncEngine/stores/operationEventHandlers.js";
|
|
@@ -66,6 +119,7 @@ export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
|
|
|
66
119
|
// Make the queryset reactive using the specified function
|
|
67
120
|
const wrapper = reactivityFn([...liveQuerySet]);
|
|
68
121
|
wrapper.original = liveQuerySet;
|
|
122
|
+
wrapper.__version = 0;
|
|
69
123
|
const eventName = `${configKey}::${modelName}::queryset::render`;
|
|
70
124
|
// Handler bumps version to trigger Vue reactivity when this queryset updates
|
|
71
125
|
const renderHandler = (eventData) => {
|
|
@@ -77,6 +131,8 @@ export function QuerySetAdaptor(liveQuerySet, reactivityFn = reactive) {
|
|
|
77
131
|
wrapper.splice(0, wrapper.length);
|
|
78
132
|
wrapper.push(...liveQuerySet);
|
|
79
133
|
}
|
|
134
|
+
// Bump version so computed/watch can track changes
|
|
135
|
+
wrapper.__version++;
|
|
80
136
|
}
|
|
81
137
|
};
|
|
82
138
|
// Subscribe to queryset events indefinitely
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a membership state for a queryset with respect to an operation.
|
|
3
|
+
*
|
|
4
|
+
* @param {boolean} optimistic - Should we show this item immediately in the UI?
|
|
5
|
+
* @param {boolean} verify - Should remote sync check this queryset?
|
|
6
|
+
* @returns {{ optimistic: boolean, verify: boolean }}
|
|
7
|
+
*
|
|
8
|
+
* Usage patterns:
|
|
9
|
+
* - { optimistic: true, verify: false } - Certain it belongs, show immediately
|
|
10
|
+
* - { optimistic: false, verify: false } - Certain it doesn't belong, skip entirely
|
|
11
|
+
* - { optimistic: false, verify: true } - Uncertain, verify with server
|
|
12
|
+
*/
|
|
13
|
+
export function createMembershipState(optimistic: boolean, verify: boolean): {
|
|
14
|
+
optimistic: boolean;
|
|
15
|
+
verify: boolean;
|
|
16
|
+
};
|
|
1
17
|
export const operationEvents: any;
|
|
2
18
|
export namespace Status {
|
|
3
19
|
let CREATED: string;
|
|
@@ -24,11 +40,6 @@ export namespace Type {
|
|
|
24
40
|
let SUM: string;
|
|
25
41
|
let AGGREGATE: string;
|
|
26
42
|
}
|
|
27
|
-
export namespace OperationMembership {
|
|
28
|
-
let DEFINITELY_YES: string;
|
|
29
|
-
let DEFINITELY_NO: string;
|
|
30
|
-
let MAYBE: string;
|
|
31
|
-
}
|
|
32
43
|
export class Operation {
|
|
33
44
|
constructor(data: any, restore?: boolean);
|
|
34
45
|
operationId: any;
|
|
@@ -103,9 +114,12 @@ declare class OperationRegistry {
|
|
|
103
114
|
* Sets the membership state for a queryset with respect to an operation.
|
|
104
115
|
* @param {string} operationId - The operation ID
|
|
105
116
|
* @param {string} semanticKey - The queryset's semantic key
|
|
106
|
-
* @param {
|
|
117
|
+
* @param {{ optimistic: boolean, verify: boolean }} state - Membership state flags
|
|
107
118
|
*/
|
|
108
|
-
setQuerysetState(operationId: string, semanticKey: string, state:
|
|
119
|
+
setQuerysetState(operationId: string, semanticKey: string, state: {
|
|
120
|
+
optimistic: boolean;
|
|
121
|
+
verify: boolean;
|
|
122
|
+
}): void;
|
|
109
123
|
/**
|
|
110
124
|
* Gets the membership state for a queryset with respect to an operation.
|
|
111
125
|
* @param {string} operationId - The operation ID
|
|
@@ -43,14 +43,20 @@ export const Type = {
|
|
|
43
43
|
AGGREGATE: 'aggregate',
|
|
44
44
|
};
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* Creates a membership state for a queryset with respect to an operation.
|
|
47
|
+
*
|
|
48
|
+
* @param {boolean} optimistic - Should we show this item immediately in the UI?
|
|
49
|
+
* @param {boolean} verify - Should remote sync check this queryset?
|
|
50
|
+
* @returns {{ optimistic: boolean, verify: boolean }}
|
|
51
|
+
*
|
|
52
|
+
* Usage patterns:
|
|
53
|
+
* - { optimistic: true, verify: false } - Certain it belongs, show immediately
|
|
54
|
+
* - { optimistic: false, verify: false } - Certain it doesn't belong, skip entirely
|
|
55
|
+
* - { optimistic: false, verify: true } - Uncertain, verify with server
|
|
48
56
|
*/
|
|
49
|
-
export
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
MAYBE: 'maybe', // Impossible to know based on slicing, needs server sync
|
|
53
|
-
};
|
|
57
|
+
export function createMembershipState(optimistic, verify) {
|
|
58
|
+
return { optimistic, verify };
|
|
59
|
+
}
|
|
54
60
|
export class Operation {
|
|
55
61
|
constructor(data, restore = false) {
|
|
56
62
|
_Operation__instances.set(this, void 0);
|
|
@@ -191,7 +197,7 @@ _Operation__instances = new WeakMap(), _Operation__frozenInstances = new WeakMap
|
|
|
191
197
|
class OperationRegistry {
|
|
192
198
|
constructor() {
|
|
193
199
|
this._operations = new Map();
|
|
194
|
-
// Map<operationId, Map<semanticKey,
|
|
200
|
+
// Map<operationId, Map<semanticKey, { optimistic: boolean, verify: boolean }>>
|
|
195
201
|
this._querysetStates = new Map();
|
|
196
202
|
}
|
|
197
203
|
/**
|
|
@@ -234,7 +240,7 @@ class OperationRegistry {
|
|
|
234
240
|
* Sets the membership state for a queryset with respect to an operation.
|
|
235
241
|
* @param {string} operationId - The operation ID
|
|
236
242
|
* @param {string} semanticKey - The queryset's semantic key
|
|
237
|
-
* @param {
|
|
243
|
+
* @param {{ optimistic: boolean, verify: boolean }} state - Membership state flags
|
|
238
244
|
*/
|
|
239
245
|
setQuerysetState(operationId, semanticKey, state) {
|
|
240
246
|
if (!this._querysetStates.has(operationId)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { operationEvents, Status, Type, operationRegistry,
|
|
1
|
+
import { operationEvents, Status, Type, operationRegistry, createMembershipState } from './operation.js';
|
|
2
2
|
import { modelStoreRegistry } from '../registries/modelStoreRegistry.js';
|
|
3
3
|
import { querysetStoreRegistry } from '../registries/querysetStoreRegistry.js';
|
|
4
4
|
import { metricRegistry } from '../registries/metricRegistry.js';
|
|
@@ -7,14 +7,27 @@ 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
|
+
/**
|
|
11
|
+
* Check if two model classes represent the same model.
|
|
12
|
+
* Uses property comparison instead of reference equality to handle
|
|
13
|
+
* cases where different instances represent the same model.
|
|
14
|
+
*/
|
|
15
|
+
function isSameModel(modelClassA, modelClassB) {
|
|
16
|
+
return modelClassA.modelName === modelClassB.modelName &&
|
|
17
|
+
modelClassA.configKey === modelClassB.configKey;
|
|
18
|
+
}
|
|
10
19
|
/**
|
|
11
20
|
* Evaluates and routes a CREATE operation to querysets, tracking membership state.
|
|
12
21
|
*
|
|
22
|
+
* Membership state has two flags:
|
|
23
|
+
* - optimistic: Should we show this item immediately in the UI?
|
|
24
|
+
* - verify: Should remote sync check this queryset?
|
|
25
|
+
*
|
|
13
26
|
* For each queryset:
|
|
14
|
-
* - If offset > 0:
|
|
15
|
-
* - If
|
|
16
|
-
* - If item matches filter AND
|
|
17
|
-
* - If
|
|
27
|
+
* - If offset > 0: { optimistic: false, verify: true } - can't determine position
|
|
28
|
+
* - If local filter says no match: { optimistic: false, verify: true } - local filtering unreliable
|
|
29
|
+
* - If item matches filter AND has room: { optimistic: true, verify: false } - show it
|
|
30
|
+
* - If at limit and ordering excludes: { optimistic: false, verify: false } - certain it's out
|
|
18
31
|
*
|
|
19
32
|
* @param {Operation} operation - The CREATE operation to route
|
|
20
33
|
* @param {Function} applyAction - Function to apply the operation to a store
|
|
@@ -23,21 +36,23 @@ function routeCreateOperation(operation, applyAction) {
|
|
|
23
36
|
const modelClass = operation.queryset.ModelClass;
|
|
24
37
|
const instances = operation.instances;
|
|
25
38
|
Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
|
|
26
|
-
if (store.modelClass
|
|
39
|
+
if (!isSameModel(store.modelClass, modelClass))
|
|
27
40
|
return;
|
|
28
41
|
const serializerOptions = store.queryset?._serializerOptions || {};
|
|
29
42
|
const { offset, limit } = serializerOptions;
|
|
30
43
|
// Offset > 0: can't determine position based on slicing
|
|
31
44
|
if (offset != null && offset > 0) {
|
|
32
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
45
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
|
|
33
46
|
return;
|
|
34
47
|
}
|
|
35
48
|
// Evaluate if instances match this queryset's filter
|
|
49
|
+
// Use fromPk to get live representation with proper FK structure
|
|
50
|
+
const liveInstances = instances.map(inst => modelClass.fromPk(inst[modelClass.primaryKeyField])).filter(Boolean);
|
|
36
51
|
const ast = store.queryset.build();
|
|
37
|
-
const matchingInstances = filter(
|
|
52
|
+
const matchingInstances = filter(liveInstances, ast, modelClass, true);
|
|
38
53
|
if (matchingInstances.length === 0) {
|
|
39
|
-
//
|
|
40
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
54
|
+
// Local filter says no match - trust it (local filtering is robust)
|
|
55
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
|
|
41
56
|
return;
|
|
42
57
|
}
|
|
43
58
|
// Item matches filter - check limit
|
|
@@ -59,7 +74,7 @@ function routeCreateOperation(operation, applyAction) {
|
|
|
59
74
|
const currentItems = renderedItems.filter(Boolean);
|
|
60
75
|
// If some ground truth items couldn't be rendered, we can't trust local sort
|
|
61
76
|
if (currentItems.length < store.groundTruthPks.length) {
|
|
62
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
77
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
|
|
63
78
|
return;
|
|
64
79
|
}
|
|
65
80
|
const allItems = [...currentItems, ...matchingInstances];
|
|
@@ -72,15 +87,15 @@ function routeCreateOperation(operation, applyAction) {
|
|
|
72
87
|
const anyInTopN = newItemPks.some(pk => topNPks.has(pk));
|
|
73
88
|
if (anyInTopN) {
|
|
74
89
|
applyAction(store);
|
|
75
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
90
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
|
|
76
91
|
}
|
|
77
92
|
else {
|
|
78
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
93
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
else {
|
|
82
97
|
// No ordering - new items go at end, won't be in first N
|
|
83
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
98
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
|
|
84
99
|
}
|
|
85
100
|
return;
|
|
86
101
|
}
|
|
@@ -88,33 +103,35 @@ function routeCreateOperation(operation, applyAction) {
|
|
|
88
103
|
}
|
|
89
104
|
// Matches filter, has room or no limit - DEFINITELY_YES
|
|
90
105
|
applyAction(store);
|
|
91
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
106
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
|
|
92
107
|
});
|
|
93
108
|
}
|
|
94
109
|
/**
|
|
95
110
|
* Evaluates and tracks membership state for UPDATE operations.
|
|
96
111
|
*
|
|
97
|
-
* For UPDATE, we check filter FIRST
|
|
98
|
-
* - If
|
|
99
|
-
* - If
|
|
100
|
-
* - If
|
|
112
|
+
* For UPDATE, we check filter FIRST, then offset:
|
|
113
|
+
* - If local filter says no match: { optimistic: false, verify: true } - local filtering unreliable
|
|
114
|
+
* - If matched AND offset > 0: { optimistic: false, verify: true } - can't determine position
|
|
115
|
+
* - If matched AND offset = 0: { optimistic: true, verify: false } - show it
|
|
101
116
|
*
|
|
102
117
|
* @param {Operation} operation - The UPDATE operation
|
|
103
118
|
*/
|
|
104
119
|
function routeUpdateOperation(operation) {
|
|
105
120
|
const modelClass = operation.queryset.ModelClass;
|
|
121
|
+
// frozenInstances is already serialized with proper FK structure
|
|
106
122
|
const beforeInstances = operation.frozenInstances;
|
|
107
|
-
|
|
123
|
+
// For after state, use fromPk to get current live representation
|
|
124
|
+
const afterInstances = operation.instances.map(inst => modelClass.fromPk(inst[modelClass.primaryKeyField])).filter(Boolean);
|
|
108
125
|
Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
|
|
109
|
-
if (store.modelClass
|
|
126
|
+
if (!isSameModel(store.modelClass, modelClass))
|
|
110
127
|
return;
|
|
111
128
|
// Check filter match FIRST (optimization: skip offset check if no match)
|
|
112
129
|
const ast = store.queryset.build();
|
|
113
130
|
const matchedBefore = filter(beforeInstances, ast, modelClass, false).length > 0;
|
|
114
131
|
const matchesAfter = filter(afterInstances, ast, modelClass, false).length > 0;
|
|
115
|
-
// If
|
|
132
|
+
// If local filter says no match before AND after, trust it
|
|
116
133
|
if (!matchedBefore && !matchesAfter) {
|
|
117
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
134
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
|
|
118
135
|
return;
|
|
119
136
|
}
|
|
120
137
|
// Item matches filter - now check offset
|
|
@@ -122,35 +139,36 @@ function routeUpdateOperation(operation) {
|
|
|
122
139
|
const { offset } = serializerOptions;
|
|
123
140
|
if (offset != null && offset > 0) {
|
|
124
141
|
// Can't determine position in paginated window
|
|
125
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
142
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
|
|
126
143
|
return;
|
|
127
144
|
}
|
|
128
145
|
// Item matched before OR matches after, no offset - definitely affected
|
|
129
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
146
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
|
|
130
147
|
});
|
|
131
148
|
}
|
|
132
149
|
/**
|
|
133
150
|
* Evaluates and tracks membership state for DELETE operations.
|
|
134
151
|
*
|
|
135
|
-
* For DELETE, we check filter FIRST
|
|
136
|
-
* - If
|
|
137
|
-
* - If
|
|
138
|
-
* - If
|
|
152
|
+
* For DELETE, we check filter FIRST, then offset:
|
|
153
|
+
* - If local filter says no match: { optimistic: false, verify: true } - local filtering unreliable
|
|
154
|
+
* - If matched AND offset > 0: { optimistic: false, verify: true } - can't determine position
|
|
155
|
+
* - If matched AND offset = 0: { optimistic: true, verify: false } - show deletion
|
|
139
156
|
*
|
|
140
157
|
* @param {Operation} operation - The DELETE operation
|
|
141
158
|
*/
|
|
142
159
|
function routeDeleteOperation(operation) {
|
|
143
160
|
const modelClass = operation.queryset.ModelClass;
|
|
161
|
+
// frozenInstances is already serialized with proper FK structure
|
|
144
162
|
const beforeInstances = operation.frozenInstances;
|
|
145
163
|
Array.from(querysetStoreRegistry._stores.entries()).forEach(([semanticKey, store]) => {
|
|
146
|
-
if (store.modelClass
|
|
164
|
+
if (!isSameModel(store.modelClass, modelClass))
|
|
147
165
|
return;
|
|
148
166
|
// Check filter match FIRST (optimization: skip offset check if no match)
|
|
149
167
|
const ast = store.queryset.build();
|
|
150
168
|
const matchedBefore = filter(beforeInstances, ast, modelClass, false).length > 0;
|
|
151
|
-
// If
|
|
169
|
+
// If local filter says no match before, trust it
|
|
152
170
|
if (!matchedBefore) {
|
|
153
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
171
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, false));
|
|
154
172
|
return;
|
|
155
173
|
}
|
|
156
174
|
// Item matched filter - now check offset
|
|
@@ -158,11 +176,11 @@ function routeDeleteOperation(operation) {
|
|
|
158
176
|
const { offset } = serializerOptions;
|
|
159
177
|
if (offset != null && offset > 0) {
|
|
160
178
|
// Can't determine position in paginated window
|
|
161
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
179
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(false, true));
|
|
162
180
|
return;
|
|
163
181
|
}
|
|
164
182
|
// Item matched before, no offset - definitely affected
|
|
165
|
-
operationRegistry.setQuerysetState(operation.operationId, semanticKey,
|
|
183
|
+
operationRegistry.setQuerysetState(operation.operationId, semanticKey, createMembershipState(true, false));
|
|
166
184
|
});
|
|
167
185
|
}
|
|
168
186
|
/**
|
|
@@ -37,10 +37,11 @@ export class SyncManager {
|
|
|
37
37
|
manageRegistry(registry: any): void;
|
|
38
38
|
removeRegistry(registry: any): void;
|
|
39
39
|
/**
|
|
40
|
-
* Sync querysets that
|
|
40
|
+
* Sync querysets that need verification for a local operation.
|
|
41
41
|
* Called when a backend event arrives for an operation we initiated locally.
|
|
42
|
+
* Checks the `verify` flag on membership state to determine which querysets need syncing.
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
+
syncQuerysetsNeedingVerification(operationId: any): void;
|
|
44
45
|
handleEvent: (event: any) => void;
|
|
45
46
|
processBatch(reason?: string): void;
|
|
46
47
|
isQuerysetFollowed(queryset: any): boolean;
|
package/dist/syncEngine/sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getAllEventReceivers } from "../core/eventReceivers.js";
|
|
2
|
-
import { operationRegistry
|
|
2
|
+
import { operationRegistry } from "./stores/operation.js";
|
|
3
3
|
import { initializeAllEventReceivers } from "../config.js";
|
|
4
4
|
import { getEventReceiver } from "../core/eventReceivers.js";
|
|
5
5
|
import { querysetStoreRegistry, QuerysetStoreRegistry, } from "./registries/querysetStoreRegistry.js";
|
|
@@ -57,7 +57,7 @@ export class SyncManager {
|
|
|
57
57
|
if (isLocalOperation) {
|
|
58
58
|
// Check if any querysets were marked MAYBE for this operation
|
|
59
59
|
// These need to be synced since we couldn't determine membership client-side
|
|
60
|
-
this.
|
|
60
|
+
this.syncQuerysetsNeedingVerification(payload.operation_id);
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
// Add to batch for queryset/model processing
|
|
@@ -241,10 +241,11 @@ export class SyncManager {
|
|
|
241
241
|
this.registries.delete(registry.constructor);
|
|
242
242
|
}
|
|
243
243
|
/**
|
|
244
|
-
* Sync querysets that
|
|
244
|
+
* Sync querysets that need verification for a local operation.
|
|
245
245
|
* Called when a backend event arrives for an operation we initiated locally.
|
|
246
|
+
* Checks the `verify` flag on membership state to determine which querysets need syncing.
|
|
246
247
|
*/
|
|
247
|
-
|
|
248
|
+
syncQuerysetsNeedingVerification(operationId) {
|
|
248
249
|
const states = operationRegistry.getQuerysetStates(operationId);
|
|
249
250
|
if (!states)
|
|
250
251
|
return;
|
|
@@ -253,7 +254,8 @@ export class SyncManager {
|
|
|
253
254
|
return;
|
|
254
255
|
const storesToSync = [];
|
|
255
256
|
for (const [semanticKey, membership] of states.entries()) {
|
|
256
|
-
if
|
|
257
|
+
// Check the verify flag - if true, this queryset needs server verification
|
|
258
|
+
if (membership?.verify) {
|
|
257
259
|
const store = registry._stores.get(semanticKey);
|
|
258
260
|
if (store) {
|
|
259
261
|
storesToSync.push(store);
|
|
@@ -262,7 +264,7 @@ export class SyncManager {
|
|
|
262
264
|
}
|
|
263
265
|
if (storesToSync.length === 0)
|
|
264
266
|
return;
|
|
265
|
-
console.log(`[SyncManager] Syncing ${storesToSync.length}
|
|
267
|
+
console.log(`[SyncManager] Syncing ${storesToSync.length} querysets needing verification for operation ${operationId}`);
|
|
266
268
|
const syncOperationId = `maybe-sync-${uuidv7()}`;
|
|
267
269
|
const dbSyncedKeys = new Set([...this.followedQuerysets].map(qs => qs.semanticKey));
|
|
268
270
|
Promise.all(storesToSync.map(store => registry.groupSync(store.queryset, syncOperationId, dbSyncedKeys)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statezero/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.43",
|
|
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",
|
|
@@ -115,8 +115,10 @@
|
|
|
115
115
|
"@types/yargs": "^17.0.32",
|
|
116
116
|
"@vitejs/plugin-vue": "^6.0.4",
|
|
117
117
|
"@vitest/coverage-v8": "^3.0.5",
|
|
118
|
+
"@vue/test-utils": "^2.4.6",
|
|
118
119
|
"fake-indexeddb": "^6.0.0",
|
|
119
120
|
"fast-glob": "^3.3.3",
|
|
121
|
+
"happy-dom": "^20.5.0",
|
|
120
122
|
"react": "^18.2.0",
|
|
121
123
|
"rimraf": "^5.0.5",
|
|
122
124
|
"ts-node": "^10.9.2",
|