@arrai-innovations/reactive-helpers 22.0.0 → 22.1.0
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/README.md +3 -3
- package/config/commonCrud.js +3 -4
- package/config/listCrud.js +13 -11
- package/config/objectCrud.js +36 -24
- package/package.json +11 -1
- package/types/config/listCrud.d.ts +6 -8
- package/types/config/objectCrud.d.ts +33 -22
- package/types/tests/benchmarks/fixtures.d.ts +60 -0
- package/types/tests/benchmarks/listLayers.bench.d.ts +1 -0
- package/types/tests/benchmarks/listPush.bench.d.ts +1 -0
- package/types/tests/benchmarks/listStream.bench.d.ts +1 -0
- package/types/tests/unit/use/lifecycleCleanup.spec.d.ts +1 -0
- package/types/tests/unit/use/listPerformance.spec.d.ts +1 -0
- package/types/tests/unit/utils/cancellablePromise.spec.d.ts +1 -0
- package/types/use/cancellableIntent.d.ts +9 -2
- package/types/use/combineClasses.d.ts +1 -1
- package/types/use/listCalculated.d.ts +21 -20
- package/types/use/listInstance.d.ts +6 -1
- package/types/use/listRelated.d.ts +15 -15
- package/types/use/listSort.d.ts +6 -1
- package/types/use/listSubscription.d.ts +6 -2
- package/types/use/objectCalculated.d.ts +18 -14
- package/types/use/objectInstance.d.ts +4 -2
- package/types/use/objectRelated.d.ts +11 -9
- package/types/use/objectSubscription.d.ts +6 -5
- package/types/utils/cancellableFetch.d.ts +2 -3
- package/types/utils/cancellablePromise.d.ts +39 -5
- package/types/utils/getFakePk.d.ts +2 -2
- package/use/cancellableIntent.js +9 -2
- package/use/combineClasses.js +1 -1
- package/use/listCalculated.js +22 -17
- package/use/listFilter.js +4 -3
- package/use/listInstance.js +76 -16
- package/use/listRelated.js +9 -9
- package/use/listSearch.js +1 -1
- package/use/listSort.js +68 -23
- package/use/listSubscription.js +10 -1
- package/use/object.js +7 -8
- package/use/objectCalculated.js +13 -11
- package/use/objectInstance.js +9 -3
- package/use/objectRelated.js +6 -5
- package/use/objectSubscription.js +7 -5
- package/use/proxyLoadingError.js +3 -0
- package/utils/cancellableFetch.js +3 -3
- package/utils/cancellablePromise.js +21 -4
- package/utils/getFakePk.js +2 -2
package/use/listCalculated.js
CHANGED
|
@@ -21,17 +21,17 @@ import { computed, effectScope, nextTick, reactive, ref, toRef, toRefs, unref, w
|
|
|
21
21
|
* [rule: string]: any,
|
|
22
22
|
* },
|
|
23
23
|
* calculatedObjects: {
|
|
24
|
-
* [rule: string]:
|
|
24
|
+
* [rule: string]: any,
|
|
25
25
|
* }
|
|
26
26
|
* ) => any,
|
|
27
|
-
* }} ListCalculatedRules - Defines rules for dynamically calculating new properties for objects in a list. Each rule is a function that takes an object from the list, optionally its related objects, and previously calculated properties to compute a new property. These functions are reactive and re-evaluate when underlying dependencies change.
|
|
27
|
+
* }} ListCalculatedRules - Defines rules for dynamically calculating new properties for objects in a list. Each rule is a function that takes an object from the list, optionally its related objects, and previously calculated properties to compute a new property. These functions are reactive and re-evaluate when underlying dependencies change. Each entry of the third argument is backed by a computed, but it is read through a reactive proxy that unwraps it, so a rule reads `calculatedObjects.otherRule` directly and never `.value`.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* The raw state for a list calculated.
|
|
32
32
|
*
|
|
33
33
|
* @typedef {object} ListCalculatedRawState - The raw state for a list calculated property.
|
|
34
|
-
* @property {{[pk: import('../config/commonCrud.js').Pk]: {[rule: string]:
|
|
34
|
+
* @property {{[pk: import('../config/commonCrud.js').Pk]: {[rule: string]: any}}} calculatedObjects - The calculated objects, by object pk and then rule name. Each entry is backed by a computed, but it is read through a reactive proxy that unwraps it, so reads yield the calculated value and never carry a `.value`.
|
|
35
35
|
* @property {ListCalculatedRules} calculatedObjectsRules - The rules for the calculated objects.
|
|
36
36
|
* @property {boolean} calculatedObjectsParentStateObjectsWatchRunning - Whether the parent state objects watch is running.
|
|
37
37
|
* @property {boolean} calculatedObjectsWatchRunning - Whether the calculated objects watch is running.
|
|
@@ -66,7 +66,7 @@ import { computed, effectScope, nextTick, reactive, ref, toRef, toRefs, unref, w
|
|
|
66
66
|
/**
|
|
67
67
|
* The options to create a list calculated composition function.
|
|
68
68
|
*
|
|
69
|
-
* @typedef {object} ListCalculatedOptions - Options to configure the
|
|
69
|
+
* @typedef {object} ListCalculatedOptions - Options to configure the behaviour of the list calculated properties.
|
|
70
70
|
* @property {ListCalculatedParentState} parentState - The parent state that interacts with the calculated objects.
|
|
71
71
|
* @property {import('vue').Ref<ListCalculatedRules>} calculatedObjectsRules - A reactive reference to rules used for dynamic calculations
|
|
72
72
|
* within list objects. Proper setup of this reference ensures that updates are managed reactively, including deep
|
|
@@ -114,7 +114,7 @@ export function useListCalculateds(listCalculatedArgs) {
|
|
|
114
114
|
* ```vue
|
|
115
115
|
* <script setup>
|
|
116
116
|
* import { useListSubscription, useListCalculated } from "@arrai-innovations/reactive-helpers";
|
|
117
|
-
* import { reactive
|
|
117
|
+
* import { reactive } from "vue";
|
|
118
118
|
*
|
|
119
119
|
* const listSubscriptionProps = reactive({
|
|
120
120
|
* // whatever props you need to get the list to work with your crud implementation
|
|
@@ -123,14 +123,15 @@ export function useListCalculateds(listCalculatedArgs) {
|
|
|
123
123
|
* pkKey: "pk",
|
|
124
124
|
* intendToList: true,
|
|
125
125
|
* });
|
|
126
|
-
* const listSubscription = useListSubscription(listSubscriptionProps);
|
|
126
|
+
* const listSubscription = useListSubscription({ props: listSubscriptionProps });
|
|
127
127
|
* const listCalculatedProps = reactive({
|
|
128
128
|
* parentState: listSubscription.state,
|
|
129
|
-
*
|
|
130
|
-
* someRule: (object,
|
|
131
|
-
* // some complex calculation
|
|
132
|
-
* //
|
|
133
|
-
* //
|
|
129
|
+
* calculatedObjectsRules: {
|
|
130
|
+
* someRule: (object, relatedObject, calculatedObjects) => {
|
|
131
|
+
* // some complex calculation. relatedObject holds this object's related objects, and is only
|
|
132
|
+
* // populated when a useListRelated sits between the list and this composable.
|
|
133
|
+
* // calculatedObjects holds the other calculated values for this same object,
|
|
134
|
+
* // including this rule, so try not to create circular dependencies.
|
|
134
135
|
* // this is used as a computed body.
|
|
135
136
|
* return object.someProperty + object.someOtherProperty;
|
|
136
137
|
* }
|
|
@@ -140,12 +141,12 @@ export function useListCalculateds(listCalculatedArgs) {
|
|
|
140
141
|
* </script>
|
|
141
142
|
* <template>
|
|
142
143
|
* <ul>
|
|
143
|
-
* <!-- reactive list of objects,
|
|
144
|
-
* <li v-for="obj in
|
|
144
|
+
* <!-- reactive list of objects, kept current by the configured subscription function. -->
|
|
145
|
+
* <li v-for="obj in listSubscription.state.objectsInOrder">
|
|
145
146
|
* {{ obj }}
|
|
146
147
|
* <div>
|
|
147
|
-
* <!-- the
|
|
148
|
-
* {{ listCalculated.state.
|
|
148
|
+
* <!-- the calculated value for this object, based on the rule -->
|
|
149
|
+
* {{ listCalculated.state.calculatedObjects[obj.pk].someRule }}
|
|
149
150
|
* </div>
|
|
150
151
|
* </li>
|
|
151
152
|
* </ul>
|
|
@@ -256,7 +257,7 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
watch(
|
|
259
|
-
() =>
|
|
260
|
+
() => parentState.objectsVersion,
|
|
260
261
|
() => {
|
|
261
262
|
state.calculatedObjectsParentStateObjectsWatchRunning = true;
|
|
262
263
|
},
|
|
@@ -264,7 +265,11 @@ export function useListCalculated({ parentState, calculatedObjectsRules }) {
|
|
|
264
265
|
);
|
|
265
266
|
watch(() => Object.keys(parentState.objects), parentStateObjectsWatch, { immediate: true });
|
|
266
267
|
watch(
|
|
267
|
-
|
|
268
|
+
[
|
|
269
|
+
() => parentState.objectsVersion,
|
|
270
|
+
() =>
|
|
271
|
+
state.calculatedObjectsRules ? Object.keys(state.calculatedObjectsRules) : state.calculatedObjectsRules,
|
|
272
|
+
],
|
|
268
273
|
() => {
|
|
269
274
|
state.calculatedObjectsWatchRunning = true;
|
|
270
275
|
},
|
package/use/listFilter.js
CHANGED
|
@@ -170,8 +170,9 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
170
170
|
|
|
171
171
|
es.run(() => {
|
|
172
172
|
watch(
|
|
173
|
-
() =>
|
|
174
|
-
(
|
|
173
|
+
() => parentState.objectsVersion,
|
|
174
|
+
() => {
|
|
175
|
+
const newVal = Object.keys(parentState.objects);
|
|
175
176
|
const { addedKeys, removedKeys } = keyDiff(newVal, Object.keys(includeMap));
|
|
176
177
|
for (const pk of removedKeys) {
|
|
177
178
|
disposeIncludeComputed(pk);
|
|
@@ -180,7 +181,7 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
180
181
|
ensureIncludeComputed(pk);
|
|
181
182
|
}
|
|
182
183
|
},
|
|
183
|
-
{
|
|
184
|
+
{ immediate: true, flush: "sync" }
|
|
184
185
|
);
|
|
185
186
|
});
|
|
186
187
|
|
package/use/listInstance.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getFakePk } from "../utils/getFakePk.js";
|
|
|
4
4
|
import { useLoadingError } from "./loadingError.js";
|
|
5
5
|
import inspect from "browser-util-inspect";
|
|
6
6
|
import { computed, effectScope, isReactive, reactive, readonly, ref, shallowReactive } from "vue";
|
|
7
|
-
import {
|
|
7
|
+
import { wrapMaybeCancellable } from "../utils/cancellablePromise.js";
|
|
8
8
|
import { refIfReactive } from "../utils/refIfReactive.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -96,6 +96,7 @@ export class ListInstanceError extends Error {
|
|
|
96
96
|
* @property {object} params - Arguments passed to the server for listing operations.
|
|
97
97
|
* @property {ObjectsMap} objectsMap - The map of objects stored by their pks.
|
|
98
98
|
* @property {ObjectsByPk} objects - The list objects stored by their pks.
|
|
99
|
+
* @property {number} objectsVersion - Increments when the set of object keys changes.
|
|
99
100
|
* @property {ListOrder} order - The order of objects in the list.
|
|
100
101
|
* @property {ObjectsInOrder} objectsInOrder - The objects in the order specified by the list.
|
|
101
102
|
* @property {import('vue').ShallowReactive<PaginateInfo>} paginateInfo - Pagination information for the list.
|
|
@@ -224,7 +225,7 @@ export function useListInstances(listInstanceArgs) {
|
|
|
224
225
|
* ```
|
|
225
226
|
*
|
|
226
227
|
* @param {ListInstanceOptions} options - Specifies the configuration options for creating a list instance, including
|
|
227
|
-
* properties for CRUD operations and UI
|
|
228
|
+
* properties for CRUD operations and UI behaviours like page persistence.
|
|
228
229
|
* @returns {ListInstance} The list instance.
|
|
229
230
|
* @throws {ListInstanceError} If the props are missing.
|
|
230
231
|
*/
|
|
@@ -237,6 +238,30 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
const es = effectScope();
|
|
241
|
+
const objectsVersion = ref(0);
|
|
242
|
+
let objectsBatchDepth = 0;
|
|
243
|
+
let objectsChangedDuringBatch = false;
|
|
244
|
+
|
|
245
|
+
function triggerObjectsChanged() {
|
|
246
|
+
if (objectsBatchDepth) {
|
|
247
|
+
objectsChangedDuringBatch = true;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
objectsVersion.value++;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function batchObjectChanges(fn) {
|
|
254
|
+
objectsBatchDepth++;
|
|
255
|
+
try {
|
|
256
|
+
return fn();
|
|
257
|
+
} finally {
|
|
258
|
+
objectsBatchDepth--;
|
|
259
|
+
if (!objectsBatchDepth && objectsChangedDuringBatch) {
|
|
260
|
+
objectsChangedDuringBatch = false;
|
|
261
|
+
objectsVersion.value++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
240
265
|
|
|
241
266
|
const [_objectsProxy, _objectsMapProxy] = es.run(() => {
|
|
242
267
|
// ### do not use this directly, because we proxy `set` to make sure that values are reactive ###
|
|
@@ -273,10 +298,14 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
273
298
|
if (typeof prop === "symbol") {
|
|
274
299
|
return Reflect.set(target, prop, value);
|
|
275
300
|
}
|
|
301
|
+
const hadKey = target.has(prop);
|
|
276
302
|
if (!isReactive(value)) {
|
|
277
303
|
value = reactive(value);
|
|
278
304
|
}
|
|
279
305
|
target.set(prop, value); // map.set() returns the map, we don't need that
|
|
306
|
+
if (!hadKey) {
|
|
307
|
+
triggerObjectsChanged();
|
|
308
|
+
}
|
|
280
309
|
return true;
|
|
281
310
|
},
|
|
282
311
|
ownKeys(target) {
|
|
@@ -289,7 +318,11 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
289
318
|
if (typeof p === "symbol") {
|
|
290
319
|
return Reflect.deleteProperty(target, p);
|
|
291
320
|
}
|
|
292
|
-
|
|
321
|
+
const deleted = target.delete(p);
|
|
322
|
+
if (deleted) {
|
|
323
|
+
triggerObjectsChanged();
|
|
324
|
+
}
|
|
325
|
+
return deleted;
|
|
293
326
|
},
|
|
294
327
|
getOwnPropertyDescriptor(target, prop) {
|
|
295
328
|
if (prop === Symbol.toStringTag) {
|
|
@@ -331,17 +364,40 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
331
364
|
|
|
332
365
|
// ### for deep reactivity on map items, we need to make sure each is reactive ###
|
|
333
366
|
const _objectsMapWrappedSet = (key, value) => {
|
|
367
|
+
const hadKey = _objectsMap.has(key);
|
|
334
368
|
const reactiveValue =
|
|
335
369
|
typeof value === "object" && value !== null && !isReactive(value) ? reactive(value) : value;
|
|
336
|
-
|
|
370
|
+
const result = _objectsMap.set(key, reactiveValue);
|
|
371
|
+
if (!hadKey) {
|
|
372
|
+
triggerObjectsChanged();
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
};
|
|
376
|
+
const _objectsMapWrappedDelete = (key) => {
|
|
377
|
+
const deleted = _objectsMap.delete(key);
|
|
378
|
+
if (deleted) {
|
|
379
|
+
triggerObjectsChanged();
|
|
380
|
+
}
|
|
381
|
+
return deleted;
|
|
382
|
+
};
|
|
383
|
+
const _objectsMapWrappedClear = () => {
|
|
384
|
+
if (!_objectsMap.size) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
_objectsMap.clear();
|
|
388
|
+
triggerObjectsChanged();
|
|
337
389
|
};
|
|
338
390
|
|
|
339
|
-
// ### wrapping
|
|
391
|
+
// ### wrapping mutation methods to enforce reactive values and track structural changes ###
|
|
340
392
|
const _objectsMapProxy = new Proxy(_objectsMap, {
|
|
341
393
|
get(target, prop, receiver) {
|
|
342
394
|
switch (prop) {
|
|
343
395
|
case "set":
|
|
344
396
|
return _objectsMapWrappedSet;
|
|
397
|
+
case "delete":
|
|
398
|
+
return _objectsMapWrappedDelete;
|
|
399
|
+
case "clear":
|
|
400
|
+
return _objectsMapWrappedClear;
|
|
345
401
|
}
|
|
346
402
|
return Reflect.get(target, prop, receiver);
|
|
347
403
|
},
|
|
@@ -367,10 +423,10 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
367
423
|
// /** @type {{[key: string]: import('../use/objectInstance.js').ExistingCrudObject}} */
|
|
368
424
|
// objects: /** @type {{[key: string]: import('../use/objectInstance.js').ExistingCrudObject}} */ _objectsProxy,
|
|
369
425
|
objects: _objectsProxy,
|
|
426
|
+
objectsVersion,
|
|
370
427
|
loading: loadingError.loading,
|
|
371
428
|
errored: loadingError.errored,
|
|
372
429
|
error: loadingError.error,
|
|
373
|
-
// order: es.run(() => computed(() => Array.from(state.objectsMap.keys()))),
|
|
374
430
|
order: es.run(() =>
|
|
375
431
|
computed(() => {
|
|
376
432
|
return [...state.objectsMap.keys()];
|
|
@@ -402,7 +458,7 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
402
458
|
return promises.list;
|
|
403
459
|
}
|
|
404
460
|
if (state.loading) {
|
|
405
|
-
return
|
|
461
|
+
return Promise.reject(new ListInstanceError("already loading.", "already-loading"));
|
|
406
462
|
}
|
|
407
463
|
loadingError.clearError();
|
|
408
464
|
loadingError.setLoading();
|
|
@@ -424,7 +480,7 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
424
480
|
} catch (e) {
|
|
425
481
|
loadingError.setError(e);
|
|
426
482
|
loadingError.clearLoading();
|
|
427
|
-
return
|
|
483
|
+
return Promise.resolve(false);
|
|
428
484
|
}
|
|
429
485
|
promises.list = wrapMaybeCancellable(
|
|
430
486
|
listPromise
|
|
@@ -469,7 +525,9 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
469
525
|
pkKey: state.pkKey,
|
|
470
526
|
})
|
|
471
527
|
.then(() => {
|
|
472
|
-
|
|
528
|
+
batchObjectChanges(() => {
|
|
529
|
+
assignReactiveObject(state.objects, {});
|
|
530
|
+
});
|
|
473
531
|
loadingError.clearError();
|
|
474
532
|
return Promise.resolve(true);
|
|
475
533
|
})
|
|
@@ -569,13 +627,15 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
569
627
|
clearError: loadingError.clearError,
|
|
570
628
|
getFakePk: () => getFakePk(state.objects, state.pkKey),
|
|
571
629
|
pushObjects: (newObjects) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
630
|
+
batchObjectChanges(() => {
|
|
631
|
+
newObjects.forEach((newObject) => {
|
|
632
|
+
const pk = String(newObject[state.pkKey]);
|
|
633
|
+
if (pk in state.objects) {
|
|
634
|
+
self.updateListObject.call(this, newObject);
|
|
635
|
+
} else {
|
|
636
|
+
self.addListObject.call(this, newObject);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
579
639
|
});
|
|
580
640
|
},
|
|
581
641
|
};
|
package/use/listRelated.js
CHANGED
|
@@ -19,8 +19,8 @@ import { computed, effectScope, nextTick, onScopeDispose, reactive, ref, toRef,
|
|
|
19
19
|
// todo: pkKey is misnamed, it should be fkKey... this will be a major breaking change
|
|
20
20
|
/**
|
|
21
21
|
* @typedef {object} ListRelatedRule - The rule for defining relationships for objects in a list.
|
|
22
|
-
* @property {string} pkKey - Specifies the foreign key used to link objects across lists.
|
|
23
|
-
* 'fkKey' to better reflect its usage.
|
|
22
|
+
* @property {string} [pkKey] - Specifies the foreign key used to link objects across lists. Defaults to the rule's
|
|
23
|
+
* own key when omitted. Planned to be renamed to 'fkKey' to better reflect its usage.
|
|
24
24
|
* @property {string[]} [order] - Specifies the order in which related objects should be sorted, if applicable.
|
|
25
25
|
* @property {import('./listInstance.js').ObjectsByPk} objects - The objects that can be related based on the foreign key.
|
|
26
26
|
*/
|
|
@@ -35,20 +35,20 @@ import { computed, effectScope, nextTick, onScopeDispose, reactive, ref, toRef,
|
|
|
35
35
|
* @typedef {object} ListRelatedRawState - Represents the internal state used by the list related composition function. It manages and computes the relationships between objects based on specified rules, providing real-time updates to related objects as the parent state changes.
|
|
36
36
|
* @property {{
|
|
37
37
|
* [pk: import('../config/commonCrud.js').Pk]: {
|
|
38
|
-
* [rule: string]:
|
|
38
|
+
* [rule: string]: any,
|
|
39
39
|
* },
|
|
40
|
-
* }} relatedObjects -
|
|
40
|
+
* }} relatedObjects - The related objects, by object pk and then rule name. Each entry is backed by a computed, but it is read through a reactive proxy that unwraps it, so reads yield the related object (or array of related objects) and never carry a `.value`.
|
|
41
41
|
* @property {ListRelatedRules} relatedObjectsRules - Defines the rules for establishing relationships, such as foreign key links and sorting orders.
|
|
42
42
|
* @property {{
|
|
43
43
|
* [pk: import('../config/commonCrud.js').Pk]: {
|
|
44
44
|
* [rule: string]: import('vue').ComputedRef<[object, string]>,
|
|
45
45
|
* },
|
|
46
|
-
* }} objAndKeyForPkAndRule - Maps each object pk and rule to a tuple consisting of the related object and its respective key, facilitating direct data manipulation.
|
|
46
|
+
* }} objAndKeyForPkAndRule - Maps each object pk and rule to a tuple consisting of the related object and its respective key, facilitating direct data manipulation. Reads through the reactive state unwrap the computed to the tuple itself, so `.value` is not used.
|
|
47
47
|
* @property {{
|
|
48
48
|
* [pk: import('../config/commonCrud.js').Pk]: {
|
|
49
|
-
* [rule: string]:
|
|
49
|
+
* [rule: string]: any,
|
|
50
50
|
* },
|
|
51
|
-
* }} fkForPkAndRule -
|
|
51
|
+
* }} fkForPkAndRule - The foreign key for each object pk and rule, crucial for navigating complex data relationships. Each entry is backed by a computed that the reactive proxy unwraps on read.
|
|
52
52
|
* @property {boolean} relatedObjectsParentStateObjectsWatchRunning - Flags whether the watch on parent state objects is currently active, ensuring updates trigger as needed.
|
|
53
53
|
* @property {boolean} relatedObjectsWatchRunning - Indicates if watches on the related objects themselves are active, managing updates efficiently.
|
|
54
54
|
* @property {boolean} relatedRunning - Signals whether any computations related to object relationships are currently in progress.
|
|
@@ -329,7 +329,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
|
|
|
329
329
|
|
|
330
330
|
es.run(() => {
|
|
331
331
|
watch(
|
|
332
|
-
() =>
|
|
332
|
+
() => parentState.objectsVersion,
|
|
333
333
|
() => {
|
|
334
334
|
state.relatedObjectsParentStateObjectsWatchRunning = true;
|
|
335
335
|
},
|
|
@@ -337,7 +337,7 @@ export function useListRelated({ parentState, relatedObjectsRules }) {
|
|
|
337
337
|
);
|
|
338
338
|
watch(() => Object.keys(parentState.objects), parentStateObjectsWatch, { immediate: true });
|
|
339
339
|
watch(
|
|
340
|
-
[() =>
|
|
340
|
+
[() => parentState.objectsVersion, () => Object.keys(state.relatedObjectsRules || {})],
|
|
341
341
|
() => {
|
|
342
342
|
state.relatedObjectsWatchRunning = true;
|
|
343
343
|
},
|
package/use/listSearch.js
CHANGED
|
@@ -262,7 +262,7 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
262
262
|
addedKeys: addedObjectPks,
|
|
263
263
|
removedKeys: removedObjectPks,
|
|
264
264
|
sameKeys: sameObjectPks,
|
|
265
|
-
} = keyDiff(Object.keys(parentState.objects), Object.keys(
|
|
265
|
+
} = keyDiff(Object.keys(parentState.objects), Object.keys(objectEffectScopes));
|
|
266
266
|
const { addedKeys: addedTextSearchRules, removedKeys: removedTextSearchRules } = keyDiff(
|
|
267
267
|
state.textSearchRules,
|
|
268
268
|
previousTextSearchRules
|
package/use/listSort.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
+
import { loadingCombine } from "../utils/loadingCombine.js";
|
|
3
|
+
import { proxyRunning } from "../utils/proxyRunning.js";
|
|
2
4
|
import get from "lodash-es/get.js";
|
|
3
5
|
import identity from "lodash-es/identity.js";
|
|
4
6
|
import throttle from "lodash-es/throttle.js";
|
|
5
|
-
import { computed, effectScope, reactive, ref, toRef, toRefs, unref, watch } from "vue";
|
|
7
|
+
import { computed, effectScope, onScopeDispose, reactive, ref, toRef, toRefs, unref, watch } from "vue";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Provides a Vue 3 composable for sorting lists based on dynamic and customizable rules. This module integrates
|
|
@@ -23,7 +25,7 @@ const defaultOptions = {
|
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* Sets default configuration options for all list sorting operations within the application. This function allows
|
|
26
|
-
* global settings to be specified that affect the
|
|
28
|
+
* global settings to be specified that affect the behaviour of sorting operations unless overridden by specific
|
|
27
29
|
* instance configurations.
|
|
28
30
|
*
|
|
29
31
|
* @param {object} options - Configuration options to set as defaults for list sorting.
|
|
@@ -47,6 +49,7 @@ export function setListSortDefaultOptions({ sortThrottleWait }) {
|
|
|
47
49
|
* @typedef {object} ListSortRawState - Represents the raw state used by the list sorting functionality. Includes all configurations and state necessary to manage sorting operations within a Vue application.
|
|
48
50
|
* @property {OrderByRule[]} orderByRules - Current sorting rules applied to the list.
|
|
49
51
|
* @property {boolean[]} orderByDesc - Flags indicating whether each sort criterion is in descending order.
|
|
52
|
+
* @property {import('vue').ComputedRef<boolean|undefined>} running - Whether the sort is settling a pending reorder, combined with the upstream running state so it propagates through the composed list state. True from when a new order is computed until the throttled reorder lands.
|
|
50
53
|
*/
|
|
51
54
|
|
|
52
55
|
/**
|
|
@@ -161,6 +164,13 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
161
164
|
})();
|
|
162
165
|
const es = effectScope();
|
|
163
166
|
|
|
167
|
+
/** @type {import('vue').Ref<boolean|undefined>} */
|
|
168
|
+
const parentRunning = ref(undefined);
|
|
169
|
+
proxyRunning(parentState, "running", parentRunning);
|
|
170
|
+
// True from the moment a new order is pending until the (possibly throttled) reorder lands.
|
|
171
|
+
const sortWatchRunning = ref(true);
|
|
172
|
+
const running = computed(() => loadingCombine(sortWatchRunning.value, parentRunning.value));
|
|
173
|
+
|
|
164
174
|
const internalState = reactive({
|
|
165
175
|
orderByRules,
|
|
166
176
|
orderByDesc: computed(() =>
|
|
@@ -206,29 +216,34 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
206
216
|
return crit;
|
|
207
217
|
}
|
|
208
218
|
|
|
219
|
+
function syncCriteria(newKeys) {
|
|
220
|
+
const { addedKeys, removedKeys } = keyDiff(newKeys, Object.keys(criteriaMap));
|
|
221
|
+
for (const pk of removedKeys) {
|
|
222
|
+
criteriaMap[pk].scope.stop();
|
|
223
|
+
delete criteriaMap[pk];
|
|
224
|
+
}
|
|
225
|
+
for (const pk of addedKeys) {
|
|
226
|
+
ensureCriteria(pk);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
209
230
|
es.run(() => {
|
|
210
231
|
watch(
|
|
211
|
-
() =>
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
for (const pk of removedKeys) {
|
|
215
|
-
criteriaMap[pk].scope.stop();
|
|
216
|
-
delete criteriaMap[pk];
|
|
217
|
-
}
|
|
218
|
-
for (const pk of addedKeys) {
|
|
219
|
-
ensureCriteria(pk);
|
|
220
|
-
}
|
|
232
|
+
() => parentState.objectsVersion,
|
|
233
|
+
() => {
|
|
234
|
+
syncCriteria(Object.keys(parentState.objects));
|
|
221
235
|
},
|
|
222
236
|
{ immediate: true, flush: "sync" }
|
|
223
237
|
);
|
|
238
|
+
watch(() => Object.keys(parentState.objects), syncCriteria);
|
|
224
239
|
});
|
|
225
240
|
|
|
226
241
|
const rawOrder = computed(() => {
|
|
227
242
|
const arr = [...unref(toRef(parentState, "order"))];
|
|
228
243
|
const rulesArr = internalState.orderByRules?.filter(identity) || [];
|
|
229
244
|
return arr.sort((a, b) => {
|
|
230
|
-
const aCrit = criteriaMap[a]
|
|
231
|
-
const bCrit = criteriaMap[b]
|
|
245
|
+
const aCrit = criteriaMap[a]?.crit ?? [];
|
|
246
|
+
const bCrit = criteriaMap[b]?.crit ?? [];
|
|
232
247
|
for (let i = 0; i < rulesArr.length; i++) {
|
|
233
248
|
const rule = rulesArr[i];
|
|
234
249
|
let x = aCrit[i],
|
|
@@ -271,16 +286,45 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
271
286
|
});
|
|
272
287
|
|
|
273
288
|
const order = ref([]);
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
289
|
+
const writeOrder = (v) => {
|
|
290
|
+
order.value = v;
|
|
291
|
+
// the pending reorder has landed; let running settle unless the parent is still running
|
|
292
|
+
sortWatchRunning.value = false;
|
|
293
|
+
};
|
|
294
|
+
// Held separately from assignOrder so the throttle's own cancel stays typed.
|
|
295
|
+
const throttledOrder = sortThrottleWaitNumber > 0 ? throttle(writeOrder, sortThrottleWaitNumber) : null;
|
|
296
|
+
const assignOrder = throttledOrder ?? writeOrder;
|
|
282
297
|
|
|
283
|
-
|
|
298
|
+
es.run(() => {
|
|
299
|
+
// Raise running synchronously the moment a new order is pending, so a throttled reorder keeps
|
|
300
|
+
// running true until it lands. This mirrors the related, calculated, and search layers, and
|
|
301
|
+
// lets the composed manager's state.running reflect the final reorder settling. Watch cheap
|
|
302
|
+
// signals rather than rawOrder itself: a sync watcher re-evaluates its source on every
|
|
303
|
+
// invalidation, and rawOrder performs the full sort, so watching it synchronously re-sorts
|
|
304
|
+
// the list once per reactive write while a page is being pushed. The batched objectsVersion
|
|
305
|
+
// covers structural changes and the rule reads cover rule changes; reorders triggered another
|
|
306
|
+
// way (a row edit, a parent subset change) raise running in the pre-flush watcher below.
|
|
307
|
+
watch(
|
|
308
|
+
() => [parentState.objectsVersion, internalState.orderByRules, internalState.orderByDesc],
|
|
309
|
+
() => {
|
|
310
|
+
sortWatchRunning.value = true;
|
|
311
|
+
},
|
|
312
|
+
{ flush: "sync" }
|
|
313
|
+
);
|
|
314
|
+
watch(
|
|
315
|
+
rawOrder,
|
|
316
|
+
(v) => {
|
|
317
|
+
sortWatchRunning.value = true;
|
|
318
|
+
assignOrder(v);
|
|
319
|
+
},
|
|
320
|
+
{ immediate: true }
|
|
321
|
+
);
|
|
322
|
+
// A throttled trailing reorder is a timer, not a reactive effect, so disposal would otherwise
|
|
323
|
+
// leave it pending and let it write order after the layer stopped.
|
|
324
|
+
if (throttledOrder) {
|
|
325
|
+
onScopeDispose(() => throttledOrder.cancel());
|
|
326
|
+
}
|
|
327
|
+
});
|
|
284
328
|
|
|
285
329
|
// 6) objectsInOrder just follows that
|
|
286
330
|
const objectsInOrder = computed(() => order.value.map((pk) => parentState.objects[pk]));
|
|
@@ -293,6 +337,7 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
293
337
|
objects,
|
|
294
338
|
order,
|
|
295
339
|
objectsInOrder,
|
|
340
|
+
running,
|
|
296
341
|
}),
|
|
297
342
|
parentState,
|
|
298
343
|
stop: () => {
|
package/use/listSubscription.js
CHANGED
|
@@ -54,7 +54,9 @@ export class ListSubscriptionError extends Error {
|
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* @typedef {Pick<import('./loadingError.js').LoadingErrorStatus, "clearError">
|
|
57
|
+
* @typedef {Pick<import('./loadingError.js').LoadingErrorStatus, "clearError"> & {
|
|
58
|
+
* stop: () => void
|
|
59
|
+
* }} ListSubscriptionFunctions - The methods available on a list subscription.
|
|
58
60
|
*/
|
|
59
61
|
|
|
60
62
|
/**
|
|
@@ -337,5 +339,12 @@ export function useListSubscription({ listInstance, props, handlers }) {
|
|
|
337
339
|
listIntent,
|
|
338
340
|
subscribeIntent,
|
|
339
341
|
clearError: proxyLoadingError.clearError,
|
|
342
|
+
// Stops both intents, mirroring useObjectSubscription, so a caller that owns this
|
|
343
|
+
// subscription's lifetime does not have to know its intent inventory. The wrapped
|
|
344
|
+
// instance keeps its state and still takes manual calls.
|
|
345
|
+
stop: () => {
|
|
346
|
+
listIntent.stop();
|
|
347
|
+
subscribeIntent.stop();
|
|
348
|
+
},
|
|
340
349
|
};
|
|
341
350
|
}
|
package/use/object.js
CHANGED
|
@@ -116,7 +116,7 @@ function mergeFns(source) {
|
|
|
116
116
|
* ```
|
|
117
117
|
* <script setup>
|
|
118
118
|
* import { useObject } from "@arrai-innovations/reactive-helpers";
|
|
119
|
-
* import {
|
|
119
|
+
* import { computed, reactive, toRef } from "vue";
|
|
120
120
|
*
|
|
121
121
|
* const someObjectsSource = reactive({
|
|
122
122
|
* objects: {
|
|
@@ -159,25 +159,24 @@ function mergeFns(source) {
|
|
|
159
159
|
* order: ['3','1','2'],
|
|
160
160
|
* },
|
|
161
161
|
* secondOrder: {
|
|
162
|
-
* pkKey: '
|
|
162
|
+
* pkKey: 'relatedItem.firstOrder.secondOrderId',
|
|
163
163
|
* objects: someOtherObjectsSource.objects,
|
|
164
164
|
* },
|
|
165
165
|
* },
|
|
166
166
|
* calculatedObjectRules: {
|
|
167
|
-
* someRule: (object, relatedObject
|
|
168
|
-
* // some complex calculation
|
|
169
|
-
* //
|
|
170
|
-
* // including yourself, so try not to create circular dependencies
|
|
167
|
+
* someRule: (object, relatedObject) => {
|
|
168
|
+
* // some complex calculation. relatedObject holds the objects matched by relatedObjectRules above,
|
|
169
|
+
* // keyed by rule name.
|
|
171
170
|
* // this is used as a computed body.
|
|
172
171
|
* return object.foo + object.name;
|
|
173
172
|
* },
|
|
174
|
-
* ...
|
|
173
|
+
* // ...further rules
|
|
175
174
|
* },
|
|
176
175
|
* intendToRetrieve: false,
|
|
177
176
|
* intendToSubscribe: false,
|
|
178
177
|
* });
|
|
179
178
|
* objectProps.intendToRetrieve = objectProps.intendToSubscribe = computed(()=> !!props.pk);
|
|
180
|
-
* const objectManager = useObject(objectProps);
|
|
179
|
+
* const objectManager = useObject({ props: objectProps });
|
|
181
180
|
* // objectManager.state.object comes back from the server (via configured crud retrieve function)
|
|
182
181
|
* // { id: 2, name: 'two', foo: 'bar', some_objects_id: 2, some_objects_list_ids: ['1','2','3'] }
|
|
183
182
|
* </script>
|