@statezero/core 0.2.52 → 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 +943 -1069
- package/dist/adaptors/vue/composables.d.ts +1 -1
- package/dist/adaptors/vue/composables.js +13 -2
- package/dist/adaptors/vue/reactivity.js +3 -3
- package/dist/core.css +1 -1
- package/dist/syncEngine/cache/cache.d.ts +1 -1
- package/dist/syncEngine/cache/cache.js +8 -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 +17 -3
- package/dist/syncEngine/registries/metricRegistry.js +30 -6
- 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 +145 -51
- package/dist/syncEngine/sync.js +2 -2
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export function useQueryset(querysetFactory: any): import("vue").ComputedRef<any>;
|
|
1
|
+
export function useQueryset(querysetFactory: any): import("vue").Ref<any, any> | import("vue").ComputedRef<any>;
|
|
2
2
|
export const querysets: Map<any, any>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { computed, getCurrentInstance, onBeforeUnmount } from 'vue';
|
|
1
|
+
import { computed, isRef, getCurrentInstance, onBeforeUnmount } from 'vue';
|
|
2
2
|
import { querysetStoreRegistry } from '../../syncEngine/registries/querysetStoreRegistry.js';
|
|
3
|
-
import { metricRegistry } from '../../syncEngine/registries/metricRegistry.js';
|
|
3
|
+
import { metricRegistry, LiveMetric } from '../../syncEngine/registries/metricRegistry.js';
|
|
4
4
|
import { syncManager } from '../../syncEngine/sync.js';
|
|
5
5
|
import { registerAdapterReset } from '../../reset.js';
|
|
6
6
|
import { v7 } from 'uuid';
|
|
@@ -22,6 +22,17 @@ export function useQueryset(querysetFactory) {
|
|
|
22
22
|
querysets.delete(composableId);
|
|
23
23
|
updateSyncManager();
|
|
24
24
|
});
|
|
25
|
+
const result = querysetFactory();
|
|
26
|
+
// Metrics: the adaptor already returns a ref(scalar) — return it directly.
|
|
27
|
+
// Wrapping in another computed would cause .value.value in templates.
|
|
28
|
+
if (isRef(result) && result.original instanceof LiveMetric) {
|
|
29
|
+
const liveMetric = result.original;
|
|
30
|
+
metricRegistry.followMetric(liveMetric.metricType, liveMetric.queryset, liveMetric.field);
|
|
31
|
+
onBeforeUnmount(() => {
|
|
32
|
+
metricRegistry.unfollowMetric(liveMetric.metricType, liveMetric.queryset, liveMetric.field);
|
|
33
|
+
});
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
25
36
|
return computed(() => {
|
|
26
37
|
const result = querysetFactory();
|
|
27
38
|
const original = result?.original || result;
|
|
@@ -173,7 +173,7 @@ export function MetricAdaptor(metric) {
|
|
|
173
173
|
return wrappedMetricCache.get(cacheKey);
|
|
174
174
|
}
|
|
175
175
|
// Create a reactive reference with the initial value
|
|
176
|
-
const wrapper = ref(metric.
|
|
176
|
+
const wrapper = ref(metric.valueOf());
|
|
177
177
|
wrapper.original = metric;
|
|
178
178
|
// Single handler for metric render events
|
|
179
179
|
const metricRenderHandler = (eventData) => {
|
|
@@ -183,9 +183,9 @@ export function MetricAdaptor(metric) {
|
|
|
183
183
|
eventData.field === metric.field &&
|
|
184
184
|
eventData.ast === hash(querysetAst) &&
|
|
185
185
|
eventData.valueChanged === true) {
|
|
186
|
-
console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.
|
|
186
|
+
console.log(`[sz] metric update: ${modelName}.${metric.field} (${metric.metricType}) =`, metric.valueOf());
|
|
187
187
|
// Update the wrapper value with the latest metric value
|
|
188
|
-
wrapper.value = metric.
|
|
188
|
+
wrapper.value = metric.valueOf();
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
191
|
// Only listen for metric render events
|
package/dist/core.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.szd[data-v-
|
|
1
|
+
.szd[data-v-cf94aafd]{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:13px;color:#1f2937;background:#fff;border:1px solid #e5e7eb;border-radius:8px;display:flex;flex-direction:column;max-height:600px;overflow:hidden}.szd-header[data-v-cf94aafd]{border-bottom:1px solid #e5e7eb;background:#f9fafb}.szd-header__top[data-v-cf94aafd]{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;gap:12px}.szd-header__left[data-v-cf94aafd]{display:flex;align-items:center;gap:8px}.szd-header__right[data-v-cf94aafd]{display:flex;align-items:center;gap:6px}.szd-header__title[data-v-cf94aafd]{font-weight:600;font-size:14px}.szd-header__model[data-v-cf94aafd]{font-size:12px;color:#1f2937;background:#e5e7eb;padding:2px 8px;border-radius:4px}.szd-header__model--empty[data-v-cf94aafd]{color:#9ca3af;background:transparent}.szd-header__badge[data-v-cf94aafd]{font-size:11px;color:#6b7280;background:#f3f4f6;padding:2px 6px;border-radius:4px}.szd-header__badge--syncing[data-v-cf94aafd]{color:#059669;background:#d1fae5}.szd-tabs[data-v-cf94aafd]{display:flex;gap:0;padding:0 12px;border-top:1px solid #e5e7eb}.szd-tabs__tab[data-v-cf94aafd]{padding:10px 16px;background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;font-size:13px;font-weight:500;color:#6b7280;transition:all .15s}.szd-tabs__tab[data-v-cf94aafd]:hover{color:#1f2937}.szd-tabs__tab--active[data-v-cf94aafd]{color:#2563eb;border-bottom-color:#2563eb}.szd-content[data-v-cf94aafd]{flex:1;overflow:auto;padding:16px}.szd-panel__section[data-v-cf94aafd]{margin-bottom:20px}.szd-panel__section[data-v-cf94aafd]:last-child{margin-bottom:0}.szd-panel__heading[data-v-cf94aafd]{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin:0 0 10px}.szd-kv__row[data-v-cf94aafd]{display:flex;gap:8px;padding:4px 0;border-bottom:1px solid #f3f4f6}.szd-kv__row[data-v-cf94aafd]:last-child{border-bottom:none}.szd-kv__key[data-v-cf94aafd]{font-weight:500;color:#6b7280;min-width:100px}.szd-kv__value[data-v-cf94aafd]{color:#1f2937}.szd-kv__value--mono[data-v-cf94aafd]{font-family:ui-monospace,monospace;font-size:12px}.szd-kv__note[data-v-cf94aafd]{color:#9ca3af;font-size:11px}.szd-stats[data-v-cf94aafd]{display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:12px}.szd-stat[data-v-cf94aafd]{background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;padding:12px;text-align:center}.szd-stat__value[data-v-cf94aafd]{font-size:24px;font-weight:600;color:#1f2937}.szd-stat__label[data-v-cf94aafd]{font-size:11px;color:#6b7280;margin-top:4px}.szd-event-preview[data-v-cf94aafd]{display:flex;align-items:center;gap:10px;width:100%;padding:10px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:6px;cursor:pointer;text-align:left}.szd-event-preview[data-v-cf94aafd]:hover{background:#f3f4f6}.szd-event-preview__badge[data-v-cf94aafd]{padding:2px 8px;border-radius:4px;color:#fff;font-size:11px;font-weight:500}.szd-event-preview__text[data-v-cf94aafd]{flex:1}.szd-event-preview__time[data-v-cf94aafd]{color:#9ca3af;font-size:12px}.szd-timeline-filters[data-v-cf94aafd]{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;align-items:center}.szd-filter-chip[data-v-cf94aafd]{display:inline-flex;align-items:center;padding:4px 10px;border:1px solid #d1d5db;border-radius:999px;font-size:12px;cursor:pointer;transition:all .15s;-webkit-user-select:none;user-select:none}.szd-filter-chip--active[data-v-cf94aafd]{color:#fff}.szd-filter-chip__input[data-v-cf94aafd]{display:none}.szd-timeline[data-v-cf94aafd]{display:flex;flex-direction:column;gap:4px;max-height:350px;overflow:auto}.szd-timeline__item[data-v-cf94aafd]{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:4px;cursor:pointer;transition:background .1s}.szd-timeline__item[data-v-cf94aafd]:hover{background:#f3f4f6}.szd-timeline__time[data-v-cf94aafd]{font-size:11px;color:#9ca3af;min-width:70px}.szd-timeline__badge[data-v-cf94aafd]{padding:2px 8px;border-radius:4px;color:#fff;font-size:10px;font-weight:500;min-width:70px;text-align:center}.szd-timeline__text[data-v-cf94aafd]{flex:1;font-size:12px}.szd-preview-header[data-v-cf94aafd]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}.szd-preview-header .szd-panel__heading[data-v-cf94aafd]{margin:0}.szd-data-grid[data-v-cf94aafd]{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px}.szd-data-card[data-v-cf94aafd]{border:1px solid #e5e7eb;border-radius:6px;overflow:hidden}.szd-data-card__header[data-v-cf94aafd]{display:flex;align-items:center;justify-content:space-between;background:#f9fafb;padding:8px 10px;font-size:12px;font-weight:500;border-bottom:1px solid #e5e7eb}.szd-data-card__badge[data-v-cf94aafd]{font-size:10px;padding:2px 6px;border-radius:4px;background:#d1fae5;color:#059669}.szd-data-card__badge--absent[data-v-cf94aafd]{background:#fee2e2;color:#dc2626}.szd-data-card__content[data-v-cf94aafd]{padding:8px 10px;font-size:11px;font-family:ui-monospace,monospace;background:#1f2937;color:#f3f4f6;margin:0;max-height:150px;overflow:auto}.szd-pipeline[data-v-cf94aafd]{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}.szd-pipeline__stage[data-v-cf94aafd]{display:flex;flex-direction:column;align-items:center;padding:12px 16px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;cursor:pointer;transition:all .15s;min-width:90px}.szd-pipeline__stage[data-v-cf94aafd]:hover{background:#eff6ff;border-color:#2563eb}.szd-pipeline__label[data-v-cf94aafd]{font-size:11px;color:#6b7280}.szd-pipeline__count[data-v-cf94aafd]{font-size:20px;font-weight:600;color:#1f2937}.szd-pipeline__arrow[data-v-cf94aafd]{color:#9ca3af;font-size:18px}.szd-ast[data-v-cf94aafd]{background:#1f2937;color:#f3f4f6;padding:12px;border-radius:6px;font-size:11px;font-family:ui-monospace,monospace;margin:0;overflow:auto;max-height:400px}.szd-detail__body .szd-ast[data-v-cf94aafd]{max-height:none;flex:1}.szd-detail__panel--sm[data-v-cf94aafd]{max-width:440px}.szd-settings-section[data-v-cf94aafd]{margin-bottom:20px}.szd-settings-section[data-v-cf94aafd]:last-child{margin-bottom:0}.szd-settings-label[data-v-cf94aafd]{display:block;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:#6b7280;margin-bottom:8px}.szd-settings-row[data-v-cf94aafd]{display:flex;align-items:center;gap:8px}.szd-settings-code[data-v-cf94aafd]{flex:1;font-size:11px;font-family:ui-monospace,monospace;background:#f3f4f6;padding:8px 10px;border-radius:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.szd-settings-actions[data-v-cf94aafd]{display:flex;gap:8px}.szd-select--full[data-v-cf94aafd]{width:100%}.szd-detail[data-v-cf94aafd]{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:24px}.szd-detail__backdrop[data-v-cf94aafd]{position:absolute;inset:0;background:#00000080}.szd-detail__panel[data-v-cf94aafd]{position:relative;width:100%;max-width:700px;max-height:80vh;background:#fff;border-radius:12px;box-shadow:0 20px 50px #00000040;display:flex;flex-direction:column;overflow:hidden}.szd-detail__header[data-v-cf94aafd]{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e5e7eb;background:#f9fafb;border-radius:12px 12px 0 0}.szd-detail__title[data-v-cf94aafd]{margin:0;font-size:16px;font-weight:600}.szd-detail__body[data-v-cf94aafd]{flex:1;padding:20px;overflow:auto}.szd-slide-enter-active[data-v-cf94aafd],.szd-slide-leave-active[data-v-cf94aafd]{transition:opacity .2s}.szd-slide-enter-active .szd-detail__panel[data-v-cf94aafd],.szd-slide-leave-active .szd-detail__panel[data-v-cf94aafd]{transition:transform .2s,opacity .2s}.szd-slide-enter-from[data-v-cf94aafd],.szd-slide-leave-to[data-v-cf94aafd]{opacity:0}.szd-slide-enter-from .szd-detail__panel[data-v-cf94aafd],.szd-slide-leave-to .szd-detail__panel[data-v-cf94aafd]{transform:scale(.95);opacity:0}.szd-select[data-v-cf94aafd]{padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;font-size:13px;background:#fff;min-width:180px}.szd-btn[data-v-cf94aafd]{padding:6px 12px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:13px;cursor:pointer;transition:all .1s}.szd-btn[data-v-cf94aafd]:hover:not(:disabled){background:#f3f4f6}.szd-btn[data-v-cf94aafd]:disabled{opacity:.5;cursor:not-allowed}.szd-btn--sm[data-v-cf94aafd]{padding:4px 8px;font-size:11px}.szd-btn--icon[data-v-cf94aafd]{padding:6px;display:flex;align-items:center;justify-content:center}.szd-toggle[data-v-cf94aafd]{display:flex;align-items:center;gap:4px;font-size:12px;cursor:pointer}.szd-empty[data-v-cf94aafd]{padding:40px;text-align:center;color:#9ca3af}
|
|
@@ -288,7 +288,14 @@ export class IndexedDBStore {
|
|
|
288
288
|
}
|
|
289
289
|
export class Cache {
|
|
290
290
|
constructor(dbName, options = {}, onHydrated = null) {
|
|
291
|
-
|
|
291
|
+
// Share IndexedDBStore instances across Cache objects with the same dbName
|
|
292
|
+
if (dbConnections.has(dbName)) {
|
|
293
|
+
this.store = dbConnections.get(dbName);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
this.store = new IndexedDBStore(dbName, options);
|
|
297
|
+
dbConnections.set(dbName, this.store);
|
|
298
|
+
}
|
|
292
299
|
this.localMap = new Map();
|
|
293
300
|
// don't await - will hydrate during app setup
|
|
294
301
|
this.hydrate()
|
|
@@ -1,79 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* Override in subclasses if needed
|
|
12
|
-
*/
|
|
13
|
-
getInitialValue(): number;
|
|
14
|
-
/**
|
|
15
|
-
* Process a single operation - implement in subclasses
|
|
16
|
-
*/
|
|
17
|
-
reduceOperation(currentValue: any, operation: any, field: any): void;
|
|
18
|
-
/**
|
|
19
|
-
* Safely get a numeric value from an object
|
|
20
|
-
*/
|
|
21
|
-
safeGetValue(obj: any, field: any): number;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Count strategy implementation
|
|
25
|
-
*/
|
|
26
|
-
export class CountStrategy extends MetricCalculationStrategy {
|
|
27
|
-
reduceOperation(currentCount: any, operation: any, field: any): any;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Sum strategy implementation
|
|
31
|
-
*/
|
|
32
|
-
export class SumStrategy extends MetricCalculationStrategy {
|
|
33
|
-
reduceOperation(currentSum: any, operation: any, field: any): any;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Min strategy implementation
|
|
37
|
-
*/
|
|
38
|
-
export class MinStrategy extends MetricCalculationStrategy {
|
|
39
|
-
reduceOperation(currentMin: any, operation: any, field: any): any;
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Max strategy implementation
|
|
43
|
-
*/
|
|
44
|
-
export class MaxStrategy extends MetricCalculationStrategy {
|
|
45
|
-
reduceOperation(currentMax: any, operation: any, field: any): any;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Factory class for creating the appropriate strategy
|
|
49
|
-
*/
|
|
50
|
-
export class MetricStrategyFactory {
|
|
51
|
-
static "__#private@#customStrategies": Map<any, any>;
|
|
52
|
-
static "__#private@#defaultStrategies": Map<string, () => CountStrategy>;
|
|
53
|
-
/**
|
|
54
|
-
* Clear all custom strategy overrides
|
|
55
|
-
*/
|
|
56
|
-
static clearCustomStrategies(): void;
|
|
57
|
-
/**
|
|
58
|
-
* Generate a unique key for the strategy map
|
|
59
|
-
* @private
|
|
60
|
-
* @param {string} metricType - The type of metric (count, sum, min, max)
|
|
61
|
-
* @param {Function} ModelClass - The model class
|
|
62
|
-
* @returns {string} A unique key
|
|
63
|
-
*/
|
|
64
|
-
private static "__#private@#generateStrategyKey";
|
|
65
|
-
/**
|
|
66
|
-
* Override a strategy for a specific metric type and model class
|
|
67
|
-
* @param {string} metricType - The type of metric (count, sum, min, max)
|
|
68
|
-
* @param {Function|null} ModelClass - The model class or null for a generic override
|
|
69
|
-
* @param {MetricCalculationStrategy} strategy - The strategy to use
|
|
70
|
-
*/
|
|
71
|
-
static overrideStrategy(metricType: string, ModelClass: Function | null, strategy: MetricCalculationStrategy): void;
|
|
72
|
-
/**
|
|
73
|
-
* Get the appropriate strategy for a model class and metric type
|
|
74
|
-
* @param {string} metricType - The type of metric (count, sum, min, max)
|
|
75
|
-
* @param {Function} ModelClass - The model class
|
|
76
|
-
* @returns {MetricCalculationStrategy} The appropriate strategy
|
|
77
|
-
*/
|
|
78
|
-
static getStrategy(metricType: string, ModelClass: Function): MetricCalculationStrategy;
|
|
79
|
-
}
|
|
2
|
+
* Compute a metric value from a queryset store's current rendered PKs.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} metricType - 'count', 'sum', 'min', 'max'
|
|
5
|
+
* @param {QuerysetStore} querysetStore - The queryset store to compute from
|
|
6
|
+
* @param {string|null} field - The field to aggregate (null for count)
|
|
7
|
+
* @param {Function} ModelClass - The model class for instance lookups
|
|
8
|
+
* @returns {number} The computed metric value
|
|
9
|
+
*/
|
|
10
|
+
export function computeMetricFromQueryset(metricType: string, querysetStore: QuerysetStore, field: string | null, ModelClass: Function): number;
|
|
@@ -1,284 +1,31 @@
|
|
|
1
|
-
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
2
|
-
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
3
|
-
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
4
|
-
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
5
|
-
};
|
|
6
|
-
var _a, _MetricStrategyFactory_customStrategies, _MetricStrategyFactory_defaultStrategies, _MetricStrategyFactory_generateStrategyKey;
|
|
7
|
-
import { Status, Type } from '../stores/operation.js';
|
|
8
1
|
import { isNil } from 'lodash-es';
|
|
9
2
|
/**
|
|
10
|
-
*
|
|
3
|
+
* Compute a metric value from a queryset store's current rendered PKs.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} metricType - 'count', 'sum', 'min', 'max'
|
|
6
|
+
* @param {QuerysetStore} querysetStore - The queryset store to compute from
|
|
7
|
+
* @param {string|null} field - The field to aggregate (null for count)
|
|
8
|
+
* @param {Function} ModelClass - The model class for instance lookups
|
|
9
|
+
* @returns {number} The computed metric value
|
|
11
10
|
*/
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Process operations sequentially
|
|
20
|
-
for (const op of operations) {
|
|
21
|
-
// Skip rejected operations
|
|
22
|
-
if (op.status === Status.REJECTED)
|
|
23
|
-
continue;
|
|
24
|
-
// Process each instance in the operation
|
|
25
|
-
op.frozenInstances.forEach((originalData, index) => {
|
|
26
|
-
let pk = originalData[ModelClass.primaryKeyField];
|
|
27
|
-
// Get the updated data for this instance (for UPDATE operations)
|
|
28
|
-
const updatedData = op.instances[index] || null;
|
|
29
|
-
// Create operation data object for the reducer
|
|
30
|
-
const singleOp = {
|
|
31
|
-
originalData, // Pre-operation state
|
|
32
|
-
updatedData, // Post-operation state (for updates)
|
|
33
|
-
type: op.type,
|
|
34
|
-
status: op.status
|
|
35
|
-
};
|
|
36
|
-
// Apply this single operation using the strategy-specific reducer
|
|
37
|
-
currentValue = this.reduceOperation(currentValue, singleOp, field);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return currentValue;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get initial value for the metric type
|
|
44
|
-
* Override in subclasses if needed
|
|
45
|
-
*/
|
|
46
|
-
getInitialValue() {
|
|
47
|
-
return 0; // Default for count and sum
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Process a single operation - implement in subclasses
|
|
51
|
-
*/
|
|
52
|
-
reduceOperation(currentValue, operation, field) {
|
|
53
|
-
throw new Error('reduceOperation must be implemented by subclass');
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Safely get a numeric value from an object
|
|
57
|
-
*/
|
|
58
|
-
safeGetValue(obj, field) {
|
|
59
|
-
if (!obj || !field)
|
|
11
|
+
export function computeMetricFromQueryset(metricType, querysetStore, field, ModelClass) {
|
|
12
|
+
const pks = querysetStore.render();
|
|
13
|
+
if (metricType === 'count')
|
|
14
|
+
return pks.length;
|
|
15
|
+
const values = pks.map(pk => {
|
|
16
|
+
const inst = ModelClass.fromPk(pk);
|
|
17
|
+
if (!inst || !field)
|
|
60
18
|
return 0;
|
|
61
|
-
const
|
|
62
|
-
if (isNil(
|
|
19
|
+
const val = inst[field];
|
|
20
|
+
if (isNil(val))
|
|
63
21
|
return 0;
|
|
64
|
-
const
|
|
65
|
-
return isNaN(
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
reduceOperation(currentCount, operation, field) {
|
|
73
|
-
// Skip rejected operations
|
|
74
|
-
if (operation.status === Status.REJECTED) {
|
|
75
|
-
return currentCount;
|
|
76
|
-
}
|
|
77
|
-
const { type } = operation;
|
|
78
|
-
// Handle operation types
|
|
79
|
-
if (type === Type.CREATE) {
|
|
80
|
-
return currentCount + 1;
|
|
81
|
-
}
|
|
82
|
-
else if ([Type.DELETE, Type.DELETE_INSTANCE].includes(type)) {
|
|
83
|
-
return Math.max(0, currentCount - 1);
|
|
84
|
-
}
|
|
85
|
-
// Other operation types don't affect the count
|
|
86
|
-
return currentCount;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Sum strategy implementation
|
|
91
|
-
*/
|
|
92
|
-
export class SumStrategy extends MetricCalculationStrategy {
|
|
93
|
-
reduceOperation(currentSum, operation, field) {
|
|
94
|
-
// Skip rejected operations
|
|
95
|
-
if (operation.status === Status.REJECTED) {
|
|
96
|
-
return currentSum;
|
|
97
|
-
}
|
|
98
|
-
if (!field) {
|
|
99
|
-
throw new Error('SumStrategy requires a field parameter');
|
|
100
|
-
}
|
|
101
|
-
const { type, originalData, updatedData } = operation;
|
|
102
|
-
switch (type) {
|
|
103
|
-
case Type.CREATE:
|
|
104
|
-
// For CREATE, add the value to the sum
|
|
105
|
-
return currentSum + this.safeGetValue(originalData, field);
|
|
106
|
-
case Type.CHECKPOINT:
|
|
107
|
-
case Type.UPDATE:
|
|
108
|
-
// For UPDATE, subtract old value and add new value
|
|
109
|
-
if (updatedData) {
|
|
110
|
-
const oldValue = this.safeGetValue(originalData, field);
|
|
111
|
-
const newValue = this.safeGetValue(updatedData, field);
|
|
112
|
-
return currentSum - oldValue + newValue;
|
|
113
|
-
}
|
|
114
|
-
return currentSum;
|
|
115
|
-
case Type.DELETE:
|
|
116
|
-
case Type.DELETE_INSTANCE:
|
|
117
|
-
// For DELETE, subtract the value from the sum
|
|
118
|
-
return currentSum - this.safeGetValue(originalData, field);
|
|
119
|
-
default:
|
|
120
|
-
return currentSum;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Min strategy implementation
|
|
126
|
-
*/
|
|
127
|
-
export class MinStrategy extends MetricCalculationStrategy {
|
|
128
|
-
getInitialValue() {
|
|
129
|
-
return Infinity;
|
|
130
|
-
}
|
|
131
|
-
reduceOperation(currentMin, operation, field) {
|
|
132
|
-
// Skip rejected operations
|
|
133
|
-
if (operation.status === Status.REJECTED) {
|
|
134
|
-
return currentMin;
|
|
135
|
-
}
|
|
136
|
-
if (!field) {
|
|
137
|
-
throw new Error('MinStrategy requires a field parameter');
|
|
138
|
-
}
|
|
139
|
-
const { type, originalData, updatedData } = operation;
|
|
140
|
-
if (type === Type.CREATE) {
|
|
141
|
-
// For CREATE, check if the new value is smaller than current min
|
|
142
|
-
const value = this.safeGetValue(originalData, field);
|
|
143
|
-
return Math.min(currentMin, value);
|
|
144
|
-
}
|
|
145
|
-
else if ((type === Type.UPDATE || type.CHECKPOINT) && updatedData) {
|
|
146
|
-
// For UPDATE, first check if we're updating the minimum value
|
|
147
|
-
const oldValue = this.safeGetValue(originalData, field);
|
|
148
|
-
const newValue = this.safeGetValue(updatedData, field);
|
|
149
|
-
if (oldValue === currentMin) {
|
|
150
|
-
// We're updating the current minimum, need to find the new minimum
|
|
151
|
-
if (newValue <= oldValue) {
|
|
152
|
-
// Simple case: new value is still the minimum
|
|
153
|
-
return newValue;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
// Harder case: need to recalculate minimum
|
|
157
|
-
// For now, conservatively use the new value
|
|
158
|
-
return newValue;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else if (newValue < currentMin) {
|
|
162
|
-
// The updated value is a new minimum
|
|
163
|
-
return newValue;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
// For other cases, maintain current min
|
|
167
|
-
return currentMin;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Max strategy implementation
|
|
172
|
-
*/
|
|
173
|
-
export class MaxStrategy extends MetricCalculationStrategy {
|
|
174
|
-
getInitialValue() {
|
|
175
|
-
return -Infinity;
|
|
176
|
-
}
|
|
177
|
-
reduceOperation(currentMax, operation, field) {
|
|
178
|
-
// Skip rejected operations
|
|
179
|
-
if (operation.status === Status.REJECTED) {
|
|
180
|
-
return currentMax;
|
|
181
|
-
}
|
|
182
|
-
if (!field) {
|
|
183
|
-
throw new Error('MaxStrategy requires a field parameter');
|
|
184
|
-
}
|
|
185
|
-
const { type, originalData, updatedData } = operation;
|
|
186
|
-
if (type === Type.CREATE) {
|
|
187
|
-
// For CREATE, check if the new value is larger than current max
|
|
188
|
-
const value = this.safeGetValue(originalData, field);
|
|
189
|
-
return Math.max(currentMax, value);
|
|
190
|
-
}
|
|
191
|
-
else if ((type === Type.UPDATE || type === Type.CHECKPOINT) && updatedData) {
|
|
192
|
-
// For UPDATE, first check if we're updating the maximum value
|
|
193
|
-
const oldValue = this.safeGetValue(originalData, field);
|
|
194
|
-
const newValue = this.safeGetValue(updatedData, field);
|
|
195
|
-
if (oldValue === currentMax) {
|
|
196
|
-
// We're updating the current maximum, need to find the new maximum
|
|
197
|
-
if (newValue >= oldValue) {
|
|
198
|
-
// Simple case: new value is still the maximum
|
|
199
|
-
return newValue;
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
// Harder case: need to recalculate maximum
|
|
203
|
-
// For now, conservatively use the new value
|
|
204
|
-
return newValue;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
else if (newValue > currentMax) {
|
|
208
|
-
// The updated value is a new maximum
|
|
209
|
-
return newValue;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// For other cases, maintain current max
|
|
213
|
-
return currentMax;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Factory class for creating the appropriate strategy
|
|
218
|
-
*/
|
|
219
|
-
export class MetricStrategyFactory {
|
|
220
|
-
/**
|
|
221
|
-
* Clear all custom strategy overrides
|
|
222
|
-
*/
|
|
223
|
-
static clearCustomStrategies() {
|
|
224
|
-
__classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).clear();
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Override a strategy for a specific metric type and model class
|
|
228
|
-
* @param {string} metricType - The type of metric (count, sum, min, max)
|
|
229
|
-
* @param {Function|null} ModelClass - The model class or null for a generic override
|
|
230
|
-
* @param {MetricCalculationStrategy} strategy - The strategy to use
|
|
231
|
-
*/
|
|
232
|
-
static overrideStrategy(metricType, ModelClass, strategy) {
|
|
233
|
-
if (!metricType || !strategy) {
|
|
234
|
-
throw new Error('overrideStrategy requires metricType and strategy');
|
|
235
|
-
}
|
|
236
|
-
if (!(strategy instanceof MetricCalculationStrategy)) {
|
|
237
|
-
throw new Error('strategy must be an instance of MetricCalculationStrategy');
|
|
238
|
-
}
|
|
239
|
-
let key;
|
|
240
|
-
if (ModelClass) {
|
|
241
|
-
// Model-specific override
|
|
242
|
-
key = __classPrivateFieldGet(this, _a, "m", _MetricStrategyFactory_generateStrategyKey).call(this, metricType, ModelClass);
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
// Generic override for all models
|
|
246
|
-
key = `${metricType}::*::*`;
|
|
247
|
-
}
|
|
248
|
-
__classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).set(key, strategy);
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Get the appropriate strategy for a model class and metric type
|
|
252
|
-
* @param {string} metricType - The type of metric (count, sum, min, max)
|
|
253
|
-
* @param {Function} ModelClass - The model class
|
|
254
|
-
* @returns {MetricCalculationStrategy} The appropriate strategy
|
|
255
|
-
*/
|
|
256
|
-
static getStrategy(metricType, ModelClass) {
|
|
257
|
-
const normalizedMetricType = metricType.toLowerCase();
|
|
258
|
-
// Check for model-specific override first
|
|
259
|
-
const specificKey = __classPrivateFieldGet(this, _a, "m", _MetricStrategyFactory_generateStrategyKey).call(this, normalizedMetricType, ModelClass);
|
|
260
|
-
if (__classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).has(specificKey)) {
|
|
261
|
-
return __classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).get(specificKey);
|
|
262
|
-
}
|
|
263
|
-
// Check for metric-only override (works across all models)
|
|
264
|
-
const genericKey = `${normalizedMetricType}::*::*`;
|
|
265
|
-
if (__classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).has(genericKey)) {
|
|
266
|
-
return __classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_customStrategies).get(genericKey);
|
|
267
|
-
}
|
|
268
|
-
// Otherwise, return the default strategy based on the metric type
|
|
269
|
-
const strategyCreator = __classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_defaultStrategies).get(normalizedMetricType) || __classPrivateFieldGet(this, _a, "f", _MetricStrategyFactory_defaultStrategies).get('count');
|
|
270
|
-
return strategyCreator();
|
|
22
|
+
const num = parseFloat(val);
|
|
23
|
+
return isNaN(num) ? 0 : num;
|
|
24
|
+
});
|
|
25
|
+
switch (metricType) {
|
|
26
|
+
case 'sum': return values.reduce((a, b) => a + b, 0);
|
|
27
|
+
case 'min': return values.length ? Math.min(...values) : 0;
|
|
28
|
+
case 'max': return values.length ? Math.max(...values) : 0;
|
|
29
|
+
default: return pks.length;
|
|
271
30
|
}
|
|
272
31
|
}
|
|
273
|
-
_a = MetricStrategyFactory, _MetricStrategyFactory_generateStrategyKey = function _MetricStrategyFactory_generateStrategyKey(metricType, ModelClass) {
|
|
274
|
-
return `${metricType}::${ModelClass.configKey}::${ModelClass.modelName}`;
|
|
275
|
-
};
|
|
276
|
-
// Collection of custom strategy overrides
|
|
277
|
-
_MetricStrategyFactory_customStrategies = { value: new Map() };
|
|
278
|
-
// Default strategy map
|
|
279
|
-
_MetricStrategyFactory_defaultStrategies = { value: new Map([
|
|
280
|
-
['count', () => new CountStrategy()],
|
|
281
|
-
['sum', () => new SumStrategy()],
|
|
282
|
-
['min', () => new MinStrategy()],
|
|
283
|
-
['max', () => new MaxStrategy()]
|
|
284
|
-
]) };
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LiveMetric
|
|
3
|
-
*
|
|
2
|
+
* LiveMetric wraps a metric scalar (count, sum, etc.) and stays live.
|
|
3
|
+
*
|
|
4
|
+
* Uses valueOf()/toString() so it coerces to the scalar naturally:
|
|
5
|
+
* count + 0 // → 5
|
|
6
|
+
* `${count}` // → "5"
|
|
7
|
+
* count > 3 // → true
|
|
8
|
+
*
|
|
9
|
+
* Note: typeof returns "object" and === compares identity, not value.
|
|
10
|
+
* Use == for loose comparison, or +metric for explicit coercion.
|
|
4
11
|
*/
|
|
5
12
|
export class LiveMetric {
|
|
6
13
|
constructor(queryset: any, metricType: any, field?: null);
|
|
@@ -14,8 +21,12 @@ export class LiveMetric {
|
|
|
14
21
|
*/
|
|
15
22
|
refreshFromDb(): any;
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
24
|
+
* Returns the current metric value from the store.
|
|
25
|
+
* Called implicitly by JS when coercing to a primitive (arithmetic, template literals, etc.)
|
|
18
26
|
*/
|
|
27
|
+
valueOf(): any;
|
|
28
|
+
toString(): string;
|
|
29
|
+
/** @deprecated Use valueOf() coercion instead (e.g. +metric, `${metric}`, metric + 0) */
|
|
19
30
|
get value(): any;
|
|
20
31
|
}
|
|
21
32
|
/**
|
|
@@ -23,8 +34,11 @@ export class LiveMetric {
|
|
|
23
34
|
*/
|
|
24
35
|
export class MetricRegistry {
|
|
25
36
|
_stores: Map<any, any>;
|
|
37
|
+
followedMetrics: Set<any>;
|
|
26
38
|
syncManager: any;
|
|
27
39
|
clear(): void;
|
|
40
|
+
followMetric(metricType: any, queryset: any, field?: null): void;
|
|
41
|
+
unfollowMetric(metricType: any, queryset: any, field?: null): void;
|
|
28
42
|
setSyncManager(syncManager: any): void;
|
|
29
43
|
/**
|
|
30
44
|
* Get all metric stores that match a specific queryset
|
|
@@ -5,8 +5,15 @@ import { QueryExecutor } from '../../flavours/django/queryExecutor.js';
|
|
|
5
5
|
import { wrapReactiveMetric } from '../../reactiveAdaptor.js';
|
|
6
6
|
import hash from 'object-hash';
|
|
7
7
|
/**
|
|
8
|
-
* LiveMetric
|
|
9
|
-
*
|
|
8
|
+
* LiveMetric wraps a metric scalar (count, sum, etc.) and stays live.
|
|
9
|
+
*
|
|
10
|
+
* Uses valueOf()/toString() so it coerces to the scalar naturally:
|
|
11
|
+
* count + 0 // → 5
|
|
12
|
+
* `${count}` // → "5"
|
|
13
|
+
* count > 3 // → true
|
|
14
|
+
*
|
|
15
|
+
* Note: typeof returns "object" and === compares identity, not value.
|
|
16
|
+
* Use == for loose comparison, or +metric for explicit coercion.
|
|
10
17
|
*/
|
|
11
18
|
export class LiveMetric {
|
|
12
19
|
constructor(queryset, metricType, field = null) {
|
|
@@ -26,17 +33,24 @@ export class LiveMetric {
|
|
|
26
33
|
return store.sync(true);
|
|
27
34
|
}
|
|
28
35
|
/**
|
|
29
|
-
*
|
|
36
|
+
* Returns the current metric value from the store.
|
|
37
|
+
* Called implicitly by JS when coercing to a primitive (arithmetic, template literals, etc.)
|
|
30
38
|
*/
|
|
31
|
-
|
|
32
|
-
// Get the latest store from the registry
|
|
39
|
+
valueOf() {
|
|
33
40
|
const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
|
|
34
41
|
if (!store) {
|
|
35
42
|
return null;
|
|
36
43
|
}
|
|
37
|
-
// Render the current value
|
|
38
44
|
return store.render();
|
|
39
45
|
}
|
|
46
|
+
toString() {
|
|
47
|
+
const val = this.valueOf();
|
|
48
|
+
return val === null ? '' : String(val);
|
|
49
|
+
}
|
|
50
|
+
/** @deprecated Use valueOf() coercion instead (e.g. +metric, `${metric}`, metric + 0) */
|
|
51
|
+
get value() {
|
|
52
|
+
return this.valueOf();
|
|
53
|
+
}
|
|
40
54
|
}
|
|
41
55
|
/**
|
|
42
56
|
* Registry to manage metric stores
|
|
@@ -45,6 +59,7 @@ export class MetricRegistry {
|
|
|
45
59
|
constructor() {
|
|
46
60
|
// Store both the store and a reference to the queryset as a tuple
|
|
47
61
|
this._stores = new Map();
|
|
62
|
+
this.followedMetrics = new Set();
|
|
48
63
|
this.syncManager = null;
|
|
49
64
|
}
|
|
50
65
|
clear() {
|
|
@@ -52,6 +67,15 @@ export class MetricRegistry {
|
|
|
52
67
|
this.syncManager.unfollowModel(this, entry.store.modelClass);
|
|
53
68
|
}
|
|
54
69
|
this._stores = new Map();
|
|
70
|
+
this.followedMetrics = new Set();
|
|
71
|
+
}
|
|
72
|
+
followMetric(metricType, queryset, field = null) {
|
|
73
|
+
const key = this._makeKey(metricType, queryset, field);
|
|
74
|
+
this.followedMetrics.add(key);
|
|
75
|
+
}
|
|
76
|
+
unfollowMetric(metricType, queryset, field = null) {
|
|
77
|
+
const key = this._makeKey(metricType, queryset, field);
|
|
78
|
+
this.followedMetrics.delete(key);
|
|
55
79
|
}
|
|
56
80
|
setSyncManager(syncManager) {
|
|
57
81
|
this.syncManager = syncManager;
|