@statezero/core 0.2.53 → 0.2.54
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/components/StateZeroDebugPanel.js +1025 -1159
- package/dist/adaptors/vue/composables.js +5 -0
- package/dist/core.css +1 -1
- package/dist/syncEngine/metrics/metricOptCalcs.d.ts +9 -78
- package/dist/syncEngine/metrics/metricOptCalcs.js +24 -277
- package/dist/syncEngine/registries/metricRegistry.d.ts +3 -0
- package/dist/syncEngine/registries/metricRegistry.js +10 -0
- package/dist/syncEngine/stores/metricStore.d.ts +18 -18
- package/dist/syncEngine/stores/metricStore.js +52 -91
- package/dist/syncEngine/stores/modelStore.js +28 -12
- package/dist/syncEngine/stores/operationEventHandlers.js +83 -349
- package/dist/syncEngine/stores/querysetStore.d.ts +10 -5
- package/dist/syncEngine/stores/querysetStore.js +134 -52
- package/dist/syncEngine/sync.js +2 -2
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Cache } from '../cache/cache.js';
|
|
2
|
-
import { MetricStrategyFactory } from "../metrics/metricOptCalcs.js";
|
|
3
2
|
import hash from 'object-hash';
|
|
4
3
|
import { isNil, isEmpty, isEqual } from 'lodash-es';
|
|
5
4
|
import { metricEventEmitter } from './reactivity.js';
|
|
6
|
-
import { Status } from './operation.js';
|
|
7
5
|
/**
|
|
8
|
-
* Store for managing a single metric with optimistic updates
|
|
6
|
+
* Store for managing a single metric with optimistic updates.
|
|
7
|
+
*
|
|
8
|
+
* Uses a delta-based approach: groundTruthValue + sum(deltas) = current value.
|
|
9
|
+
* Deltas are computed externally (by processMetricStores) and stored by operationId.
|
|
9
10
|
*/
|
|
10
11
|
export class MetricStore {
|
|
11
12
|
constructor(metricType, modelClass, queryset, field = null, ast = null, fetchFn) {
|
|
@@ -17,21 +18,16 @@ export class MetricStore {
|
|
|
17
18
|
this.fetchFn = fetchFn;
|
|
18
19
|
this.groundTruthValue = null;
|
|
19
20
|
this.isSyncing = false;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.operations = [];
|
|
23
|
-
// Keep track of which operations have been confirmed
|
|
24
|
-
this.confirmedOps = new Set();
|
|
21
|
+
// Delta storage: opId → { delta: number, confirmed: boolean }
|
|
22
|
+
this.deltas = new Map();
|
|
25
23
|
// Initialize cache with AST-specific key
|
|
26
24
|
this.metricCache = new Cache("metric-store-cache", {}, this.onHydrated.bind(this));
|
|
27
|
-
// Initialize _lastCalculatedValue
|
|
28
25
|
this._lastCalculatedValue = null;
|
|
29
26
|
}
|
|
30
27
|
reset() {
|
|
31
28
|
this.groundTruthValue = null;
|
|
32
29
|
this._lastCalculatedValue = null;
|
|
33
|
-
this.
|
|
34
|
-
this.confirmedOps = new Set();
|
|
30
|
+
this.deltas = new Map();
|
|
35
31
|
this.isSyncing = false;
|
|
36
32
|
this.clearCache();
|
|
37
33
|
}
|
|
@@ -39,86 +35,56 @@ export class MetricStore {
|
|
|
39
35
|
return `${this.modelClass.configKey}::${this.modelClass.modelName}::metric::${this.metricType}::${this.field || "null"}::${this.ast ? hash(this.ast) : "global"}`;
|
|
40
36
|
}
|
|
41
37
|
/**
|
|
42
|
-
* Add
|
|
43
|
-
* @param {Operation} operation - The operation to add
|
|
38
|
+
* Add a delta for an operation
|
|
44
39
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
// Check if operation already exists to avoid duplicates
|
|
51
|
-
const existingIndex = this.operations.findIndex((op) => op.operationId === operation.operationId);
|
|
52
|
-
if (existingIndex !== -1) {
|
|
53
|
-
// Update existing operation instead of adding a duplicate
|
|
54
|
-
this.operations[existingIndex] = operation;
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Add to our operations list
|
|
58
|
-
this.operations.push(operation);
|
|
59
|
-
}
|
|
60
|
-
// Trigger a render to update the metric value
|
|
61
|
-
if (!isNil(this.groundTruthValue)) {
|
|
62
|
-
this.render();
|
|
63
|
-
}
|
|
40
|
+
addDelta(opId, delta) {
|
|
41
|
+
this.deltas.set(opId, { delta, confirmed: false });
|
|
42
|
+
this.render();
|
|
64
43
|
}
|
|
65
44
|
/**
|
|
66
|
-
*
|
|
67
|
-
* @param {Operation} operation - The operation to update
|
|
45
|
+
* Mark a delta as confirmed
|
|
68
46
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
// Find and update the operation - use operationId not id
|
|
75
|
-
const index = this.operations.findIndex((op) => op.operationId === operation.operationId);
|
|
76
|
-
if (index !== -1) {
|
|
77
|
-
this.operations[index] = operation;
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
// If not found, add it
|
|
81
|
-
this.operations.push(operation);
|
|
82
|
-
}
|
|
83
|
-
// Trigger a render to update the metric value
|
|
84
|
-
if (!isNil(this.groundTruthValue)) {
|
|
47
|
+
confirmDelta(opId) {
|
|
48
|
+
const entry = this.deltas.get(opId);
|
|
49
|
+
if (entry) {
|
|
50
|
+
entry.confirmed = true;
|
|
85
51
|
this.render();
|
|
86
52
|
}
|
|
87
53
|
}
|
|
88
54
|
/**
|
|
89
|
-
*
|
|
90
|
-
* @param {Operation} operation - The operation to confirm
|
|
55
|
+
* Remove a rejected delta
|
|
91
56
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
// Update operation status in our list and mark as confirmed - use operationId not id
|
|
98
|
-
const index = this.operations.findIndex((op) => op.operationId === operation.operationId);
|
|
99
|
-
if (index !== -1) {
|
|
100
|
-
this.operations[index] = operation;
|
|
101
|
-
this.confirmedOps.add(operation.operationId);
|
|
57
|
+
rejectDelta(opId) {
|
|
58
|
+
if (this.deltas.delete(opId)) {
|
|
59
|
+
this.render();
|
|
102
60
|
}
|
|
103
|
-
// Trigger a sync to update ground truth
|
|
104
|
-
this.render();
|
|
105
61
|
}
|
|
106
62
|
/**
|
|
107
|
-
*
|
|
108
|
-
* @param {Operation} operation - The operation to reject
|
|
63
|
+
* Update a delta (e.g., when operation is mutated)
|
|
109
64
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
// Remove the operation as it's now rejected - use operationId not id
|
|
116
|
-
this.operations = this.operations.filter((op) => op.operationId !== operation.operationId);
|
|
117
|
-
this.confirmedOps.delete(operation.operationId);
|
|
118
|
-
// Trigger a render to update the metric value
|
|
119
|
-
if (!isNil(this.groundTruthValue)) {
|
|
65
|
+
updateDelta(opId, delta) {
|
|
66
|
+
const entry = this.deltas.get(opId);
|
|
67
|
+
if (entry) {
|
|
68
|
+
entry.delta = delta;
|
|
120
69
|
this.render();
|
|
121
70
|
}
|
|
71
|
+
else {
|
|
72
|
+
this.addDelta(opId, delta);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Legacy API — kept for backward compatibility with existing callers.
|
|
76
|
+
// processMetricStores in operationEventHandlers still calls these.
|
|
77
|
+
addOperation(operation) {
|
|
78
|
+
// No-op: deltas are computed externally
|
|
79
|
+
}
|
|
80
|
+
updateOperation(operation) {
|
|
81
|
+
// No-op: deltas are computed externally
|
|
82
|
+
}
|
|
83
|
+
confirm(operation) {
|
|
84
|
+
this.confirmDelta(operation.operationId);
|
|
85
|
+
}
|
|
86
|
+
reject(operation) {
|
|
87
|
+
this.rejectDelta(operation.operationId);
|
|
122
88
|
}
|
|
123
89
|
onHydrated() {
|
|
124
90
|
if (this.groundTruthValue === null) {
|
|
@@ -141,11 +107,9 @@ export class MetricStore {
|
|
|
141
107
|
this.metricCache.delete(this.cacheKey);
|
|
142
108
|
}
|
|
143
109
|
setGroundTruth(value) {
|
|
144
|
-
// Check if the value has actually changed
|
|
145
110
|
const valueChanged = !isEqual(this.groundTruthValue, value);
|
|
146
111
|
this.groundTruthValue = value;
|
|
147
112
|
this.setCache();
|
|
148
|
-
// Only emit event if the value changed
|
|
149
113
|
if (valueChanged) {
|
|
150
114
|
metricEventEmitter.emit("metric::render", {
|
|
151
115
|
metricType: this.metricType,
|
|
@@ -157,20 +121,18 @@ export class MetricStore {
|
|
|
157
121
|
}
|
|
158
122
|
}
|
|
159
123
|
/**
|
|
160
|
-
* Render
|
|
161
|
-
* @returns {any} Calculated metric value
|
|
124
|
+
* Render: groundTruthValue + sum of all deltas
|
|
162
125
|
*/
|
|
163
126
|
render() {
|
|
164
|
-
// Check if ground truth value is null
|
|
165
127
|
if (isNil(this.groundTruthValue)) {
|
|
166
128
|
return null;
|
|
167
129
|
}
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
130
|
+
let newValue = this.groundTruthValue;
|
|
131
|
+
for (const { delta } of this.deltas.values()) {
|
|
132
|
+
newValue += delta;
|
|
133
|
+
}
|
|
171
134
|
if (!isEqual(this._lastCalculatedValue, newValue)) {
|
|
172
135
|
this._lastCalculatedValue = newValue;
|
|
173
|
-
// Only emit event if the value changed
|
|
174
136
|
metricEventEmitter.emit("metric::render", {
|
|
175
137
|
metricType: this.metricType,
|
|
176
138
|
ModelClass: this.modelClass,
|
|
@@ -191,19 +153,18 @@ export class MetricStore {
|
|
|
191
153
|
}
|
|
192
154
|
this.isSyncing = true;
|
|
193
155
|
try {
|
|
194
|
-
// Use fetchFn to get server metric value
|
|
195
156
|
const result = await this.fetchFn({
|
|
196
157
|
metricType: this.metricType,
|
|
197
158
|
modelClass: this.modelClass,
|
|
198
159
|
field: this.field,
|
|
199
160
|
ast: this.ast,
|
|
200
161
|
});
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
162
|
+
// Remove confirmed deltas — server value already includes them
|
|
163
|
+
for (const [opId, entry] of this.deltas) {
|
|
164
|
+
if (entry.confirmed) {
|
|
165
|
+
this.deltas.delete(opId);
|
|
166
|
+
}
|
|
205
167
|
}
|
|
206
|
-
// Update ground truth
|
|
207
168
|
this.setGroundTruth(result);
|
|
208
169
|
return result;
|
|
209
170
|
}
|
|
@@ -308,19 +308,35 @@ export class ModelStore {
|
|
|
308
308
|
// Add completely new instances (these don't need checkpoint operations)
|
|
309
309
|
updatedGroundTruth.push(...Array.from(pkMap.values()));
|
|
310
310
|
this.groundTruthArray = updatedGroundTruth;
|
|
311
|
-
// Create CHECKPOINT operation for instances that already existed
|
|
311
|
+
// Create CHECKPOINT operation for instances that already existed,
|
|
312
|
+
// but skip instances that have inflight UPDATE operations — a stale re-fetch
|
|
313
|
+
// must not overwrite optimistic update data.
|
|
312
314
|
if (checkpointInstances.length > 0) {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
.
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
315
|
+
const inflightUpdatePks = new Set();
|
|
316
|
+
for (const op of this.operations) {
|
|
317
|
+
if ((op.type === Type.UPDATE || op.type === Type.UPDATE_INSTANCE) &&
|
|
318
|
+
op.status !== Status.CONFIRMED && op.status !== Status.REJECTED) {
|
|
319
|
+
for (const inst of op.instances) {
|
|
320
|
+
if (inst && inst[pkField] != null) {
|
|
321
|
+
inflightUpdatePks.add(inst[pkField]);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const safeCheckpointInstances = checkpointInstances.filter(inst => !inflightUpdatePks.has(inst[pkField]));
|
|
327
|
+
if (safeCheckpointInstances.length > 0) {
|
|
328
|
+
const checkpointOperation = new Operation({
|
|
329
|
+
operationId: `checkpoint_${Date.now()}_${Math.random()
|
|
330
|
+
.toString(36)
|
|
331
|
+
.substr(2, 9)}`,
|
|
332
|
+
type: Type.CHECKPOINT,
|
|
333
|
+
instances: safeCheckpointInstances,
|
|
334
|
+
status: Status.CONFIRMED,
|
|
335
|
+
timestamp: Date.now(),
|
|
336
|
+
queryset: this.modelClass.objects.all(),
|
|
337
|
+
});
|
|
338
|
+
this.operationsMap.set(checkpointOperation.operationId, checkpointOperation);
|
|
339
|
+
}
|
|
324
340
|
}
|
|
325
341
|
// reactivity - use all the newly added instances (both new and updated)
|
|
326
342
|
emitEvents(this, EventData.fromInstances([...checkpointInstances, ...Array.from(pkMap.values())], this.modelClass));
|