@mobx-query/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/README.md +98 -0
- package/eslint.config.js +29 -0
- package/index.html +13 -0
- package/package.json +60 -0
- package/public/vite.svg +1 -0
- package/src/App.css +0 -0
- package/src/App.tsx +5 -0
- package/src/api/constants.ts +1 -0
- package/src/api/fetch.ts +29 -0
- package/src/api/todos.ts +14 -0
- package/src/api/types.ts +30 -0
- package/src/api/users.ts +19 -0
- package/src/assets/react.svg +1 -0
- package/src/index.css +60 -0
- package/src/libs/mobx-query/client/MQClient.ts +75 -0
- package/src/libs/mobx-query/client/MQClientAccessor.ts +74 -0
- package/src/libs/mobx-query/client/index.ts +8 -0
- package/src/libs/mobx-query/client/types.ts +37 -0
- package/src/libs/mobx-query/entity/Entity.ts +232 -0
- package/src/libs/mobx-query/entity/EntityCollection.ts +285 -0
- package/src/libs/mobx-query/entity/constants.ts +7 -0
- package/src/libs/mobx-query/entity/index.ts +15 -0
- package/src/libs/mobx-query/entity/types.ts +11 -0
- package/src/libs/mobx-query/mutations/BatchMutationBase.ts +105 -0
- package/src/libs/mobx-query/mutations/BatchUpdateMutation.ts +48 -0
- package/src/libs/mobx-query/mutations/CreateMutation.ts +172 -0
- package/src/libs/mobx-query/mutations/DeleteMutation.ts +94 -0
- package/src/libs/mobx-query/mutations/EntityMutationBase.ts +110 -0
- package/src/libs/mobx-query/mutations/MutationBase.ts +40 -0
- package/src/libs/mobx-query/mutations/OptimisticMutationStrategy.ts +122 -0
- package/src/libs/mobx-query/mutations/UpdateMutation.ts +76 -0
- package/src/libs/mobx-query/mutations/constants.ts +16 -0
- package/src/libs/mobx-query/mutations/index.ts +44 -0
- package/src/libs/mobx-query/mutations/types.ts +205 -0
- package/src/libs/mobx-query/queries/QueryBase.ts +65 -0
- package/src/libs/mobx-query/queries/QueryFragmentMany.ts +31 -0
- package/src/libs/mobx-query/queries/QueryFragmentOne.ts +35 -0
- package/src/libs/mobx-query/queries/QueryMany.ts +80 -0
- package/src/libs/mobx-query/queries/QueryManyBase.ts +135 -0
- package/src/libs/mobx-query/queries/QueryOne.ts +84 -0
- package/src/libs/mobx-query/queries/QueryOneBase.ts +93 -0
- package/src/libs/mobx-query/queries/index.ts +33 -0
- package/src/libs/mobx-query/queries/types.ts +60 -0
- package/src/libs/mobx-query/react/createReactContext.tsx +23 -0
- package/src/libs/mobx-query/react/index.ts +3 -0
- package/src/libs/mobx-query/utils/generateEntityId.ts +12 -0
- package/src/libs/mobx-query/utils/index.ts +8 -0
- package/src/libs/mobx-query/utils/invalidateQueryByHash.ts +18 -0
- package/src/libs/mobx-query/utils/types.ts +18 -0
- package/src/libs/react-query.ts +11 -0
- package/src/main.tsx +16 -0
- package/src/utils.ts +3 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +27 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +52 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import {
|
|
2
|
+
action,
|
|
3
|
+
type IObjectDidChange,
|
|
4
|
+
isObservableArray,
|
|
5
|
+
isObservableMap,
|
|
6
|
+
observable,
|
|
7
|
+
observe,
|
|
8
|
+
toJS,
|
|
9
|
+
} from "mobx";
|
|
10
|
+
import type { EntityData, EntityEvents, EntityId } from "./types";
|
|
11
|
+
import { MQClientAccessor } from "../client";
|
|
12
|
+
import { invalidateQueryByHash } from "../utils";
|
|
13
|
+
import { EntityState } from "./constants";
|
|
14
|
+
|
|
15
|
+
export type EntityConstructor<
|
|
16
|
+
TData = unknown,
|
|
17
|
+
TEntityId extends EntityId = string,
|
|
18
|
+
E extends Entity<TData, TEntityId> = Entity<TData, TEntityId>,
|
|
19
|
+
> = new () => E;
|
|
20
|
+
|
|
21
|
+
export type EntityConstructorAny = EntityConstructor<
|
|
22
|
+
EntityData,
|
|
23
|
+
any,
|
|
24
|
+
EntityAny
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export type EntityAny = Entity<any, any>;
|
|
28
|
+
|
|
29
|
+
const IGNORE_FIELDS: PropertyKey[] = ["isDirty", "state"] as const;
|
|
30
|
+
|
|
31
|
+
export type EntityValueKeys<T> = {
|
|
32
|
+
[K in keyof T]: T[K] extends Function
|
|
33
|
+
? never
|
|
34
|
+
: K extends "isDirty" | "state" | "queryHashes"
|
|
35
|
+
? never
|
|
36
|
+
: K;
|
|
37
|
+
}[keyof T] &
|
|
38
|
+
string;
|
|
39
|
+
|
|
40
|
+
export abstract class Entity<
|
|
41
|
+
TData = unknown,
|
|
42
|
+
TEntityId extends EntityId = string,
|
|
43
|
+
> extends MQClientAccessor {
|
|
44
|
+
abstract id: TEntityId;
|
|
45
|
+
|
|
46
|
+
abstract hydrate(data: TData): void;
|
|
47
|
+
|
|
48
|
+
readonly queryHashes = new Set<string>();
|
|
49
|
+
|
|
50
|
+
private events!: EntityEvents<TEntityId>;
|
|
51
|
+
|
|
52
|
+
@observable accessor state: EntityState = EntityState.CONFIRMED;
|
|
53
|
+
@observable accessor isDirty = false;
|
|
54
|
+
|
|
55
|
+
private readonly initValuesSnapshot: Map<string | number | symbol, unknown> =
|
|
56
|
+
new Map();
|
|
57
|
+
|
|
58
|
+
private isHydrated = false;
|
|
59
|
+
|
|
60
|
+
_init(queryHashes: string[], events: EntityEvents<TEntityId>) {
|
|
61
|
+
this.events = events;
|
|
62
|
+
|
|
63
|
+
for (const hash of queryHashes) {
|
|
64
|
+
this.queryHashes.add(hash);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
observe(this, (change) => this.onObservableChange(change));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// @computed get isDirty() {
|
|
71
|
+
// return this._isDirty
|
|
72
|
+
// }
|
|
73
|
+
|
|
74
|
+
_removeQueryHashes(hashes: string[]) {
|
|
75
|
+
for (const hash of hashes) {
|
|
76
|
+
this.queryHashes.delete(hash);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.queryHashes.size === 0) {
|
|
80
|
+
this.events.onAllQueryHashesRemoved(this.id);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_markAsHydrated() {
|
|
85
|
+
this.isHydrated = true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@action private onObservableChange(change: IObjectDidChange) {
|
|
89
|
+
if (change.type !== "update") {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!this.isHydrated) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (IGNORE_FIELDS.includes(change.name as any)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Store the initial value (deep cloned for complex data structures)
|
|
102
|
+
if (!this.initValuesSnapshot.has(change.name)) {
|
|
103
|
+
// Deep clone complex data structures (objects, arrays, nested observables)
|
|
104
|
+
// toJS converts observables to plain JS, effectively creating a deep clone
|
|
105
|
+
this.initValuesSnapshot.set(
|
|
106
|
+
change.name,
|
|
107
|
+
this.deepCloneValue(change.oldValue),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.isDirty === false) {
|
|
112
|
+
this.isDirty = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Deep clones a value to ensure we store a snapshot, not a reference.
|
|
118
|
+
* Handles primitives, objects, arrays, and MobX observables.
|
|
119
|
+
*/
|
|
120
|
+
private deepCloneValue(value: unknown): unknown {
|
|
121
|
+
// Handle null and undefined
|
|
122
|
+
if (value === null || value === undefined) {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle primitives (string, number, boolean, symbol, bigint)
|
|
127
|
+
const valueType = typeof value;
|
|
128
|
+
if (
|
|
129
|
+
valueType === "string" ||
|
|
130
|
+
valueType === "number" ||
|
|
131
|
+
valueType === "boolean" ||
|
|
132
|
+
valueType === "symbol" ||
|
|
133
|
+
valueType === "bigint"
|
|
134
|
+
) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Handle functions - store as-is (though they shouldn't typically be observable)
|
|
139
|
+
if (valueType === "function") {
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (value instanceof Date) {
|
|
144
|
+
return new Date(value.getTime());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (value instanceof RegExp) {
|
|
148
|
+
return new RegExp(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (value instanceof Set) {
|
|
152
|
+
return new Set(
|
|
153
|
+
Array.from(value).map((item) => this.deepCloneValue(item)),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (value instanceof Map) {
|
|
158
|
+
const clonedMap = new Map();
|
|
159
|
+
for (const [key, val] of value.entries()) {
|
|
160
|
+
clonedMap.set(this.deepCloneValue(key), this.deepCloneValue(val));
|
|
161
|
+
}
|
|
162
|
+
return clonedMap;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// For objects and arrays (including MobX observables), use toJS to convert
|
|
166
|
+
// observables to plain JS and create a deep clone
|
|
167
|
+
try {
|
|
168
|
+
return toJS(value);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// Fallback: if toJS fails (e.g., circular reference), return as-is
|
|
171
|
+
// This is a safety measure, though it may not fully solve the problem
|
|
172
|
+
console.warn(
|
|
173
|
+
`Failed to deep clone value for property, using reference:`,
|
|
174
|
+
error,
|
|
175
|
+
);
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@action reset() {
|
|
181
|
+
for (const [key, value] of this.initValuesSnapshot.entries()) {
|
|
182
|
+
// Deep clone the stored value before restoring to ensure we're not
|
|
183
|
+
// assigning a reference that might have been mutated
|
|
184
|
+
const clonedValue = this.deepCloneValue(value);
|
|
185
|
+
|
|
186
|
+
// @ts-expect-error: unknown field
|
|
187
|
+
const currentValue = this[key];
|
|
188
|
+
|
|
189
|
+
// Handle observable arrays and maps specially, similar to createViewModel
|
|
190
|
+
// This ensures proper restoration of MobX observable collections
|
|
191
|
+
if (isObservableArray(currentValue)) {
|
|
192
|
+
// For observable arrays, use replace() to restore the original array
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
194
|
+
currentValue.replace(clonedValue as any);
|
|
195
|
+
} else if (isObservableMap(currentValue)) {
|
|
196
|
+
// For observable maps, clear and merge to restore the original map
|
|
197
|
+
currentValue.clear();
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
currentValue.merge(clonedValue as any);
|
|
200
|
+
} else {
|
|
201
|
+
// For other types, assign directly
|
|
202
|
+
// @ts-expect-error: unknown field
|
|
203
|
+
this[key] = clonedValue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this._clearDirty();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@action _clearDirty() {
|
|
211
|
+
this.isDirty = false;
|
|
212
|
+
this.initValuesSnapshot.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// isDirtyField(field: EntityValueKeys<this>) {
|
|
216
|
+
// return this.initValuesSnapshot.has(field)
|
|
217
|
+
// }
|
|
218
|
+
|
|
219
|
+
// getOriginalValue<K extends EntityValueKeys<this>>(
|
|
220
|
+
// field: K,
|
|
221
|
+
// ): this[K] | undefined {
|
|
222
|
+
// return this.initValuesSnapshot.get(field) as this[K] | undefined
|
|
223
|
+
// }
|
|
224
|
+
|
|
225
|
+
invalidateRelatedQueries() {
|
|
226
|
+
const cache = this.queryClient.getQueryCache();
|
|
227
|
+
|
|
228
|
+
for (const hash of this.queryHashes) {
|
|
229
|
+
invalidateQueryByHash(hash, cache, () => this._removeQueryHashes([hash]));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { action, computed, observable } from "mobx";
|
|
2
|
+
import type { EntityConstructor, EntityConstructorAny } from "./Entity";
|
|
3
|
+
import { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { EntityDataAny, EntityId } from "./types";
|
|
5
|
+
|
|
6
|
+
export class EntityCollection<
|
|
7
|
+
TEntityConstructor extends EntityConstructorAny = EntityConstructorAny,
|
|
8
|
+
> {
|
|
9
|
+
@observable accessor collection = new Map<
|
|
10
|
+
EntityId,
|
|
11
|
+
InstanceType<TEntityConstructor>
|
|
12
|
+
>();
|
|
13
|
+
@observable accessor deletedRecordIds = new Set<EntityId>();
|
|
14
|
+
@observable accessor clientOnlyEntityIds = new Set<EntityId>();
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly entityConstructor: EntityConstructor<
|
|
18
|
+
any,
|
|
19
|
+
any,
|
|
20
|
+
InstanceType<TEntityConstructor>
|
|
21
|
+
>,
|
|
22
|
+
private readonly queryClient: QueryClient,
|
|
23
|
+
) {
|
|
24
|
+
this.initQueryClientCacheListener();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@computed get size() {
|
|
28
|
+
return this.collection.size;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@computed get entities() {
|
|
32
|
+
const entities: InstanceType<TEntityConstructor>[] = [];
|
|
33
|
+
|
|
34
|
+
for (const entity of this.collection.values()) {
|
|
35
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
36
|
+
entities.push(entity);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return entities;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@computed get clientOnlyEntitiesMap() {
|
|
43
|
+
const entities = new Map<EntityId, InstanceType<TEntityConstructor>>();
|
|
44
|
+
|
|
45
|
+
for (const entityId of this.clientOnlyEntityIds.values()) {
|
|
46
|
+
const entity = this.collection.get(entityId);
|
|
47
|
+
|
|
48
|
+
if (!entity) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
entities.set(entityId, entity);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return entities;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@computed get clientOnlyEntities() {
|
|
59
|
+
const entities: InstanceType<TEntityConstructor>[] = [];
|
|
60
|
+
|
|
61
|
+
for (const entityId of this.clientOnlyEntityIds.values()) {
|
|
62
|
+
const entity = this.collection.get(entityId);
|
|
63
|
+
|
|
64
|
+
if (!entity) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
entities.push(entity);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return entities;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@action setEntity(data: EntityDataAny, queryHashes?: string[]) {
|
|
75
|
+
// if (clearQueryHashes && queryHashes) {
|
|
76
|
+
// console.log('clearQueryHashes', queryHashes)
|
|
77
|
+
// this.removeQueryHashesFromAllEntities(queryHashes)
|
|
78
|
+
// }
|
|
79
|
+
|
|
80
|
+
const entity =
|
|
81
|
+
new this.entityConstructor() as InstanceType<TEntityConstructor>;
|
|
82
|
+
entity.hydrate(data);
|
|
83
|
+
entity._markAsHydrated();
|
|
84
|
+
|
|
85
|
+
const prevEntity = this.collection.get(entity.id) as
|
|
86
|
+
| InstanceType<TEntityConstructor>
|
|
87
|
+
| undefined;
|
|
88
|
+
|
|
89
|
+
const updatedQueryHashes = prevEntity
|
|
90
|
+
? [...prevEntity.queryHashes, ...(queryHashes || [])]
|
|
91
|
+
: [...(queryHashes || [])];
|
|
92
|
+
|
|
93
|
+
if (prevEntity) {
|
|
94
|
+
if (queryHashes) {
|
|
95
|
+
for (const hash of queryHashes) {
|
|
96
|
+
prevEntity.queryHashes.add(hash);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
prevEntity.hydrate(data);
|
|
101
|
+
prevEntity._clearDirty();
|
|
102
|
+
|
|
103
|
+
return prevEntity;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
entity._init(updatedQueryHashes, {
|
|
107
|
+
onAllQueryHashesRemoved: (entityId: EntityId) =>
|
|
108
|
+
this.deleteEntity(entityId),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.collection.set(entity.id, entity);
|
|
112
|
+
return entity;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@action setEntities(entityData: EntityDataAny[], queryHashes: string[]) {
|
|
116
|
+
const entities: InstanceType<TEntityConstructor>[] = [];
|
|
117
|
+
const ids: EntityId[] = [];
|
|
118
|
+
|
|
119
|
+
if (!Array.isArray(entityData)) {
|
|
120
|
+
throw new Error("setEntities: entityData must be an array");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (queryHashes) {
|
|
124
|
+
this.removeQueryHashesFromAllEntities(queryHashes);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const record of entityData) {
|
|
128
|
+
const entity = this.setEntity(record, queryHashes);
|
|
129
|
+
entities.push(entity);
|
|
130
|
+
ids.push(entity.id);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return [entities, ids] as const;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@action deleteEntity(entityId: EntityId) {
|
|
137
|
+
this.collection.delete(entityId);
|
|
138
|
+
this.clientOnlyEntityIds.delete(entityId);
|
|
139
|
+
this.deletedRecordIds.delete(entityId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getEntityById(entityId: EntityId) {
|
|
143
|
+
return this.collection.get(entityId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
filter(
|
|
147
|
+
predicate: (
|
|
148
|
+
value: InstanceType<TEntityConstructor>,
|
|
149
|
+
index: number,
|
|
150
|
+
) => boolean,
|
|
151
|
+
): InstanceType<TEntityConstructor>[] {
|
|
152
|
+
const result: InstanceType<TEntityConstructor>[] = [];
|
|
153
|
+
let i = 0;
|
|
154
|
+
for (const entity of this.collection.values()) {
|
|
155
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
156
|
+
if (predicate(entity, i++)) {
|
|
157
|
+
result.push(entity);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
find(
|
|
164
|
+
predicate: (
|
|
165
|
+
value: InstanceType<TEntityConstructor>,
|
|
166
|
+
index: number,
|
|
167
|
+
) => boolean,
|
|
168
|
+
): InstanceType<TEntityConstructor> | undefined {
|
|
169
|
+
let i = 0;
|
|
170
|
+
for (const entity of this.collection.values()) {
|
|
171
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
172
|
+
if (predicate(entity, i++)) {
|
|
173
|
+
return entity;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
findIndex(
|
|
180
|
+
predicate: (
|
|
181
|
+
value: InstanceType<TEntityConstructor>,
|
|
182
|
+
index: number,
|
|
183
|
+
) => boolean,
|
|
184
|
+
): number {
|
|
185
|
+
let i = 0;
|
|
186
|
+
for (const entity of this.collection.values()) {
|
|
187
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
188
|
+
if (predicate(entity, i)) {
|
|
189
|
+
return i;
|
|
190
|
+
}
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
findLast(
|
|
197
|
+
predicate: (
|
|
198
|
+
value: InstanceType<TEntityConstructor>,
|
|
199
|
+
index: number,
|
|
200
|
+
) => boolean,
|
|
201
|
+
): InstanceType<TEntityConstructor> | undefined {
|
|
202
|
+
const entities = this.entities;
|
|
203
|
+
for (let i = entities.length - 1; i >= 0; i--) {
|
|
204
|
+
const entity = entities[i];
|
|
205
|
+
if (predicate(entity, i)) {
|
|
206
|
+
return entity;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
findLastIndex(
|
|
213
|
+
predicate: (
|
|
214
|
+
value: InstanceType<TEntityConstructor>,
|
|
215
|
+
index: number,
|
|
216
|
+
) => boolean,
|
|
217
|
+
): number {
|
|
218
|
+
const entities = this.entities;
|
|
219
|
+
for (let i = entities.length - 1; i >= 0; i--) {
|
|
220
|
+
const entity = entities[i];
|
|
221
|
+
if (predicate(entity, i)) {
|
|
222
|
+
return i;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return -1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
some(
|
|
229
|
+
predicate: (
|
|
230
|
+
value: InstanceType<TEntityConstructor>,
|
|
231
|
+
index: number,
|
|
232
|
+
) => boolean,
|
|
233
|
+
): boolean {
|
|
234
|
+
let i = 0;
|
|
235
|
+
for (const entity of this.collection.values()) {
|
|
236
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
237
|
+
if (predicate(entity, i++)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
every(
|
|
245
|
+
predicate: (
|
|
246
|
+
value: InstanceType<TEntityConstructor>,
|
|
247
|
+
index: number,
|
|
248
|
+
) => boolean,
|
|
249
|
+
): boolean {
|
|
250
|
+
let i = 0;
|
|
251
|
+
for (const entity of this.collection.values()) {
|
|
252
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
253
|
+
if (!predicate(entity, i++)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
map<T>(
|
|
261
|
+
predicate: (value: InstanceType<TEntityConstructor>, index: number) => T,
|
|
262
|
+
): T[] {
|
|
263
|
+
const result: T[] = [];
|
|
264
|
+
let i = 0;
|
|
265
|
+
for (const entity of this.collection.values()) {
|
|
266
|
+
if (this.deletedRecordIds.has(entity.id)) continue;
|
|
267
|
+
result.push(predicate(entity, i++));
|
|
268
|
+
}
|
|
269
|
+
return observable.array(result);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private initQueryClientCacheListener() {
|
|
273
|
+
this.queryClient.getQueryCache().subscribe((event) => {
|
|
274
|
+
if (event.type === "removed") {
|
|
275
|
+
this.removeQueryHashesFromAllEntities([event.query.queryHash]);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private removeQueryHashesFromAllEntities(hashes: string[]) {
|
|
281
|
+
for (const entity of this.collection.values()) {
|
|
282
|
+
entity._removeQueryHashes(hashes);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Entity, type EntityConstructorAny, type EntityAny } from "./Entity";
|
|
2
|
+
import { EntityCollection } from "./EntityCollection";
|
|
3
|
+
import { EntityState } from "./constants";
|
|
4
|
+
|
|
5
|
+
import type { EntityId, EntityData, EntityDataAny } from "./types";
|
|
6
|
+
|
|
7
|
+
export { Entity, EntityState, EntityCollection };
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
EntityConstructorAny,
|
|
11
|
+
EntityAny,
|
|
12
|
+
EntityData,
|
|
13
|
+
EntityDataAny,
|
|
14
|
+
EntityId,
|
|
15
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type EntityId = string | number;
|
|
2
|
+
|
|
3
|
+
export interface EntityData<TEntityId extends EntityId = string> {
|
|
4
|
+
id: TEntityId;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type EntityDataAny = EntityData<EntityId>;
|
|
8
|
+
|
|
9
|
+
export interface EntityEvents<TEntityId extends EntityId = string> {
|
|
10
|
+
onAllQueryHashesRemoved: (entityId: TEntityId) => void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DefaultError,
|
|
3
|
+
MutationFunctionContext,
|
|
4
|
+
} from "@tanstack/react-query";
|
|
5
|
+
import type { EntityConstructorAny, EntityCollection } from "../entity";
|
|
6
|
+
import type {
|
|
7
|
+
BatchMutationInputInternal,
|
|
8
|
+
UseBatchMutationHookOptions,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { MutationBase } from "./MutationBase";
|
|
11
|
+
|
|
12
|
+
export class BatchMutationBase<
|
|
13
|
+
TEntityConstructor extends EntityConstructorAny,
|
|
14
|
+
TError = DefaultError,
|
|
15
|
+
TMutateResult = unknown,
|
|
16
|
+
> extends MutationBase<TEntityConstructor> {
|
|
17
|
+
protected readonly collection: EntityCollection<TEntityConstructor>;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
mutationPrefix: string,
|
|
21
|
+
protected readonly options: UseBatchMutationHookOptions<
|
|
22
|
+
TEntityConstructor,
|
|
23
|
+
TError,
|
|
24
|
+
TMutateResult
|
|
25
|
+
>,
|
|
26
|
+
) {
|
|
27
|
+
super(mutationPrefix, options.entity);
|
|
28
|
+
|
|
29
|
+
this.collection = this.getEntityCollection(options.entity);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected async onMutateBase(
|
|
33
|
+
input: BatchMutationInputInternal<TEntityConstructor>,
|
|
34
|
+
context: MutationFunctionContext,
|
|
35
|
+
): Promise<TMutateResult> {
|
|
36
|
+
input.strategy.onMutate();
|
|
37
|
+
const result = await this.options.onMutate?.(input.entities, context);
|
|
38
|
+
return result ?? ({} as TMutateResult);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected async onSuccessBase(
|
|
42
|
+
input: BatchMutationInputInternal<TEntityConstructor>,
|
|
43
|
+
onMutateResult: TMutateResult | undefined,
|
|
44
|
+
context: MutationFunctionContext,
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
input.strategy.onSuccess();
|
|
47
|
+
await this.options.onSuccess?.(input.entities, context, onMutateResult);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async onErrorBase(
|
|
51
|
+
error: TError,
|
|
52
|
+
input: BatchMutationInputInternal<TEntityConstructor>,
|
|
53
|
+
onMutateResult: TMutateResult | undefined,
|
|
54
|
+
context: MutationFunctionContext,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
input.strategy.onError();
|
|
57
|
+
await this.options.onError?.(
|
|
58
|
+
error,
|
|
59
|
+
input.entities,
|
|
60
|
+
context,
|
|
61
|
+
onMutateResult,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected async onSettledBase(
|
|
66
|
+
error: TError | null,
|
|
67
|
+
input: BatchMutationInputInternal<TEntityConstructor>,
|
|
68
|
+
onMutateResult: TMutateResult | undefined,
|
|
69
|
+
context: MutationFunctionContext,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
await this.options.onSettled?.(
|
|
72
|
+
input.entities,
|
|
73
|
+
context,
|
|
74
|
+
onMutateResult,
|
|
75
|
+
error,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected createInternalInput(
|
|
80
|
+
entities: InstanceType<TEntityConstructor>[],
|
|
81
|
+
): BatchMutationInputInternal<TEntityConstructor> {
|
|
82
|
+
const dirtyEntities: InstanceType<TEntityConstructor>[] = [];
|
|
83
|
+
|
|
84
|
+
for (const entity of entities) {
|
|
85
|
+
if (!entity.isDirty) {
|
|
86
|
+
console.warn(
|
|
87
|
+
"Entity values has not been changed, mutation is skipped.",
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dirtyEntities.push(entity);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
entities: dirtyEntities,
|
|
98
|
+
strategy: this.createMutationStrategy(dirtyEntities, {
|
|
99
|
+
invalidationStrategy: this.options.invalidationStrategy,
|
|
100
|
+
errorStrategy: this.options.errorStrategy,
|
|
101
|
+
invalidateOnError: this.options.invalidateOnError,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type DefaultError, useMutation } from "@tanstack/react-query";
|
|
2
|
+
import type { EntityConstructorAny } from "../entity";
|
|
3
|
+
import { BatchMutationBase } from "./BatchMutationBase";
|
|
4
|
+
import type {
|
|
5
|
+
BatchMutationInputInternal,
|
|
6
|
+
UseBatchMutationHookOptions,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
export class BatchUpdateMutation<
|
|
10
|
+
TEntityConstructor extends EntityConstructorAny,
|
|
11
|
+
TError = DefaultError,
|
|
12
|
+
TMutateResult = unknown,
|
|
13
|
+
> extends BatchMutationBase<TEntityConstructor, TError, TMutateResult> {
|
|
14
|
+
static readonly mutationPrefix = "__mutation__batch__update__";
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
options: UseBatchMutationHookOptions<
|
|
18
|
+
TEntityConstructor,
|
|
19
|
+
TError,
|
|
20
|
+
TMutateResult
|
|
21
|
+
>,
|
|
22
|
+
) {
|
|
23
|
+
super(BatchUpdateMutation.mutationPrefix, options);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
useMutation() {
|
|
27
|
+
const mutation = useMutation<
|
|
28
|
+
void,
|
|
29
|
+
TError,
|
|
30
|
+
BatchMutationInputInternal<TEntityConstructor>,
|
|
31
|
+
TMutateResult
|
|
32
|
+
>({
|
|
33
|
+
mutationFn: (input) =>
|
|
34
|
+
this.options.mutationFn(input.entities, this.context),
|
|
35
|
+
onMutate: (input, context) => this.onMutateBase(input, context),
|
|
36
|
+
onSuccess: (_, input, onMutateResult, context) =>
|
|
37
|
+
this.onSuccessBase(input, onMutateResult, context),
|
|
38
|
+
onError: (error, input, onMutateResult, context) =>
|
|
39
|
+
this.onErrorBase(error, input, onMutateResult, context),
|
|
40
|
+
onSettled: (_, error, input, onMutateResult, context) =>
|
|
41
|
+
this.onSettledBase(error, input, onMutateResult, context),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (entities: InstanceType<TEntityConstructor>[]) => {
|
|
45
|
+
mutation.mutate(this.createInternalInput(entities));
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|