@arrai-innovations/reactive-helpers 23.0.0 → 23.0.2
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/package.json +7 -2
- package/types/tests/benchmarks/fixtures.d.ts +1 -1
- package/types/tests/benchmarks/listObserved.bench.d.ts +1 -0
- package/types/tests/benchmarks/reviewFixtures.d.ts +102 -0
- package/types/tests/unit/use/listLoadingPropagation.spec.d.ts +1 -0
- package/types/tests/unit/use/listMembershipWatch.spec.d.ts +1 -0
- package/types/tests/unit/use/listNotifications.spec.d.ts +1 -0
- package/types/tests/unit/use/listObjectsVersion.spec.d.ts +1 -0
- package/types/use/listCalculated.d.ts +5 -0
- package/types/use/listFilter.d.ts +5 -0
- package/types/use/listInstance.d.ts +7 -2
- package/types/use/listRelated.d.ts +5 -0
- package/types/use/listSearch.d.ts +5 -0
- package/types/use/listSort.d.ts +5 -0
- package/types/use/listSubscription.d.ts +5 -0
- package/types/utils/watches.d.ts +40 -7
- package/use/listCalculated.js +25 -1
- package/use/listFilter.js +136 -24
- package/use/listInstance.js +4 -1
- package/use/listRelated.js +25 -1
- package/use/listSearch.js +100 -12
- package/use/listSort.js +91 -30
- package/use/listSubscription.js +3 -0
- package/utils/watches.js +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arrai-innovations/reactive-helpers",
|
|
3
|
-
"version": "23.0.
|
|
3
|
+
"version": "23.0.2",
|
|
4
4
|
"description": "VueJS 3 utility composition functions to help manipulate objects and lists.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@arrai-innovations/commitlint-config": "^3.0.0",
|
|
46
46
|
"@arrai-innovations/diction-md": "^1.0.0",
|
|
47
|
-
"@arrai-innovations/vitepress-theme": "1.0.
|
|
47
|
+
"@arrai-innovations/vitepress-theme": "^1.0.2",
|
|
48
48
|
"@commitlint/cli": "^21.2.1",
|
|
49
49
|
"@eslint/js": "9.39.4",
|
|
50
50
|
"@godaddy/dmd": "^1.0.4",
|
|
@@ -89,11 +89,16 @@
|
|
|
89
89
|
},
|
|
90
90
|
"scripts": {
|
|
91
91
|
"benchmark": "vitest bench --run",
|
|
92
|
+
"benchmark:ci": "pnpm benchmark:list:ci && pnpm benchmark:stream:ci && pnpm benchmark:layers:ci && pnpm benchmark:observed:ci",
|
|
92
93
|
"benchmark:list": "vitest bench --run tests/benchmarks/listPush.bench.js",
|
|
93
94
|
"benchmark:list:ci": "vitest bench --run tests/benchmarks/listPush.bench.js --outputJson benchmark-results/list-push.json",
|
|
94
95
|
"benchmark:check": "node check_benchmark.js",
|
|
95
96
|
"benchmark:layers": "vitest bench --run tests/benchmarks/listLayers.bench.js",
|
|
97
|
+
"benchmark:layers:ci": "vitest bench --run tests/benchmarks/listLayers.bench.js --outputJson benchmark-results/list-layers.json",
|
|
98
|
+
"benchmark:observed": "vitest bench --run tests/benchmarks/listObserved.bench.js",
|
|
99
|
+
"benchmark:observed:ci": "vitest bench --run tests/benchmarks/listObserved.bench.js --outputJson benchmark-results/list-observed.json",
|
|
96
100
|
"benchmark:stream": "vitest bench --run tests/benchmarks/listStream.bench.js",
|
|
101
|
+
"benchmark:stream:ci": "vitest bench --run tests/benchmarks/listStream.bench.js --outputJson benchmark-results/list-stream.json",
|
|
97
102
|
"test": "vitest",
|
|
98
103
|
"eslint": "eslint --fix index.js config/**/*.js tests/**/*.js use/**/*.js utils/**/*.js",
|
|
99
104
|
"prettier": "prettier --write .",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** How many entries each generated lookup collection holds. */
|
|
2
|
+
export const lookupSize: 13;
|
|
3
|
+
export function makeLookup(index: number): import("../../use/listInstance.js").ObjectsByPk;
|
|
4
|
+
/** Lookup collection for the array-valued rule, which resolves a list of foreign keys per record. */
|
|
5
|
+
export const tagLookup: import("../../use/listInstance.js").ObjectsByPk;
|
|
6
|
+
/** The order the array-valued rule sorts its resolved tags into. */
|
|
7
|
+
export const tagOrder: string[];
|
|
8
|
+
export function makeRelatedRules(ruleCount: number, includeArrayRule?: boolean): import("../../use/listRelated.js").ListRelatedRules;
|
|
9
|
+
export function makeCalculatedRules(ruleCount: number, relatedRuleCount: number): {
|
|
10
|
+
[rule: string]: (object: object, related: object, calculated: object) => any;
|
|
11
|
+
};
|
|
12
|
+
export function makeReviewRows(count: number, start?: number, fkCount?: number): object[];
|
|
13
|
+
export function makeOrderByRules(sortOn: "none" | "plain" | "related" | "calculated"): import("../../use/listSort.js").OrderByRule[];
|
|
14
|
+
export function makeReviewList({ relatedRuleCount, calculatedRuleCount, sortOn, includeArrayRule, filter, sortThrottleWait, }?: ReviewListOptions): ReturnType<typeof useList>;
|
|
15
|
+
export function composeReviewStack({ relatedRuleCount, calculatedRuleCount, sortOn, includeArrayRule, filter, sortThrottleWait, }?: ReviewListOptions): {
|
|
16
|
+
states: {
|
|
17
|
+
[layer: string]: object;
|
|
18
|
+
};
|
|
19
|
+
push: (objects: object[]) => void;
|
|
20
|
+
stop: () => void;
|
|
21
|
+
};
|
|
22
|
+
export namespace readChannels {
|
|
23
|
+
let collection: {
|
|
24
|
+
[channel: string]: (state: object) => void;
|
|
25
|
+
};
|
|
26
|
+
let record: {
|
|
27
|
+
[channel: string]: (state: object, pk: string) => void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function attachRenderObservers(state: object, { collectionChannels, recordChannels }?: RenderObserverOptions): {
|
|
31
|
+
sync: () => void;
|
|
32
|
+
stop: () => void;
|
|
33
|
+
counts: {
|
|
34
|
+
[channel: string]: ChannelCounts;
|
|
35
|
+
};
|
|
36
|
+
totals: () => {
|
|
37
|
+
effects: number;
|
|
38
|
+
runs: number;
|
|
39
|
+
triggers: number;
|
|
40
|
+
};
|
|
41
|
+
reset: () => void;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The shape of the review list to build.
|
|
45
|
+
*/
|
|
46
|
+
export type ReviewListOptions = {
|
|
47
|
+
/**
|
|
48
|
+
* How many related rules the list carries.
|
|
49
|
+
*/
|
|
50
|
+
relatedRuleCount?: number;
|
|
51
|
+
/**
|
|
52
|
+
* How many calculated rules the list carries.
|
|
53
|
+
*/
|
|
54
|
+
calculatedRuleCount?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Which value the sort orders on.
|
|
57
|
+
*/
|
|
58
|
+
sortOn?: "none" | "plain" | "related" | "calculated";
|
|
59
|
+
/**
|
|
60
|
+
* Whether the related rules include an array-valued rule.
|
|
61
|
+
*/
|
|
62
|
+
includeArrayRule?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether an allowed filter is active.
|
|
65
|
+
*/
|
|
66
|
+
filter?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The sort throttle, in milliseconds.
|
|
69
|
+
*/
|
|
70
|
+
sortThrottleWait?: number;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Which channels observe the list.
|
|
74
|
+
*/
|
|
75
|
+
export type RenderObserverOptions = {
|
|
76
|
+
/**
|
|
77
|
+
* Names from `readChannels.collection` to attach once.
|
|
78
|
+
*/
|
|
79
|
+
collectionChannels?: string[];
|
|
80
|
+
/**
|
|
81
|
+
* Names from `readChannels.record` to attach per record.
|
|
82
|
+
*/
|
|
83
|
+
recordChannels?: string[];
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* What one channel's effects have done.
|
|
87
|
+
*/
|
|
88
|
+
export type ChannelCounts = {
|
|
89
|
+
/**
|
|
90
|
+
* How many effects the channel has attached.
|
|
91
|
+
*/
|
|
92
|
+
effects: number;
|
|
93
|
+
/**
|
|
94
|
+
* How many times those effects have evaluated.
|
|
95
|
+
*/
|
|
96
|
+
runs: number;
|
|
97
|
+
/**
|
|
98
|
+
* How many times those effects have been notified.
|
|
99
|
+
*/
|
|
100
|
+
triggers: number;
|
|
101
|
+
};
|
|
102
|
+
import { useList } from "../../use/list.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
* @typedef {object} ListCalculatedProperties - The properties for the list computed composition function.
|
|
65
65
|
* @property {ListCalculatedState} state - The state for the list calculated property.
|
|
66
66
|
* @property {ListCalculatedParentState} parentState - The parent state object.
|
|
67
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
67
68
|
* @property {() => void} stop - Stops composition's effects and cleans up resources.
|
|
68
69
|
*/
|
|
69
70
|
/**
|
|
@@ -221,6 +222,10 @@ export type ListCalculatedProperties = {
|
|
|
221
222
|
* The parent state object.
|
|
222
223
|
*/
|
|
223
224
|
parentState: ListCalculatedParentState;
|
|
225
|
+
/**
|
|
226
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
227
|
+
*/
|
|
228
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
224
229
|
/**
|
|
225
230
|
* Stops composition's effects and cleans up resources.
|
|
226
231
|
*/
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* @typedef {object} ListFilterProperties - The properties of a list filter, including its state and associated Vue composition API utilities.
|
|
52
52
|
* @property {ListFilterState} state - The reactive state managing the filter logic and results.
|
|
53
53
|
* @property {ListFilterParentState} parentState - The state of the list being filtered.
|
|
54
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
54
55
|
* @property {() => void} stop - A function to stop the effect scope and clean up resources.
|
|
55
56
|
*/
|
|
56
57
|
/**
|
|
@@ -166,6 +167,10 @@ export type ListFilterProperties = {
|
|
|
166
167
|
* The state of the list being filtered.
|
|
167
168
|
*/
|
|
168
169
|
parentState: ListFilterParentState;
|
|
170
|
+
/**
|
|
171
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
172
|
+
*/
|
|
173
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
169
174
|
/**
|
|
170
175
|
* A function to stop the effect scope and clean up resources.
|
|
171
176
|
*/
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* @property {object} params - Arguments passed to the server for listing operations.
|
|
55
55
|
* @property {ObjectsMap} objectsMap - The map of objects stored by their pks.
|
|
56
56
|
* @property {ObjectsByPk} objects - The list objects stored by their pks.
|
|
57
|
-
* @property {number} objectsVersion - Increments when
|
|
57
|
+
* @property {number} objectsVersion - Increments when this layer's set of object keys changes. Each layer that narrows membership publishes its own, so the value belongs to the state reporting it and is not comparable with another layer's. Watch it rather than reading it, and prefer `watchMembershipChanged`, which carries the same signal without exposing how it is counted.
|
|
58
58
|
* @property {ListOrder} order - The order of objects in the list.
|
|
59
59
|
* @property {ObjectsInOrder} objectsInOrder - The objects in the order specified by the list.
|
|
60
60
|
* @property {import('vue').ShallowReactive<PaginateInfo>} paginateInfo - Pagination information for the list.
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
* @property {(args: {action: string, pks?: import('../config/commonCrud.js').Pk[]} & import('../config/listCrud.js').AdditionalListArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<object|string|boolean|null>} executeAction - Initiates an action on all objects in the list, returning the response, or null if the action failed. The promise carries a `cancel` method when the handler's promise did.
|
|
99
99
|
* @property {(info: PaginateInfo) => void} setPaginateInfo - The method to update pagination information.
|
|
100
100
|
* @property {(total: ColumnTotals) => void} setColumnTotals - The method to update column totals.
|
|
101
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
101
102
|
*/
|
|
102
103
|
/**
|
|
103
104
|
* @typedef {ListInstanceMyFunctions & Pick<import('./loadingError.js').LoadingErrorStatus, "clearError">} ListInstanceFunctions - The methods contributed by the list instance, including its CRUD operations plus clearError.
|
|
@@ -321,7 +322,7 @@ export type ListInstanceRawMyState = {
|
|
|
321
322
|
*/
|
|
322
323
|
objects: ObjectsByPk;
|
|
323
324
|
/**
|
|
324
|
-
* Increments when
|
|
325
|
+
* Increments when this layer's set of object keys changes. Each layer that narrows membership publishes its own, so the value belongs to the state reporting it and is not comparable with another layer's. Watch it rather than reading it, and prefer `watchMembershipChanged`, which carries the same signal without exposing how it is counted.
|
|
325
326
|
*/
|
|
326
327
|
objectsVersion: number;
|
|
327
328
|
/**
|
|
@@ -436,6 +437,10 @@ export type ListInstanceMyFunctions = {
|
|
|
436
437
|
* The method to update column totals.
|
|
437
438
|
*/
|
|
438
439
|
setColumnTotals: (total: ColumnTotals) => void;
|
|
440
|
+
/**
|
|
441
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
442
|
+
*/
|
|
443
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
439
444
|
};
|
|
440
445
|
/**
|
|
441
446
|
* The methods contributed by the list instance, including its CRUD operations plus clearError.
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
* @typedef {object} ListRelatedProperties - The properties for the list related composition function.
|
|
60
60
|
* @property {ListRelatedState} state - The state for the list related property.
|
|
61
61
|
* @property {ListRelatedParentState} parentState - The parent state object.
|
|
62
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
62
63
|
* @property {() => void} stop - Stops all effects of the list related property.
|
|
63
64
|
*/
|
|
64
65
|
/**
|
|
@@ -289,6 +290,10 @@ export type ListRelatedProperties = {
|
|
|
289
290
|
* The parent state object.
|
|
290
291
|
*/
|
|
291
292
|
parentState: ListRelatedParentState;
|
|
293
|
+
/**
|
|
294
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
295
|
+
*/
|
|
296
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
292
297
|
/**
|
|
293
298
|
* Stops all effects of the list related property.
|
|
294
299
|
*/
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
* @typedef {object} ListSearchProperties - The properties on a list search instance.
|
|
68
68
|
* @property {ListSearchState} state - The state.
|
|
69
69
|
* @property {import('./search.js').SearchInstance} textSearchIndex - The text search index.
|
|
70
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
70
71
|
* @property {() => void} stop - Stops the effect scope and cleans up resources.
|
|
71
72
|
*/
|
|
72
73
|
/**
|
|
@@ -253,6 +254,10 @@ export type ListSearchProperties = {
|
|
|
253
254
|
* The text search index.
|
|
254
255
|
*/
|
|
255
256
|
textSearchIndex: import("./search.js").SearchInstance;
|
|
257
|
+
/**
|
|
258
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
259
|
+
*/
|
|
260
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
256
261
|
/**
|
|
257
262
|
* Stops the effect scope and cleans up resources.
|
|
258
263
|
*/
|
package/types/use/listSort.d.ts
CHANGED
|
@@ -55,6 +55,7 @@ export function setListSortDefaultOptions({ sortThrottleWait }: {
|
|
|
55
55
|
* @typedef {object} ListSortProperties - The properties available on a list sort instance.
|
|
56
56
|
* @property {ListSortState} state - The reactive state for the list sort.
|
|
57
57
|
* @property {ListSortParentState} parentState - The parent state.
|
|
58
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
58
59
|
* @property {() => void} stop - A function to stop the effect scope and clean up resources.
|
|
59
60
|
*/
|
|
60
61
|
/**
|
|
@@ -183,6 +184,10 @@ export type ListSortProperties = {
|
|
|
183
184
|
* The parent state.
|
|
184
185
|
*/
|
|
185
186
|
parentState: ListSortParentState;
|
|
187
|
+
/**
|
|
188
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
189
|
+
*/
|
|
190
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
186
191
|
/**
|
|
187
192
|
* A function to stop the effect scope and clean up resources.
|
|
188
193
|
*/
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* @property {import('./listInstance.js').ListInstance} listInstance - The list instance used by the subscription.
|
|
34
34
|
* @property {import('./cancellableIntent.js').CancellableIntent} listIntent - The `CancellableIntent` instance managing if the list should be (re)fetched.
|
|
35
35
|
* @property {import('./cancellableIntent.js').CancellableIntent} subscribeIntent - The `CancellableIntent` instance managing if the subscription should be (un)subscribed.
|
|
36
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
36
37
|
*/
|
|
37
38
|
/**
|
|
38
39
|
* @typedef {ListSubscriptionFunctions & ListSubscriptionProperties} ListSubscription - An instance of a list subscription, returned by `useListSubscription`.
|
|
@@ -184,6 +185,10 @@ export type ListSubscriptionProperties = {
|
|
|
184
185
|
* The `CancellableIntent` instance managing if the subscription should be (un)subscribed.
|
|
185
186
|
*/
|
|
186
187
|
subscribeIntent: import("./cancellableIntent.js").CancellableIntent;
|
|
188
|
+
/**
|
|
189
|
+
* Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
190
|
+
*/
|
|
191
|
+
watchMembershipChanged: import("../utils/watches.js").WatchMembershipChanged;
|
|
187
192
|
};
|
|
188
193
|
/**
|
|
189
194
|
* An instance of a list subscription, returned by `useListSubscription`.
|
package/types/utils/watches.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils/watches.js
|
|
3
|
+
* @description
|
|
4
|
+
*
|
|
5
|
+
* A collection of utility classes and functions for managing Vue.js watchers.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* @callback WatchMembershipChanged - Calls back when a list layer's set of object keys changes.
|
|
10
|
+
* Registers in the effect scope active where it is called, not in the layer's own scope, so the
|
|
11
|
+
* returned handle and the surrounding scope own the watcher. Stopping the layer does not dispose it,
|
|
12
|
+
* because the layer never owned it; it only silences it, since a stopped layer publishes nothing
|
|
13
|
+
* further. Stop it through the returned handle or through the enclosing scope.
|
|
14
|
+
* The callback takes no arguments: it reports that membership moved, not what it moved to. Read the
|
|
15
|
+
* layer's collection views for that.
|
|
16
|
+
* @param {() => void} callback - Called after the layer's set of object keys changes.
|
|
17
|
+
* @param {import('vue').WatchOptions} [options] - Passed through to Vue's `watch`, so `immediate`,
|
|
18
|
+
* `flush`, and `once` all behave as they do there.
|
|
19
|
+
* @returns {import('vue').WatchHandle} - Stops the watcher.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Builds a layer's membership watcher over the counter that layer publishes.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
* @param {{objectsVersion: number}} state - The layer's own state.
|
|
26
|
+
* @returns {WatchMembershipChanged} - The watcher registration function for that layer.
|
|
27
|
+
*/
|
|
28
|
+
export function makeMembershipWatcher(state: {
|
|
29
|
+
objectsVersion: number;
|
|
30
|
+
}): WatchMembershipChanged;
|
|
1
31
|
/**
|
|
2
32
|
* Helper function to get the resulting promise from an AwaitTimeout instance.
|
|
3
33
|
*
|
|
@@ -23,13 +53,6 @@ export function doAwaitNot({ obj, prop, ref, couldAlreadyBeFalse, timeout }: {
|
|
|
23
53
|
couldAlreadyBeFalse?: boolean;
|
|
24
54
|
timeout?: number;
|
|
25
55
|
}): Promise<any>;
|
|
26
|
-
/**
|
|
27
|
-
* @module utils/watches.js
|
|
28
|
-
* @description
|
|
29
|
-
*
|
|
30
|
-
* A collection of utility classes and functions for managing Vue.js watchers.
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
56
|
/**
|
|
34
57
|
* Provides a mechanism for immediately starting and potentially stopping a Vue.js watcher
|
|
35
58
|
* during its first invocation. This is useful when the need arises to terminate the watch
|
|
@@ -152,3 +175,13 @@ export class AwaitNot {
|
|
|
152
175
|
*/
|
|
153
176
|
stop(): void;
|
|
154
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Calls back when a list layer's set of object keys changes.
|
|
180
|
+
* Registers in the effect scope active where it is called, not in the layer's own scope, so the
|
|
181
|
+
* returned handle and the surrounding scope own the watcher. Stopping the layer does not dispose it,
|
|
182
|
+
* because the layer never owned it; it only silences it, since a stopped layer publishes nothing
|
|
183
|
+
* further. Stop it through the returned handle or through the enclosing scope.
|
|
184
|
+
* The callback takes no arguments: it reports that membership moved, not what it moved to. Read the
|
|
185
|
+
* layer's collection views for that.
|
|
186
|
+
*/
|
|
187
|
+
export type WatchMembershipChanged = (callback: () => void, options?: import("vue").WatchOptions) => import("vue").WatchHandle;
|
package/use/listCalculated.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
2
3
|
import { loadingCombine } from "../utils/loadingCombine.js";
|
|
3
4
|
import { proxyRunning } from "../utils/proxyRunning.js";
|
|
4
5
|
import isEmpty from "lodash-es/isEmpty.js";
|
|
@@ -78,6 +79,7 @@ import { warnWrongSideRuleOptions } from "../utils/relatedCalculatedHelpers.js";
|
|
|
78
79
|
* @typedef {object} ListCalculatedProperties - The properties for the list computed composition function.
|
|
79
80
|
* @property {ListCalculatedState} state - The state for the list calculated property.
|
|
80
81
|
* @property {ListCalculatedParentState} parentState - The parent state object.
|
|
82
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
81
83
|
* @property {() => void} stop - Stops composition's effects and cleans up resources.
|
|
82
84
|
*/
|
|
83
85
|
|
|
@@ -181,6 +183,14 @@ export function useListCalculated(options) {
|
|
|
181
183
|
running: computed(() => loadingCombine(state.calculatedRunning, parentRunning.value)),
|
|
182
184
|
});
|
|
183
185
|
const calculatedObjectsEffectScopes = {};
|
|
186
|
+
// Records whose rule bag has not been reconciled against the rules yet. A record arriving with an
|
|
187
|
+
// empty bag needs every rule applied. A record already carrying the full rule set needs nothing,
|
|
188
|
+
// and rediscovering that by diffing its bag cost one keyDiff per record in the collection, per
|
|
189
|
+
// page, along with a pair of refs and an effect scope activation for each.
|
|
190
|
+
const pendingObjectKeys = new Set();
|
|
191
|
+
// The rule keys the last reconciliation ran against, so a run can tell which of its two causes woke
|
|
192
|
+
// it. Only a rule change touches records that were already reconciled.
|
|
193
|
+
let previousRuleKeys = [];
|
|
184
194
|
|
|
185
195
|
function parentStateObjectsWatch() {
|
|
186
196
|
const { addedKeys, removedKeys } = keyDiff(
|
|
@@ -188,6 +198,7 @@ export function useListCalculated(options) {
|
|
|
188
198
|
Object.keys(state.calculatedObjects)
|
|
189
199
|
);
|
|
190
200
|
for (const removedKey of removedKeys) {
|
|
201
|
+
pendingObjectKeys.delete(removedKey);
|
|
191
202
|
delete state.calculatedObjects[removedKey];
|
|
192
203
|
if (calculatedObjectsEffectScopes[removedKey]) {
|
|
193
204
|
calculatedObjectsEffectScopes[removedKey].objectScope.stop();
|
|
@@ -196,6 +207,7 @@ export function useListCalculated(options) {
|
|
|
196
207
|
}
|
|
197
208
|
for (const addedKey of addedKeys) {
|
|
198
209
|
state.calculatedObjects[addedKey] = {};
|
|
210
|
+
pendingObjectKeys.add(addedKey);
|
|
199
211
|
}
|
|
200
212
|
nextTick(() => {
|
|
201
213
|
state.calculatedObjectsParentStateObjectsWatchRunning = false;
|
|
@@ -204,7 +216,18 @@ export function useListCalculated(options) {
|
|
|
204
216
|
|
|
205
217
|
function calculatedObjectsWatch() {
|
|
206
218
|
const calculatedObjectsRulesIsEmpty = !state.calculatedObjectsRules || isEmpty(state.calculatedObjectsRules);
|
|
207
|
-
|
|
219
|
+
const ruleKeys = Object.keys(state.calculatedObjectsRules || {});
|
|
220
|
+
const { addedKeys: addedRules, removedKeys: removedRules } = keyDiff(ruleKeys, previousRuleKeys, {
|
|
221
|
+
sameKeys: false,
|
|
222
|
+
});
|
|
223
|
+
previousRuleKeys = ruleKeys;
|
|
224
|
+
// A rule change alters the bag of every record, so it is the only cause that has to walk the
|
|
225
|
+
// collection. A page arriving leaves every record already present holding the rule set it
|
|
226
|
+
// already held, so only the arrivals have anything to reconcile.
|
|
227
|
+
const rulesChanged = addedRules.size > 0 || removedRules.size > 0;
|
|
228
|
+
const objectKeys = rulesChanged ? Object.keys(state.calculatedObjects) : [...pendingObjectKeys];
|
|
229
|
+
pendingObjectKeys.clear();
|
|
230
|
+
for (const objectKey of objectKeys) {
|
|
208
231
|
if (!state.calculatedObjects[objectKey]) {
|
|
209
232
|
state.calculatedObjects[objectKey] = {};
|
|
210
233
|
}
|
|
@@ -292,6 +315,7 @@ export function useListCalculated(options) {
|
|
|
292
315
|
return {
|
|
293
316
|
state,
|
|
294
317
|
parentState,
|
|
318
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
295
319
|
stop: () => {
|
|
296
320
|
es.stop();
|
|
297
321
|
for (const key of Object.keys(calculatedObjectsEffectScopes)) {
|
package/use/listFilter.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
-
import {
|
|
2
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
3
|
+
import {
|
|
4
|
+
computed,
|
|
5
|
+
effectScope,
|
|
6
|
+
isRef,
|
|
7
|
+
reactive,
|
|
8
|
+
ref,
|
|
9
|
+
shallowReactive,
|
|
10
|
+
shallowReadonly,
|
|
11
|
+
toRef,
|
|
12
|
+
toRefs,
|
|
13
|
+
unref,
|
|
14
|
+
watch,
|
|
15
|
+
} from "vue";
|
|
3
16
|
|
|
4
17
|
/**
|
|
5
18
|
* Provides reactive filtering functionality for lists within a Vue application. This composable
|
|
@@ -63,6 +76,7 @@ import { computed, effectScope, isRef, reactive, shallowReadonly, toRef, toRefs,
|
|
|
63
76
|
* @typedef {object} ListFilterProperties - The properties of a list filter, including its state and associated Vue composition API utilities.
|
|
64
77
|
* @property {ListFilterState} state - The reactive state managing the filter logic and results.
|
|
65
78
|
* @property {ListFilterParentState} parentState - The state of the list being filtered.
|
|
79
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
66
80
|
* @property {() => void} stop - A function to stop the effect scope and clean up resources.
|
|
67
81
|
*/
|
|
68
82
|
|
|
@@ -125,10 +139,12 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
125
139
|
const getAllowed = () => (isRef(allowedFilter) ? allowedFilter.value : allowedFilter);
|
|
126
140
|
const getExcluded = () => (isRef(excludedFilter) ? excludedFilter.value : excludedFilter);
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
// Track entry changes without proxying entries or unwrapping their computed refs.
|
|
143
|
+
/** @type {Map<import('../config/commonCrud.js').Pk, {scope: import('vue').EffectScope, include: import('vue').ComputedRef<boolean>}>} */
|
|
144
|
+
const includeMap = shallowReactive(new Map());
|
|
129
145
|
|
|
130
146
|
function ensureIncludeComputed(pk) {
|
|
131
|
-
if (!includeMap
|
|
147
|
+
if (!includeMap.get(pk)) {
|
|
132
148
|
const child = es.run(() => effectScope());
|
|
133
149
|
const include = child.run(() => {
|
|
134
150
|
const objRef = toRef(parentState.objects, pk);
|
|
@@ -155,52 +171,148 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
155
171
|
});
|
|
156
172
|
});
|
|
157
173
|
|
|
158
|
-
includeMap
|
|
174
|
+
includeMap.set(pk, { scope: child, include });
|
|
159
175
|
}
|
|
160
|
-
return includeMap
|
|
176
|
+
return includeMap.get(pk).include;
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
function disposeIncludeComputed(pk) {
|
|
164
|
-
const entry = includeMap
|
|
180
|
+
const entry = includeMap.get(pk);
|
|
165
181
|
if (entry) {
|
|
166
182
|
entry.scope.stop();
|
|
167
|
-
delete
|
|
183
|
+
includeMap.delete(pk);
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
|
|
187
|
+
const isIncluded = (/** @type {import('../config/commonCrud.js').Pk} */ pk) => {
|
|
188
|
+
const entry = includeMap.get(pk);
|
|
189
|
+
return entry ? entry.include.value : false;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Cache enumeration separately from per-key reads.
|
|
193
|
+
/** @type {import('vue').ComputedRef<import('./listInstance.js').ObjectsByPk>} */
|
|
194
|
+
const includedObjects = computed(() => {
|
|
195
|
+
/** @type {{[pk: import('../config/commonCrud.js').Pk]: import('./objectInstance.js').ExistingCrudObject}} */
|
|
196
|
+
const out = {};
|
|
197
|
+
for (const [pk, o] of Object.entries(parentState.objects)) {
|
|
198
|
+
if (isIncluded(pk)) {
|
|
199
|
+
out[pk] = o;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return out;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// This layer's membership is its own: a rule change or a record edit moves it without the parent's
|
|
206
|
+
// key set moving, so the parent's objectsVersion does not describe this collection. Own one here
|
|
207
|
+
// and keep the property's contract true for every layer downstream, which is what lets them watch
|
|
208
|
+
// it instead of enumerating this layer once each.
|
|
209
|
+
const objectsVersion = ref(0);
|
|
210
|
+
/** @type {Set<import('../config/commonCrud.js').Pk>} */
|
|
211
|
+
const includedKeys = new Set();
|
|
212
|
+
|
|
171
213
|
es.run(() => {
|
|
172
214
|
watch(
|
|
173
215
|
() => parentState.objectsVersion,
|
|
174
216
|
() => {
|
|
175
217
|
const newVal = Object.keys(parentState.objects);
|
|
176
|
-
const { addedKeys, removedKeys } = keyDiff(newVal,
|
|
218
|
+
const { addedKeys, removedKeys } = keyDiff(newVal, [...includeMap.keys()]);
|
|
219
|
+
// Only an arriving or departing record can move membership structurally, so the version
|
|
220
|
+
// is settled from those keys rather than from a fresh pass over the collection. This
|
|
221
|
+
// runs synchronously, and a full pass here would scale the synchronous half of a page
|
|
222
|
+
// with the collection behind it.
|
|
223
|
+
let moved = false;
|
|
177
224
|
for (const pk of removedKeys) {
|
|
178
225
|
disposeIncludeComputed(pk);
|
|
226
|
+
moved = includedKeys.delete(pk) || moved;
|
|
179
227
|
}
|
|
180
228
|
for (const pk of addedKeys) {
|
|
181
229
|
ensureIncludeComputed(pk);
|
|
230
|
+
if (isIncluded(pk)) {
|
|
231
|
+
includedKeys.add(pk);
|
|
232
|
+
moved = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (moved) {
|
|
236
|
+
objectsVersion.value++;
|
|
182
237
|
}
|
|
183
238
|
},
|
|
184
239
|
{ immediate: true, flush: "sync" }
|
|
185
240
|
);
|
|
241
|
+
|
|
242
|
+
// A rule change or a record edit flips inclusion without moving any key set, so neither reaches
|
|
243
|
+
// the watcher above. Deferred on purpose: includedObjects invalidates once per record written,
|
|
244
|
+
// and a synchronous watcher here would rebuild the whole map once per record in a page.
|
|
245
|
+
watch(includedObjects, (included) => {
|
|
246
|
+
let moved = false;
|
|
247
|
+
for (const pk of Object.keys(included)) {
|
|
248
|
+
if (!includedKeys.has(pk)) {
|
|
249
|
+
includedKeys.add(pk);
|
|
250
|
+
moved = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (includedKeys.size !== Object.keys(included).length) {
|
|
254
|
+
for (const pk of [...includedKeys]) {
|
|
255
|
+
if (!(pk in included)) {
|
|
256
|
+
includedKeys.delete(pk);
|
|
257
|
+
moved = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (moved) {
|
|
262
|
+
objectsVersion.value++;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
186
265
|
});
|
|
187
266
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
267
|
+
// Resolve individual keys without tracking the full collection.
|
|
268
|
+
// Implement readonly behaviour here to avoid an extra proxy layer.
|
|
269
|
+
/** @type {import('./listInstance.js').ObjectsByPk} */
|
|
270
|
+
const objects = new Proxy(/** @type {any} */ ({}), {
|
|
271
|
+
get(target, prop) {
|
|
272
|
+
if (typeof prop === "symbol") {
|
|
273
|
+
return Reflect.get(target, prop);
|
|
274
|
+
}
|
|
275
|
+
if (prop === "__v_isReadonly") {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
return isIncluded(prop) ? parentState.objects[prop] : undefined;
|
|
279
|
+
},
|
|
280
|
+
set(target, prop) {
|
|
281
|
+
console.warn(`useListFilter: set operation on key "${String(prop)}" failed: objects is read-only.`);
|
|
282
|
+
return true;
|
|
283
|
+
},
|
|
284
|
+
deleteProperty(target, prop) {
|
|
285
|
+
console.warn(`useListFilter: delete operation on key "${String(prop)}" failed: objects is read-only.`);
|
|
286
|
+
return true;
|
|
287
|
+
},
|
|
288
|
+
has(target, prop) {
|
|
289
|
+
if (typeof prop === "symbol") {
|
|
290
|
+
return Reflect.has(target, prop);
|
|
291
|
+
}
|
|
292
|
+
return isIncluded(prop) && prop in parentState.objects;
|
|
293
|
+
},
|
|
294
|
+
ownKeys() {
|
|
295
|
+
return Reflect.ownKeys(includedObjects.value);
|
|
296
|
+
},
|
|
297
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
298
|
+
if (typeof prop === "symbol") {
|
|
299
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
300
|
+
}
|
|
301
|
+
// Use the cached collection because descriptor lookups occur during enumeration.
|
|
302
|
+
const object = includedObjects.value[prop];
|
|
303
|
+
if (object === undefined) {
|
|
304
|
+
// Keep descriptors consistent with ownKeys().
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return { configurable: true, enumerable: true, value: object, writable: true };
|
|
308
|
+
},
|
|
309
|
+
getPrototypeOf() {
|
|
310
|
+
return Object.prototype;
|
|
311
|
+
},
|
|
200
312
|
});
|
|
201
313
|
|
|
202
314
|
/** @type {import('./listInstance.js').ListOrder} */
|
|
203
|
-
const order = computed(() => shallowReadonly(parentState.order.filter(
|
|
315
|
+
const order = computed(() => shallowReadonly(parentState.order.filter(isIncluded)));
|
|
204
316
|
|
|
205
317
|
/** @type {import('./listInstance.js').ObjectsInOrder} */
|
|
206
318
|
const objectsInOrder = computed(() => shallowReadonly(order.value.map((pk) => parentState.objects[pk])));
|
|
@@ -211,6 +323,7 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
211
323
|
allowedFilter,
|
|
212
324
|
excludedFilter,
|
|
213
325
|
|
|
326
|
+
objectsVersion,
|
|
214
327
|
objects,
|
|
215
328
|
order,
|
|
216
329
|
objectsInOrder,
|
|
@@ -223,11 +336,10 @@ export function useListFilter({ parentState, allowedFilter, excludedFilter }) {
|
|
|
223
336
|
return {
|
|
224
337
|
state,
|
|
225
338
|
parentState,
|
|
339
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
226
340
|
stop: () => {
|
|
227
341
|
es.stop();
|
|
228
|
-
|
|
229
|
-
delete includeMap[pk];
|
|
230
|
-
}
|
|
342
|
+
includeMap.clear();
|
|
231
343
|
},
|
|
232
344
|
};
|
|
233
345
|
}
|
package/use/listInstance.js
CHANGED
|
@@ -2,6 +2,7 @@ import { defaultListCrud, getListCrud } from "../config/listCrud.js";
|
|
|
2
2
|
import { assignReactiveObject } from "../utils/assignReactiveObject.js";
|
|
3
3
|
import { getFakePk } from "../utils/getFakePk.js";
|
|
4
4
|
import { normalizePk } from "../utils/refIfReactive.js";
|
|
5
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
5
6
|
import { useLoadingError } from "./loadingError.js";
|
|
6
7
|
import inspect from "browser-util-inspect";
|
|
7
8
|
import { computed, effectScope, isReactive, reactive, readonly, ref, shallowReactive, shallowReadonly } from "vue";
|
|
@@ -97,7 +98,7 @@ export class ListInstanceError extends Error {
|
|
|
97
98
|
* @property {object} params - Arguments passed to the server for listing operations.
|
|
98
99
|
* @property {ObjectsMap} objectsMap - The map of objects stored by their pks.
|
|
99
100
|
* @property {ObjectsByPk} objects - The list objects stored by their pks.
|
|
100
|
-
* @property {number} objectsVersion - Increments when
|
|
101
|
+
* @property {number} objectsVersion - Increments when this layer's set of object keys changes. Each layer that narrows membership publishes its own, so the value belongs to the state reporting it and is not comparable with another layer's. Watch it rather than reading it, and prefer `watchMembershipChanged`, which carries the same signal without exposing how it is counted.
|
|
101
102
|
* @property {ListOrder} order - The order of objects in the list.
|
|
102
103
|
* @property {ObjectsInOrder} objectsInOrder - The objects in the order specified by the list.
|
|
103
104
|
* @property {import('vue').ShallowReactive<PaginateInfo>} paginateInfo - Pagination information for the list.
|
|
@@ -149,6 +150,7 @@ export class ListInstanceError extends Error {
|
|
|
149
150
|
* @property {(args: {action: string, pks?: import('../config/commonCrud.js').Pk[]} & import('../config/listCrud.js').AdditionalListArgs) => import('../utils/cancellablePromise.js').MaybeCancellablePromise<object|string|boolean|null>} executeAction - Initiates an action on all objects in the list, returning the response, or null if the action failed. The promise carries a `cancel` method when the handler's promise did.
|
|
150
151
|
* @property {(info: PaginateInfo) => void} setPaginateInfo - The method to update pagination information.
|
|
151
152
|
* @property {(total: ColumnTotals) => void} setColumnTotals - The method to update column totals.
|
|
153
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
152
154
|
*/
|
|
153
155
|
|
|
154
156
|
/**
|
|
@@ -449,6 +451,7 @@ export function useListInstance({ props, handlers = {} }) {
|
|
|
449
451
|
/** @type {ListInstance} */
|
|
450
452
|
const self = {
|
|
451
453
|
state,
|
|
454
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
452
455
|
setPaginateInfo: (info) => {
|
|
453
456
|
assignReactiveObject(state.paginateInfo, info || {});
|
|
454
457
|
},
|
package/use/listRelated.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
2
3
|
import { loadingCombine } from "../utils/loadingCombine.js";
|
|
3
4
|
import { normalizePk } from "../utils/refIfReactive.js";
|
|
4
5
|
import { proxyRunning } from "../utils/proxyRunning.js";
|
|
@@ -109,6 +110,7 @@ export class ListRelatedError extends Error {
|
|
|
109
110
|
* @typedef {object} ListRelatedProperties - The properties for the list related composition function.
|
|
110
111
|
* @property {ListRelatedState} state - The state for the list related property.
|
|
111
112
|
* @property {ListRelatedParentState} parentState - The parent state object.
|
|
113
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
112
114
|
* @property {() => void} stop - Stops all effects of the list related property.
|
|
113
115
|
*/
|
|
114
116
|
|
|
@@ -238,6 +240,14 @@ export function useListRelated(options) {
|
|
|
238
240
|
}
|
|
239
241
|
);
|
|
240
242
|
const relatedObjectsEffectScopes = {};
|
|
243
|
+
// Records whose rule bag has not been reconciled against the rules yet. A record arriving with an
|
|
244
|
+
// empty bag needs every rule applied. A record already carrying the full rule set needs nothing,
|
|
245
|
+
// and rediscovering that by diffing its bag cost one keyDiff per record in the collection, per
|
|
246
|
+
// page, which is what made an identical page cost more the further into a stream it arrived.
|
|
247
|
+
const pendingObjectKeys = new Set();
|
|
248
|
+
// The rule keys the last reconciliation ran against, so a run can tell which of its two causes woke
|
|
249
|
+
// it. Only a rule change touches records that were already reconciled.
|
|
250
|
+
let previousRuleKeys = [];
|
|
241
251
|
|
|
242
252
|
function parentStateObjectsWatch() {
|
|
243
253
|
const { addedKeys: addedIds, removedKeys: removedIds } = keyDiff(
|
|
@@ -245,6 +255,7 @@ export function useListRelated(options) {
|
|
|
245
255
|
Object.keys(state.relatedObjects)
|
|
246
256
|
);
|
|
247
257
|
for (const removedId of removedIds) {
|
|
258
|
+
pendingObjectKeys.delete(removedId);
|
|
248
259
|
delete state.relatedObjects[removedId];
|
|
249
260
|
delete state.objAndKeyForPkAndRule[removedId];
|
|
250
261
|
delete state.fkForPkAndRule[removedId];
|
|
@@ -257,6 +268,7 @@ export function useListRelated(options) {
|
|
|
257
268
|
state.relatedObjects[addedId] = {};
|
|
258
269
|
state.objAndKeyForPkAndRule[addedId] = {};
|
|
259
270
|
state.fkForPkAndRule[addedId] = {};
|
|
271
|
+
pendingObjectKeys.add(addedId);
|
|
260
272
|
}
|
|
261
273
|
nextTick(() => {
|
|
262
274
|
state.relatedObjectsParentStateObjectsWatchRunning = false;
|
|
@@ -326,7 +338,18 @@ export function useListRelated(options) {
|
|
|
326
338
|
|
|
327
339
|
function relatedObjectsWatch() {
|
|
328
340
|
const relatedObjectsRulesIsEmpty = !state.relatedObjectsRules || isEmpty(state.relatedObjectsRules);
|
|
329
|
-
|
|
341
|
+
const ruleKeys = Object.keys(state.relatedObjectsRules || {});
|
|
342
|
+
const { addedKeys: addedRules, removedKeys: removedRules } = keyDiff(ruleKeys, previousRuleKeys, {
|
|
343
|
+
sameKeys: false,
|
|
344
|
+
});
|
|
345
|
+
previousRuleKeys = ruleKeys;
|
|
346
|
+
// A rule change alters the bag of every record, so it is the only cause that has to walk the
|
|
347
|
+
// collection. A page arriving leaves every record already present holding the rule set it
|
|
348
|
+
// already held, so only the arrivals have anything to reconcile.
|
|
349
|
+
const rulesChanged = addedRules.size > 0 || removedRules.size > 0;
|
|
350
|
+
const objectKeys = rulesChanged ? Object.keys(state.relatedObjects) : [...pendingObjectKeys];
|
|
351
|
+
pendingObjectKeys.clear();
|
|
352
|
+
for (const objectKey of objectKeys) {
|
|
330
353
|
let removedRuleKeys, addedRuleKeys;
|
|
331
354
|
if (!relatedObjectsRulesIsEmpty) {
|
|
332
355
|
({ removedKeys: removedRuleKeys, addedKeys: addedRuleKeys } = keyDiff(
|
|
@@ -401,6 +424,7 @@ export function useListRelated(options) {
|
|
|
401
424
|
return {
|
|
402
425
|
state,
|
|
403
426
|
parentState,
|
|
427
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
404
428
|
stop: () => {
|
|
405
429
|
es.stop();
|
|
406
430
|
for (const objectKey of Object.keys(relatedObjectsEffectScopes)) {
|
package/use/listSearch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
2
3
|
import { proxyRunning } from "../utils/proxyRunning.js";
|
|
3
4
|
import { getObjectRelatedCalculatedByKey } from "../utils/relatedCalculatedHelpers.js";
|
|
4
5
|
import { useSearch } from "./search.js";
|
|
@@ -87,6 +88,7 @@ import { refIfReactive } from "../utils/refIfReactive.js";
|
|
|
87
88
|
* @typedef {object} ListSearchProperties - The properties on a list search instance.
|
|
88
89
|
* @property {ListSearchState} state - The state.
|
|
89
90
|
* @property {import('./search.js').SearchInstance} textSearchIndex - The text search index.
|
|
91
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
90
92
|
* @property {() => void} stop - Stops the effect scope and cleans up resources.
|
|
91
93
|
*/
|
|
92
94
|
|
|
@@ -186,16 +188,41 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
186
188
|
const _objects = reactive({});
|
|
187
189
|
/** @type {import('vue').Ref<import('../config/commonCrud.js').Pk[]>} */
|
|
188
190
|
const _order = ref([]);
|
|
191
|
+
const textSearchRules = refIfReactive(props, "textSearchRules", []);
|
|
192
|
+
const textSearchValue = refIfReactive(props, "textSearchValue", "");
|
|
193
|
+
// With no rules or no query the layer selects nothing, so it hands the parent's views on unchanged
|
|
194
|
+
// rather than copying the collection into `_objects` once per page.
|
|
195
|
+
const passthrough = computed(() => !textSearchRules.value?.length || !textSearchValue.value?.length);
|
|
196
|
+
// Constant collection for a pass-through that shows nothing, read-only like every other view here.
|
|
197
|
+
const noObjects = shallowReadonly({});
|
|
198
|
+
// This layer's membership is its own: a query or rule change moves it without the parent's key set
|
|
199
|
+
// moving, so the parent's objectsVersion does not describe this collection. Own one here and keep
|
|
200
|
+
// the property's contract true for the sort layer downstream, which is what lets it watch the
|
|
201
|
+
// version instead of enumerating this layer. Synced by syncObjectsVersion below.
|
|
202
|
+
const objectsVersion = ref(0);
|
|
203
|
+
const searchedObjects = shallowReadonly(_objects);
|
|
189
204
|
/** @type {ListSearchState} */
|
|
190
205
|
// @ts-ignore
|
|
191
206
|
const state = reactive(
|
|
192
207
|
/** @type {ListSearchRawState} */ {
|
|
193
208
|
.../** @type {ListSearchParentStateToRefs} */ toRefs(parentState),
|
|
194
|
-
|
|
195
|
-
|
|
209
|
+
objectsVersion,
|
|
210
|
+
objects: computed(() => {
|
|
211
|
+
if (!passthrough.value) {
|
|
212
|
+
return searchedObjects;
|
|
213
|
+
}
|
|
214
|
+
return showAllWhenEmpty ? parentState.objects : noObjects;
|
|
215
|
+
}),
|
|
216
|
+
objectsInOrder: computed(() => {
|
|
217
|
+
if (passthrough.value) {
|
|
218
|
+
// no private collection to hold refs into, so resolve against the parent per key
|
|
219
|
+
return shallowReadonly(_order.value.map((pk) => parentState.objects[pk]));
|
|
220
|
+
}
|
|
221
|
+
return shallowReadonly(internalState.objectsInOrderRefs.map((ref) => unref(ref)));
|
|
222
|
+
}),
|
|
196
223
|
order: computed(() => shallowReadonly(_order.value)),
|
|
197
|
-
textSearchRules
|
|
198
|
-
textSearchValue
|
|
224
|
+
textSearchRules,
|
|
225
|
+
textSearchValue,
|
|
199
226
|
objectIndexes: {},
|
|
200
227
|
customDocumentOptions: refIfReactive(props, "customDocumentOptions", {}),
|
|
201
228
|
customSearchOptions: refIfReactive(props, "customSearchOptions", {}),
|
|
@@ -237,6 +264,38 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
237
264
|
// @ts-ignore
|
|
238
265
|
textSearchIndex.state.search = toRef(state, "textSearchValue");
|
|
239
266
|
|
|
267
|
+
/** @type {import('../config/commonCrud.js').Pk[]} */
|
|
268
|
+
let previousSearchedKeys = [];
|
|
269
|
+
/** @type {boolean} */
|
|
270
|
+
let previousPassthrough;
|
|
271
|
+
/** @type {number} */
|
|
272
|
+
let previousParentVersion;
|
|
273
|
+
const syncObjectsVersion = () => {
|
|
274
|
+
const parentVersion = parentState.objectsVersion;
|
|
275
|
+
const flipped = previousPassthrough !== passthrough.value;
|
|
276
|
+
if (passthrough.value) {
|
|
277
|
+
// The private collection is released while passing through, so the key set is either the
|
|
278
|
+
// parent's or empty. Neither needs enumerating: a shown pass-through moves exactly when
|
|
279
|
+
// the parent moves, and a hidden one never moves at all.
|
|
280
|
+
const moved = flipped || (showAllWhenEmpty && previousParentVersion !== parentVersion);
|
|
281
|
+
previousSearchedKeys = [];
|
|
282
|
+
previousPassthrough = true;
|
|
283
|
+
previousParentVersion = parentVersion;
|
|
284
|
+
if (moved) {
|
|
285
|
+
objectsVersion.value++;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const searchedKeys = Object.keys(_objects);
|
|
290
|
+
const { addedKeys, removedKeys } = keyDiff(searchedKeys, previousSearchedKeys, { sameKeys: false });
|
|
291
|
+
previousSearchedKeys = searchedKeys;
|
|
292
|
+
previousPassthrough = false;
|
|
293
|
+
previousParentVersion = parentVersion;
|
|
294
|
+
if (flipped || addedKeys.size || removedKeys.size) {
|
|
295
|
+
objectsVersion.value++;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
240
299
|
const objectEffectScopes = {};
|
|
241
300
|
const objectComputeds = {};
|
|
242
301
|
|
|
@@ -244,8 +303,9 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
244
303
|
const previousObjectIndexes = {};
|
|
245
304
|
|
|
246
305
|
const doPassthrough = (cleanComputed = false) => {
|
|
247
|
-
//
|
|
248
|
-
|
|
306
|
+
// the state resolves against the parent while passing through, so the private collection is
|
|
307
|
+
// released rather than rebuilt.
|
|
308
|
+
assignReactiveObject(_objects, {});
|
|
249
309
|
if (!cleanComputed) {
|
|
250
310
|
return;
|
|
251
311
|
}
|
|
@@ -361,7 +421,7 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
361
421
|
};
|
|
362
422
|
|
|
363
423
|
const updateObjectsForResults = () => {
|
|
364
|
-
if (
|
|
424
|
+
if (passthrough.value) {
|
|
365
425
|
doPassthrough();
|
|
366
426
|
return;
|
|
367
427
|
}
|
|
@@ -373,9 +433,21 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
373
433
|
.map(([pk]) => [pk, toRef(parentState.objects, pk)])
|
|
374
434
|
)
|
|
375
435
|
);
|
|
436
|
+
// Published once the writes are done. Watching the collection instead would re-enumerate it on
|
|
437
|
+
// every key assignReactiveObject writes.
|
|
438
|
+
syncObjectsVersion();
|
|
376
439
|
};
|
|
377
440
|
|
|
378
441
|
const updateOrder = () => {
|
|
442
|
+
if (passthrough.value) {
|
|
443
|
+
// Every parent key is a member, so the order needs no filtering against a private
|
|
444
|
+
// collection and objectsInOrder needs no refs into one. `_order` is still written here
|
|
445
|
+
// rather than read from the parent live: the write is what coalesces a page's worth of
|
|
446
|
+
// parent order changes into one notification for everything downstream.
|
|
447
|
+
_order.value = showAllWhenEmpty ? [...parentState.order] : [];
|
|
448
|
+
assignReactiveObject(internalState.objectsInOrderRefs, []);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
379
451
|
_order.value = parentState.order.filter((pk) => !!_objects[pk]);
|
|
380
452
|
assignReactiveObject(
|
|
381
453
|
internalState.objectsInOrderRefs,
|
|
@@ -396,10 +468,20 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
396
468
|
};
|
|
397
469
|
|
|
398
470
|
es.run(() => {
|
|
399
|
-
|
|
471
|
+
// The parent now owns a version that describes its own key set, so this watches the version
|
|
472
|
+
// rather than enumerating the parent's collection on every invalidation of anything it tracks.
|
|
473
|
+
watch([() => parentState.objectsVersion, toRef(state.textSearchRules)], makeComputeds, {
|
|
400
474
|
immediate: true,
|
|
401
475
|
});
|
|
402
476
|
|
|
477
|
+
// Publish this layer's own key set changes. A parent structural change reaches this collection
|
|
478
|
+
// only while passing through; the query-driven case is published by updateObjectsForResults
|
|
479
|
+
// once it has finished writing, so nothing here enumerates the private collection per write.
|
|
480
|
+
watch([passthrough, () => parentState.objectsVersion], syncObjectsVersion, {
|
|
481
|
+
immediate: true,
|
|
482
|
+
flush: "sync",
|
|
483
|
+
});
|
|
484
|
+
|
|
403
485
|
watch(
|
|
404
486
|
toRef(state, "objectIndexes"),
|
|
405
487
|
() => {
|
|
@@ -423,15 +505,21 @@ export function useListSearch({ parentState, props, throttle = 500, showAllWhenE
|
|
|
423
505
|
}
|
|
424
506
|
);
|
|
425
507
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
508
|
+
// While passing through, the private collection is empty and enumerating it buys nothing.
|
|
509
|
+
watch(
|
|
510
|
+
[passthrough, () => (passthrough.value ? null : Object.keys(_objects)), toRef(parentState, "order")],
|
|
511
|
+
updateOrder,
|
|
512
|
+
{
|
|
513
|
+
immediate: true,
|
|
514
|
+
deep: true,
|
|
515
|
+
}
|
|
516
|
+
);
|
|
430
517
|
});
|
|
431
518
|
textSearchIndex.events.addEventListener("newIndex", indexWasCleared);
|
|
432
519
|
return {
|
|
433
520
|
state,
|
|
434
521
|
textSearchIndex,
|
|
522
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
435
523
|
stop: () => {
|
|
436
524
|
textSearchIndex.events.removeEventListener("newIndex", indexWasCleared);
|
|
437
525
|
es.stop();
|
package/use/listSort.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keyDiff } from "../utils/keyDiff.js";
|
|
2
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
2
3
|
import { loadingCombine } from "../utils/loadingCombine.js";
|
|
3
4
|
import { proxyRunning } from "../utils/proxyRunning.js";
|
|
4
5
|
import get from "lodash-es/get.js";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
onScopeDispose,
|
|
11
12
|
reactive,
|
|
12
13
|
ref,
|
|
14
|
+
shallowReactive,
|
|
13
15
|
shallowReadonly,
|
|
14
16
|
toRef,
|
|
15
17
|
toRefs,
|
|
@@ -98,6 +100,7 @@ export function setListSortDefaultOptions({ sortThrottleWait }) {
|
|
|
98
100
|
* @typedef {object} ListSortProperties - The properties available on a list sort instance.
|
|
99
101
|
* @property {ListSortState} state - The reactive state for the list sort.
|
|
100
102
|
* @property {ListSortParentState} parentState - The parent state.
|
|
103
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
101
104
|
* @property {() => void} stop - A function to stop the effect scope and clean up resources.
|
|
102
105
|
*/
|
|
103
106
|
|
|
@@ -189,11 +192,14 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
189
192
|
),
|
|
190
193
|
});
|
|
191
194
|
|
|
192
|
-
|
|
195
|
+
// Track entry changes without proxying entries or unwrapping their computed refs.
|
|
196
|
+
/** @type {Map<import('../config/commonCrud.js').Pk, {scope: import('vue').EffectScope, crit: import('vue').ComputedRef<any[]>}>} */
|
|
197
|
+
const criteriaMap = shallowReactive(new Map());
|
|
193
198
|
|
|
194
199
|
function ensureCriteria(pk) {
|
|
195
|
-
|
|
196
|
-
|
|
200
|
+
const existing = criteriaMap.get(pk);
|
|
201
|
+
if (existing) {
|
|
202
|
+
return existing;
|
|
197
203
|
}
|
|
198
204
|
const scope = es.run(() => effectScope());
|
|
199
205
|
const crit = scope.run(() =>
|
|
@@ -223,15 +229,15 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
223
229
|
);
|
|
224
230
|
})
|
|
225
231
|
);
|
|
226
|
-
criteriaMap
|
|
232
|
+
criteriaMap.set(pk, { scope, crit });
|
|
227
233
|
return crit;
|
|
228
234
|
}
|
|
229
235
|
|
|
230
236
|
function syncCriteria(newKeys) {
|
|
231
|
-
const { addedKeys, removedKeys } = keyDiff(newKeys,
|
|
237
|
+
const { addedKeys, removedKeys } = keyDiff(newKeys, [...criteriaMap.keys()]);
|
|
232
238
|
for (const pk of removedKeys) {
|
|
233
|
-
criteriaMap
|
|
234
|
-
delete
|
|
239
|
+
criteriaMap.get(pk).scope.stop();
|
|
240
|
+
criteriaMap.delete(pk);
|
|
235
241
|
}
|
|
236
242
|
for (const pk of addedKeys) {
|
|
237
243
|
ensureCriteria(pk);
|
|
@@ -239,6 +245,10 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
239
245
|
}
|
|
240
246
|
|
|
241
247
|
es.run(() => {
|
|
248
|
+
// One structural pass per change. This used to carry a second, deferred watcher on the parent's
|
|
249
|
+
// keys, because the filter and search layers moved their own key set without moving the
|
|
250
|
+
// version they forwarded. Both own an accurate version now, so the sync watcher sees every
|
|
251
|
+
// cause and criteria exist before anything reads them.
|
|
242
252
|
watch(
|
|
243
253
|
() => parentState.objectsVersion,
|
|
244
254
|
() => {
|
|
@@ -246,15 +256,14 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
246
256
|
},
|
|
247
257
|
{ immediate: true, flush: "sync" }
|
|
248
258
|
);
|
|
249
|
-
watch(() => Object.keys(parentState.objects), syncCriteria);
|
|
250
259
|
});
|
|
251
260
|
|
|
252
261
|
const rawOrder = computed(() => {
|
|
253
262
|
const arr = [...unref(toRef(parentState, "order"))];
|
|
254
263
|
const rulesArr = internalState.orderByRules?.filter(identity) || [];
|
|
255
264
|
return arr.sort((a, b) => {
|
|
256
|
-
const aCrit = criteriaMap
|
|
257
|
-
const bCrit = criteriaMap
|
|
265
|
+
const aCrit = criteriaMap.get(a)?.crit.value ?? [];
|
|
266
|
+
const bCrit = criteriaMap.get(b)?.crit.value ?? [];
|
|
258
267
|
for (let i = 0; i < rulesArr.length; i++) {
|
|
259
268
|
const rule = rulesArr[i];
|
|
260
269
|
let x = aCrit[i],
|
|
@@ -286,17 +295,67 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
286
295
|
});
|
|
287
296
|
});
|
|
288
297
|
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
// Track membership without reading the record's criteria.
|
|
299
|
+
const hasCriteria = (/** @type {import('../config/commonCrud.js').Pk} */ pk) => criteriaMap.has(pk);
|
|
300
|
+
|
|
301
|
+
// Cache enumeration separately from per-key reads.
|
|
302
|
+
/** @type {import('vue').ComputedRef<import('./listInstance.js').ObjectsByPk>} */
|
|
303
|
+
const criteriaObjects = computed(() => {
|
|
291
304
|
/** @type {{[pk: import('../config/commonCrud.js').Pk]: import('./objectInstance.js').ExistingCrudObject}} */
|
|
292
305
|
const out = {};
|
|
293
306
|
for (const [pk, o] of Object.entries(parentState.objects)) {
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
if (hasCriteria(pk)) {
|
|
308
|
+
out[pk] = o;
|
|
309
|
+
}
|
|
296
310
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
311
|
+
return out;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Resolve individual keys without tracking the full collection.
|
|
315
|
+
// Implement readonly behaviour here to avoid an extra proxy layer.
|
|
316
|
+
/** @type {import('./listInstance.js').ObjectsByPk} */
|
|
317
|
+
const objects = new Proxy(/** @type {any} */ ({}), {
|
|
318
|
+
get(target, prop) {
|
|
319
|
+
if (typeof prop === "symbol") {
|
|
320
|
+
return Reflect.get(target, prop);
|
|
321
|
+
}
|
|
322
|
+
if (prop === "__v_isReadonly") {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return hasCriteria(prop) ? parentState.objects[prop] : undefined;
|
|
326
|
+
},
|
|
327
|
+
set(target, prop) {
|
|
328
|
+
console.warn(`useListSort: set operation on key "${String(prop)}" failed: objects is read-only.`);
|
|
329
|
+
return true;
|
|
330
|
+
},
|
|
331
|
+
deleteProperty(target, prop) {
|
|
332
|
+
console.warn(`useListSort: delete operation on key "${String(prop)}" failed: objects is read-only.`);
|
|
333
|
+
return true;
|
|
334
|
+
},
|
|
335
|
+
has(target, prop) {
|
|
336
|
+
if (typeof prop === "symbol") {
|
|
337
|
+
return Reflect.has(target, prop);
|
|
338
|
+
}
|
|
339
|
+
return hasCriteria(prop) && prop in parentState.objects;
|
|
340
|
+
},
|
|
341
|
+
ownKeys() {
|
|
342
|
+
return Reflect.ownKeys(criteriaObjects.value);
|
|
343
|
+
},
|
|
344
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
345
|
+
if (typeof prop === "symbol") {
|
|
346
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
347
|
+
}
|
|
348
|
+
// Use the cached collection because descriptor lookups occur during enumeration.
|
|
349
|
+
const object = criteriaObjects.value[prop];
|
|
350
|
+
if (object === undefined) {
|
|
351
|
+
// Keep descriptors consistent with ownKeys().
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
return { configurable: true, enumerable: true, value: object, writable: true };
|
|
355
|
+
},
|
|
356
|
+
getPrototypeOf() {
|
|
357
|
+
return Object.prototype;
|
|
358
|
+
},
|
|
300
359
|
});
|
|
301
360
|
|
|
302
361
|
const order = ref([]);
|
|
@@ -343,23 +402,25 @@ export function useListSort({ parentState, orderByRules, sortThrottleWait = defa
|
|
|
343
402
|
// 6) objectsInOrder just follows that
|
|
344
403
|
const objectsInOrder = computed(() => shallowReadonly(order.value.map((pk) => parentState.objects[pk])));
|
|
345
404
|
|
|
405
|
+
/** @type {ListSortState} */
|
|
406
|
+
const state = reactive({
|
|
407
|
+
...toRefs(parentState),
|
|
408
|
+
orderByRules: toRef(internalState, "orderByRules"),
|
|
409
|
+
orderByDesc: toRef(internalState, "orderByDesc"),
|
|
410
|
+
objects,
|
|
411
|
+
// the ref stays private to writeOrder; the state exposes a read-only view of it
|
|
412
|
+
order: computed(() => shallowReadonly(order.value)),
|
|
413
|
+
objectsInOrder,
|
|
414
|
+
running,
|
|
415
|
+
});
|
|
416
|
+
|
|
346
417
|
return {
|
|
347
|
-
state
|
|
348
|
-
...toRefs(parentState),
|
|
349
|
-
orderByRules: toRef(internalState, "orderByRules"),
|
|
350
|
-
orderByDesc: toRef(internalState, "orderByDesc"),
|
|
351
|
-
objects,
|
|
352
|
-
// the ref stays private to writeOrder; the state exposes a read-only view of it
|
|
353
|
-
order: computed(() => shallowReadonly(order.value)),
|
|
354
|
-
objectsInOrder,
|
|
355
|
-
running,
|
|
356
|
-
}),
|
|
418
|
+
state,
|
|
357
419
|
parentState,
|
|
420
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
358
421
|
stop: () => {
|
|
359
422
|
es.stop();
|
|
360
|
-
|
|
361
|
-
delete criteriaMap[key];
|
|
362
|
-
}
|
|
423
|
+
criteriaMap.clear();
|
|
363
424
|
},
|
|
364
425
|
};
|
|
365
426
|
}
|
package/use/listSubscription.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useCancellableIntent } from "./cancellableIntent.js";
|
|
|
2
2
|
import { useListInstance } from "./listInstance.js";
|
|
3
3
|
import { useLoadingError } from "./loadingError.js";
|
|
4
4
|
import { normalizePk } from "../utils/refIfReactive.js";
|
|
5
|
+
import { makeMembershipWatcher } from "../utils/watches.js";
|
|
5
6
|
import inspect from "browser-util-inspect";
|
|
6
7
|
import cloneDeep from "lodash-es/cloneDeep.js";
|
|
7
8
|
import isEmpty from "lodash-es/isEmpty.js";
|
|
@@ -74,6 +75,7 @@ export class ListSubscriptionError extends Error {
|
|
|
74
75
|
* @property {import('./listInstance.js').ListInstance} listInstance - The list instance used by the subscription.
|
|
75
76
|
* @property {import('./cancellableIntent.js').CancellableIntent} listIntent - The `CancellableIntent` instance managing if the list should be (re)fetched.
|
|
76
77
|
* @property {import('./cancellableIntent.js').CancellableIntent} subscribeIntent - The `CancellableIntent` instance managing if the subscription should be (un)subscribed.
|
|
78
|
+
* @property {import('../utils/watches.js').WatchMembershipChanged} watchMembershipChanged - Registers a callback for changes to the set of object keys this layer holds. The watcher belongs to the effect scope active where it is called, not to this layer, so stopping this layer silences it without disposing it.
|
|
77
79
|
*/
|
|
78
80
|
|
|
79
81
|
/**
|
|
@@ -339,6 +341,7 @@ export function useListSubscription({ listInstance, props, handlers }) {
|
|
|
339
341
|
listInstance,
|
|
340
342
|
listIntent,
|
|
341
343
|
subscribeIntent,
|
|
344
|
+
watchMembershipChanged: makeMembershipWatcher(state),
|
|
342
345
|
clearError: proxyLoadingError.clearError,
|
|
343
346
|
// Stops both intents, mirroring useObjectSubscription, so a caller that owns this
|
|
344
347
|
// subscription's lifetime does not have to know its intent inventory. The wrapped
|
package/utils/watches.js
CHANGED
|
@@ -8,6 +8,38 @@ import { watch, toRef } from "vue";
|
|
|
8
8
|
*
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @callback WatchMembershipChanged - Calls back when a list layer's set of object keys changes.
|
|
13
|
+
* Registers in the effect scope active where it is called, not in the layer's own scope, so the
|
|
14
|
+
* returned handle and the surrounding scope own the watcher. Stopping the layer does not dispose it,
|
|
15
|
+
* because the layer never owned it; it only silences it, since a stopped layer publishes nothing
|
|
16
|
+
* further. Stop it through the returned handle or through the enclosing scope.
|
|
17
|
+
* The callback takes no arguments: it reports that membership moved, not what it moved to. Read the
|
|
18
|
+
* layer's collection views for that.
|
|
19
|
+
* @param {() => void} callback - Called after the layer's set of object keys changes.
|
|
20
|
+
* @param {import('vue').WatchOptions} [options] - Passed through to Vue's `watch`, so `immediate`,
|
|
21
|
+
* `flush`, and `once` all behave as they do there.
|
|
22
|
+
* @returns {import('vue').WatchHandle} - Stops the watcher.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a layer's membership watcher over the counter that layer publishes.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
* @param {{objectsVersion: number}} state - The layer's own state.
|
|
30
|
+
* @returns {WatchMembershipChanged} - The watcher registration function for that layer.
|
|
31
|
+
*/
|
|
32
|
+
export function makeMembershipWatcher(state) {
|
|
33
|
+
// The counter is an implementation detail of how membership changes are published, so it is not
|
|
34
|
+
// passed to the callback. That keeps how the signal is carried free to change.
|
|
35
|
+
return (callback, options) =>
|
|
36
|
+
watch(
|
|
37
|
+
() => state.objectsVersion,
|
|
38
|
+
() => callback(),
|
|
39
|
+
options
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
/**
|
|
12
44
|
* Provides a mechanism for immediately starting and potentially stopping a Vue.js watcher
|
|
13
45
|
* during its first invocation. This is useful when the need arises to terminate the watch
|