@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.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/config/commonCrud.js +3 -4
  3. package/config/listCrud.js +13 -11
  4. package/config/objectCrud.js +36 -24
  5. package/package.json +11 -1
  6. package/types/config/listCrud.d.ts +6 -8
  7. package/types/config/objectCrud.d.ts +33 -22
  8. package/types/tests/benchmarks/fixtures.d.ts +60 -0
  9. package/types/tests/benchmarks/listLayers.bench.d.ts +1 -0
  10. package/types/tests/benchmarks/listPush.bench.d.ts +1 -0
  11. package/types/tests/benchmarks/listStream.bench.d.ts +1 -0
  12. package/types/tests/unit/use/lifecycleCleanup.spec.d.ts +1 -0
  13. package/types/tests/unit/use/listPerformance.spec.d.ts +1 -0
  14. package/types/tests/unit/utils/cancellablePromise.spec.d.ts +1 -0
  15. package/types/use/cancellableIntent.d.ts +9 -2
  16. package/types/use/combineClasses.d.ts +1 -1
  17. package/types/use/listCalculated.d.ts +21 -20
  18. package/types/use/listInstance.d.ts +6 -1
  19. package/types/use/listRelated.d.ts +15 -15
  20. package/types/use/listSort.d.ts +6 -1
  21. package/types/use/listSubscription.d.ts +6 -2
  22. package/types/use/objectCalculated.d.ts +18 -14
  23. package/types/use/objectInstance.d.ts +4 -2
  24. package/types/use/objectRelated.d.ts +11 -9
  25. package/types/use/objectSubscription.d.ts +6 -5
  26. package/types/utils/cancellableFetch.d.ts +2 -3
  27. package/types/utils/cancellablePromise.d.ts +39 -5
  28. package/types/utils/getFakePk.d.ts +2 -2
  29. package/use/cancellableIntent.js +9 -2
  30. package/use/combineClasses.js +1 -1
  31. package/use/listCalculated.js +22 -17
  32. package/use/listFilter.js +4 -3
  33. package/use/listInstance.js +76 -16
  34. package/use/listRelated.js +9 -9
  35. package/use/listSearch.js +1 -1
  36. package/use/listSort.js +68 -23
  37. package/use/listSubscription.js +10 -1
  38. package/use/object.js +7 -8
  39. package/use/objectCalculated.js +13 -11
  40. package/use/objectInstance.js +9 -3
  41. package/use/objectRelated.js +6 -5
  42. package/use/objectSubscription.js +7 -5
  43. package/use/proxyLoadingError.js +3 -0
  44. package/utils/cancellableFetch.js +3 -3
  45. package/utils/cancellablePromise.js +21 -4
  46. package/utils/getFakePk.js +2 -2
@@ -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]: import('vue').ComputedRef<any>,
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]: import('vue').ComputedRef<any>}}} calculatedObjects - The calculated objects.
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 behavior of the list calculated properties.
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, toRef } from "vue";
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
- * computedObjectsRules: {
130
- * someRule: (object, relatedObjects, calculatedObjects) => {
131
- * // some complex calculation, relatedObjects would be assuming there was a listRelated between the two
132
- * // calculatedObjects would be the other calculated objects in the list
133
- * // including yourself, so try not to create circular dependencies
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, re-retrieving the list as someListFilter changes. -->
144
- * <li v-for="obj in listInstance.state.objectsInOrder">
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 computed object or objects based on the rule -->
148
- * {{ listCalculated.state.computedObjects[obj.pk].someRule }}
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
- () => Object.keys(parentState.objects),
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
- () => Object.keys(state.calculatedObjects),
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
- () => Object.keys(parentState.objects),
174
- (newVal) => {
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
- { deep: true, immediate: true, flush: "sync" }
184
+ { immediate: true, flush: "sync" }
184
185
  );
185
186
  });
186
187
 
@@ -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 { CancellablePromise, wrapMaybeCancellable } from "../utils/cancellablePromise.js";
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 behaviors like page persistence.
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
- return target.delete(p);
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
- return _objectsMap.set(key, reactiveValue);
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 the set method to make sure that values are reactive ###
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 CancellablePromise.reject(new ListInstanceError("already loading.", "already-loading"));
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 CancellablePromise.resolve(false);
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
- assignReactiveObject(state.objects, {});
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
- newObjects.forEach((newObject) => {
573
- const pk = String(newObject[state.pkKey]);
574
- if (pk in state.objects) {
575
- self.updateListObject.call(this, newObject);
576
- } else {
577
- self.addListObject.call(this, newObject);
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
  };
@@ -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. Planned to be renamed to
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]: import('vue').ComputedRef<any>,
38
+ * [rule: string]: any,
39
39
  * },
40
- * }} relatedObjects - Stores computed references to related objects, allowing for dynamic access based on object pk and specific rules.
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]: import('vue').ComputedRef<any>,
49
+ * [rule: string]: any,
50
50
  * },
51
- * }} fkForPkAndRule - Maintains computed references to the foreign keys for each object pk and rule, crucial for navigating complex data relationships.
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
- () => Object.keys(parentState.objects),
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
- [() => Object.keys(state.relatedObjects), () => Object.keys(state.relatedObjectsRules || {})],
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(state.objects));
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 behavior of sorting operations unless overridden by specific
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
- () => Object.keys(parentState.objects),
212
- (newKeys) => {
213
- const { addedKeys, removedKeys } = keyDiff(newKeys, Object.keys(criteriaMap));
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].crit ?? [];
231
- const bCrit = criteriaMap[b].crit ?? [];
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 assignOrder =
275
- sortThrottleWaitNumber > 0
276
- ? throttle((v) => {
277
- order.value = v;
278
- }, sortThrottleWaitNumber)
279
- : (v) => {
280
- order.value = v;
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
- watch(rawOrder, (v) => assignOrder(v), { immediate: true });
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: () => {
@@ -54,7 +54,9 @@ export class ListSubscriptionError extends Error {
54
54
  */
55
55
 
56
56
  /**
57
- * @typedef {Pick<import('./loadingError.js').LoadingErrorStatus, "clearError">} ListSubscriptionFunctions - The methods available on a list subscription.
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 { reactive, ref, toRef } from "vue";
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: 'relatedObject.secondOrderId',
162
+ * pkKey: 'relatedItem.firstOrder.secondOrderId',
163
163
  * objects: someOtherObjectsSource.objects,
164
164
  * },
165
165
  * },
166
166
  * calculatedObjectRules: {
167
- * someRule: (object, relatedObject, calculatedObjects) => {
168
- * // some complex calculation, relatedObjects would be assuming there was a listRelated between the two
169
- * // calculatedObjects would be the other calculated objects in the list
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>