cabloy 5.1.207 → 5.1.209
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/.cabloy-version +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +9 -9
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +4 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +13 -1
- package/repo-docs/backend/unit-testing.md +1 -1
- package/repo-docs/frontend/form-guide.md +17 -1
- package/repo-docs/frontend/model-architecture.md +4 -0
- package/repo-docs/frontend/model-state-guide.md +23 -2
- package/repo-docs/frontend/resource-entry-page-deep-dive.md +119 -0
- package/repo-docs/frontend/scripts.md +3 -3
- package/repo-docs/frontend/zova-form-under-the-hood.md +5 -0
- package/repo-docs/fullstack/parallel-worktree-environment.md +3 -3
- package/repo-docs/fullstack/quickstart.md +11 -1
- package/repo-docs/reference/repo-scripts.md +19 -5
- package/repo-e2e/specs/cabloy-basic.spec.ts +1577 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/passport.ts +4 -0
- package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +172 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-cli/cli/package.json +2 -2
- package/zova/packages-cli/cli-set-front/cli/templates/rest/render.ts +2 -0
- package/zova/packages-cli/cli-set-front/cli/templates/rest/rest.ts +9 -0
- package/zova/packages-cli/cli-set-front/package.json +1 -1
- package/zova/packages-zova/zova/package.json +2 -2
- package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +17 -17
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +68 -52
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +524 -343
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +45 -11
- package/zova/src/suite/a-home/modules/home-passport/test/lib/persistenceIdentity.test.ts +229 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/package.json +9 -1
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/.metadata/index.ts +81 -5
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/bean/bean.draftCoordinator.ts +158 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockForm/controller.tsx +11 -3
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/component/blockPageEntry/controller.tsx +438 -8
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/config.ts +11 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/en-us.ts +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/config/locale/zh-cn.ts +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/lib/draftSchema.ts +649 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/model/draft.ts +59 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/monkey.ts +11 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/draft.ts +49 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftDate.test.ts +262 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLifecycle.test.ts +1073 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftLoader.mjs +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftModel.test.ts +572 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchema.test.ts +427 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/test/lib/draftSchemaSafety.test.ts +200 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/page/entry/controller.tsx +6 -1
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-form/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/form/controller.tsx +86 -1
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/component/formField/controller.tsx +88 -13
- package/zova/src/suite-vendor/a-zova/modules/a-form/src/lib/schemaRefresh.ts +751 -0
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefresh.test.ts +637 -0
- package/zova/src/suite-vendor/a-zova/modules/a-form/test/lib/schemaRefreshLifecycle.test.ts +994 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +331 -87
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts +11 -6
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts +3 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +17 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/modelPersistence.ts +204 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/lib/persistenceQueue.ts +50 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +5 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/service/storage.ts +47 -2
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +3 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelLoader.mjs +34 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/modelPersistence.test.ts +727 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceQueue.test.ts +137 -0
- package/zova/src/suite-vendor/a-zova/modules/a-model/test/lib/persistenceTransition.test.ts +129 -0
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/monkey.ts +7 -5
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +2 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/bean.router.ts +14 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/lib/routeDiscard.ts +26 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/service/routerGuards.ts +6 -3
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/index.ts +1 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/routeDiscard.ts +24 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/test/lib/routeDiscard.test.ts +105 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +202 -30
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabs.test.ts +524 -0
- package/zova/src/suite-vendor/a-zova/modules/a-routertabs/test/lib/tabsLoader.mjs +35 -0
- package/zova/src/suite-vendor/a-zova/package.json +6 -6
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts
CHANGED
|
@@ -1,90 +1,147 @@
|
|
|
1
1
|
import type { Query, QueryKey } from '@tanstack/vue-query';
|
|
2
2
|
|
|
3
|
+
import { CancelledError, notifyManager } from '@tanstack/query-core';
|
|
3
4
|
import { experimental_createQueryPersister } from '@tanstack/query-persist-client-core';
|
|
5
|
+
import { hashKey } from '@tanstack/vue-query';
|
|
4
6
|
import localforage from 'localforage';
|
|
5
7
|
import { SymbolBeanFullName } from 'zova';
|
|
6
8
|
|
|
9
|
+
import type { ModelPersistenceToken } from '../../lib/modelPersistence.js';
|
|
7
10
|
import type { QueryMetaPersister } from '../../types/index.js';
|
|
8
11
|
|
|
9
12
|
import { CookieWrapper } from '../../common/cookieWrapper.js';
|
|
13
|
+
import { getModelPersistenceStorageKey } from '../../lib/modelPersistence.js';
|
|
14
|
+
import { getPersistenceQueue } from '../../lib/persistenceQueue.js';
|
|
10
15
|
import { createQueryPersister } from '../../lib/queryPersister.js';
|
|
11
16
|
import { resolveMaxAgeTime } from '../../types/index.js';
|
|
12
17
|
import { BeanModelLast } from './bean.model.last.js';
|
|
13
18
|
|
|
14
19
|
export class BeanModelPersister extends BeanModelLast {
|
|
20
|
+
private _persisterRestoreErrors = new Set<string>();
|
|
21
|
+
|
|
22
|
+
$persisterError(queryKey: QueryKey): boolean {
|
|
23
|
+
const query = this.self.$queryFind({ queryKey });
|
|
24
|
+
const options = this._adjustPersisterOptions(query?.meta?.persister);
|
|
25
|
+
return (
|
|
26
|
+
!!options &&
|
|
27
|
+
this._persisterRestoreErrors.has(this._getPersisterStorageKey(options, query, queryKey))
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
private _persisterLoad_inner<T>(
|
|
16
|
-
storage: Storage,
|
|
17
|
-
storageKey: string,
|
|
18
32
|
storedData: any,
|
|
19
33
|
query: Query,
|
|
20
34
|
options: QueryMetaPersister,
|
|
21
|
-
): T
|
|
22
|
-
if (!storedData) return
|
|
35
|
+
): { data?: T; remove: boolean; timestamps?: { dataUpdatedAt: number; errorUpdatedAt: number } } {
|
|
36
|
+
if (!storedData) return { remove: false };
|
|
23
37
|
const persistedQuery = options.deserialize
|
|
24
38
|
? options.deserialize(storedData as string, options.deserializeDefault!)
|
|
25
39
|
: options.deserializeDefault!(storedData as string);
|
|
26
|
-
|
|
27
40
|
if (persistedQuery.state.dataUpdatedAt) {
|
|
28
41
|
const queryAge = Date.now() - persistedQuery.state.dataUpdatedAt;
|
|
29
42
|
const expired = queryAge > (resolveMaxAgeTime(options.maxAge, query) ?? Infinity);
|
|
30
43
|
const busted = persistedQuery.buster !== options.buster;
|
|
31
|
-
if (expired
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
if (!expired && !busted) {
|
|
45
|
+
return {
|
|
46
|
+
data: persistedQuery.state.data as T,
|
|
47
|
+
remove: false,
|
|
48
|
+
timestamps: {
|
|
49
|
+
dataUpdatedAt: persistedQuery.state.dataUpdatedAt,
|
|
50
|
+
errorUpdatedAt: persistedQuery.state.errorUpdatedAt,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
40
53
|
}
|
|
41
|
-
} else {
|
|
42
|
-
storage.removeItem(storageKey);
|
|
43
54
|
}
|
|
55
|
+
return { remove: true };
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
$persisterLoad<T>(queryKey: QueryKey): Promise<T | undefined> | T | undefined {
|
|
59
|
+
const ctx = this.ctx;
|
|
60
|
+
if (ctx?.disposed) return undefined;
|
|
61
|
+
const persistence = this.$modelPersistence;
|
|
47
62
|
const query = this.self.$queryFind({ queryKey });
|
|
48
63
|
if (!query) return undefined;
|
|
49
64
|
const options = this._adjustPersisterOptions(query.meta?.persister);
|
|
50
65
|
if (!options) return undefined;
|
|
51
66
|
const storage = this._getPersisterStorage(options, query);
|
|
52
67
|
if (!storage) return undefined;
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
const token = persistence.capture();
|
|
69
|
+
if (options.storage === 'db') persistence.touch(query);
|
|
70
|
+
const storageKey = this._getPersisterStorageKey(options, query, undefined, token);
|
|
71
|
+
this._persisterRestoreErrors.delete(storageKey);
|
|
72
|
+
const queue = getPersistenceQueue(storage);
|
|
73
|
+
const generation = queue.generation(storageKey);
|
|
74
|
+
const current = () =>
|
|
75
|
+
!ctx?.disposed &&
|
|
76
|
+
generation === queue.generation(storageKey) &&
|
|
77
|
+
(options.storage !== 'db' || persistence.current(token));
|
|
78
|
+
if (!options.sync) {
|
|
79
|
+
return queue
|
|
80
|
+
.enqueue(async () => {
|
|
81
|
+
if (!current()) return undefined;
|
|
82
|
+
const storedData = await storage.getItem(storageKey);
|
|
83
|
+
if (!current()) return undefined;
|
|
84
|
+
try {
|
|
85
|
+
const result = this._persisterLoad_inner<T>(storedData, query, options);
|
|
86
|
+
if (!current()) return undefined;
|
|
87
|
+
if (result.timestamps) query.setState(result.timestamps);
|
|
88
|
+
if (result.remove) await storage.removeItem(storageKey);
|
|
89
|
+
return current() ? result.data : undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
if (!current()) return undefined;
|
|
92
|
+
this._persisterRestoreErrors.add(storageKey);
|
|
93
|
+
await storage.removeItem(storageKey);
|
|
94
|
+
this._reportPersisterError('restore');
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {
|
|
99
|
+
if (current()) {
|
|
100
|
+
this._persisterRestoreErrors.add(storageKey);
|
|
101
|
+
this._reportPersisterError('restore');
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
61
104
|
});
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = this._persisterLoad_inner<T>(storage.getItem(storageKey), query, options);
|
|
108
|
+
if (result.timestamps) query.setState(result.timestamps);
|
|
109
|
+
if (result.remove) storage.removeItem(storageKey);
|
|
110
|
+
return result.data;
|
|
111
|
+
} catch {
|
|
112
|
+
this._persisterRestoreErrors.add(storageKey);
|
|
113
|
+
this._reportPersisterError('restore');
|
|
114
|
+
try {
|
|
115
|
+
storage.removeItem(storageKey);
|
|
116
|
+
} catch {
|
|
117
|
+
this._reportPersisterError('remove');
|
|
62
118
|
}
|
|
63
|
-
} catch (err) {
|
|
64
|
-
if (process.env.DEV) {
|
|
65
|
-
console.error(err);
|
|
66
|
-
console.warn(
|
|
67
|
-
'Encountered an error attempting to restore query cache from persisted location.',
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
storage.removeItem(storageKey);
|
|
71
119
|
}
|
|
72
120
|
}
|
|
73
121
|
|
|
74
|
-
$
|
|
122
|
+
$persisterGeneration(queryKey: QueryKey): string {
|
|
75
123
|
const query = this.self.$queryFind({ queryKey });
|
|
76
|
-
|
|
124
|
+
const options = this._adjustPersisterOptions(query?.meta?.persister);
|
|
125
|
+
if (!options) return '';
|
|
126
|
+
const storage = this._getPersisterStorage(options, query);
|
|
127
|
+
if (!storage) return '';
|
|
128
|
+
const storageKey = this._getPersisterStorageKey(options, query, queryKey);
|
|
129
|
+
return `${options.storage === 'db' ? this.$modelPersistence.epoch : ''}:${storageKey}:${getPersistenceQueue(storage).generation(storageKey)}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
$persisterSave(queryKey: QueryKey): Promise<void> {
|
|
133
|
+
const query = this.self.$queryFind({ queryKey });
|
|
134
|
+
if (!query) return Promise.resolve();
|
|
77
135
|
const options = this._adjustPersisterOptions(query.meta?.persister);
|
|
78
|
-
if (!options) return;
|
|
79
|
-
this._persisterSave(query, options);
|
|
136
|
+
if (!options) return Promise.resolve();
|
|
137
|
+
return this._persisterSave(query, options);
|
|
80
138
|
}
|
|
81
139
|
|
|
82
140
|
$persisterSaveTo(queryKey: QueryKey, storageTarget: 'cookie' | 'local', value?: unknown) {
|
|
83
141
|
const query = this.self.$queryFind({ queryKey });
|
|
84
142
|
if (!query) {
|
|
85
|
-
if (process.env.DEV)
|
|
143
|
+
if (process.env.DEV)
|
|
86
144
|
console.warn('Cannot save persisted query because it was not found', queryKey);
|
|
87
|
-
}
|
|
88
145
|
return;
|
|
89
146
|
}
|
|
90
147
|
const persister =
|
|
@@ -98,72 +155,252 @@ export class BeanModelPersister extends BeanModelLast {
|
|
|
98
155
|
? String(obj?.state?.data ?? '')
|
|
99
156
|
: JSON.stringify(obj?.state?.data),
|
|
100
157
|
});
|
|
101
|
-
if (
|
|
102
|
-
this._persisterSave(query, options, value);
|
|
158
|
+
if (options) this._persisterSave(query, options, value);
|
|
103
159
|
}
|
|
104
160
|
|
|
105
|
-
private
|
|
106
|
-
const storage = this._getPersisterStorage(options, query);
|
|
107
|
-
if (!storage) return;
|
|
108
|
-
const storageKey = this._getPersisterStorageKey(options, query);
|
|
161
|
+
private _serializePersister(query: Query, options: QueryMetaPersister, value?: unknown): string {
|
|
109
162
|
const params = {
|
|
110
163
|
state: value === undefined ? query.state : { ...query.state, data: value },
|
|
111
164
|
queryKey: query.queryKey,
|
|
112
165
|
queryHash: query.queryHash,
|
|
113
166
|
buster: options.buster,
|
|
114
167
|
};
|
|
115
|
-
|
|
168
|
+
return options.serialize
|
|
116
169
|
? options.serialize(params, options.serializeDefault!)
|
|
117
170
|
: options.serializeDefault!(params);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private _persisterSave(
|
|
174
|
+
query: Query,
|
|
175
|
+
options: QueryMetaPersister,
|
|
176
|
+
value?: unknown,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const storage = this._getPersisterStorage(options, query);
|
|
179
|
+
if (!storage) return Promise.resolve();
|
|
180
|
+
if (options.storage === 'db') this.$modelPersistence.touch(query);
|
|
181
|
+
const storageKey = this._getPersisterStorageKey(options, query);
|
|
182
|
+
const queue = getPersistenceQueue(storage);
|
|
183
|
+
queue.invalidate(storageKey);
|
|
184
|
+
let data: string;
|
|
185
|
+
try {
|
|
186
|
+
data = this._serializePersister(query, options, value);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return this._handlePersisterOperation(
|
|
189
|
+
queue.enqueue(() => {
|
|
190
|
+
throw error;
|
|
191
|
+
}),
|
|
192
|
+
'save',
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (options.sync === true && options.storage !== 'db') {
|
|
196
|
+
try {
|
|
123
197
|
storage.setItem(storageKey, data);
|
|
124
|
-
|
|
198
|
+
return Promise.resolve();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return this._handlePersisterOperation(Promise.reject(error), 'save');
|
|
201
|
+
}
|
|
125
202
|
}
|
|
203
|
+
// Accepted explicit writes keep their captured physical identity. The shared
|
|
204
|
+
// queue orders them before a later remove or full-store clear.
|
|
205
|
+
return this._handlePersisterOperation(
|
|
206
|
+
queue.enqueue(async () => {
|
|
207
|
+
await storage.setItem(storageKey, data);
|
|
208
|
+
}),
|
|
209
|
+
'save',
|
|
210
|
+
);
|
|
126
211
|
}
|
|
127
212
|
|
|
128
|
-
$persisterRemove(queryKey: QueryKey) {
|
|
213
|
+
$persisterRemove(queryKey: QueryKey, persister?: QueryMetaPersister): Promise<void> {
|
|
129
214
|
const query = this.self.$queryFind({ queryKey });
|
|
130
|
-
if (!query) return;
|
|
131
|
-
const options = this._adjustPersisterOptions(query
|
|
132
|
-
if (!options) return;
|
|
215
|
+
if (!query && !persister) return Promise.resolve();
|
|
216
|
+
const options = this._adjustPersisterOptions(persister ?? query?.meta?.persister);
|
|
217
|
+
if (!options) return Promise.resolve();
|
|
133
218
|
const storage = this._getPersisterStorage(options, query);
|
|
134
|
-
if (!storage) return;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
219
|
+
if (!storage) return Promise.resolve();
|
|
220
|
+
if (options.storage === 'db') this.$modelPersistence.touch(query);
|
|
221
|
+
const storageKey = this._getPersisterStorageKey(options, query, queryKey);
|
|
222
|
+
const queue = getPersistenceQueue(storage);
|
|
223
|
+
queue.invalidate(storageKey);
|
|
224
|
+
if (options.sync === true && options.storage !== 'db') {
|
|
225
|
+
try {
|
|
141
226
|
storage.removeItem(storageKey);
|
|
142
|
-
|
|
227
|
+
return Promise.resolve();
|
|
228
|
+
} catch (error) {
|
|
229
|
+
return this._handlePersisterOperation(Promise.reject(error), 'remove');
|
|
230
|
+
}
|
|
143
231
|
}
|
|
232
|
+
return this._handlePersisterOperation(
|
|
233
|
+
queue.enqueue(async () => {
|
|
234
|
+
await storage.removeItem(storageKey);
|
|
235
|
+
}),
|
|
236
|
+
'remove',
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async $persisterFlush(): Promise<void> {
|
|
241
|
+
if (process.env.SERVER) return;
|
|
242
|
+
await getPersistenceQueue(localforage).flush();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private _handlePersisterOperation<T>(operation: Promise<T>, action: string): Promise<T> {
|
|
246
|
+
operation.catch(() => this._reportPersisterError(action));
|
|
247
|
+
return operation;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private _reportPersisterError(action: string) {
|
|
251
|
+
if (process.env.DEV)
|
|
252
|
+
console.warn(`Encountered an error attempting to ${action} persisted query cache.`);
|
|
144
253
|
}
|
|
145
254
|
|
|
146
255
|
protected _createPersister(options?: QueryMetaPersister | boolean): any {
|
|
147
256
|
options = this._adjustPersisterOptions(options);
|
|
148
257
|
if (!options) return undefined;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
258
|
+
if (options.storage === 'db') return this._createDbPersister(options);
|
|
259
|
+
const storage = this._getPersisterStorage(options);
|
|
260
|
+
const persisterOptions = options;
|
|
261
|
+
return createQueryPersister((queryFn, context, query) => {
|
|
262
|
+
const queue = storage ? getPersistenceQueue(storage) : undefined;
|
|
263
|
+
const storageKey = `${persisterOptions.prefix}-${query.queryHash}`;
|
|
264
|
+
const generation = queue?.generation(storageKey);
|
|
265
|
+
const current = () => generation === queue?.generation(storageKey);
|
|
266
|
+
const orderedStorage =
|
|
267
|
+
storage && queue && !persisterOptions.sync
|
|
268
|
+
? {
|
|
269
|
+
getItem: (key: string) =>
|
|
270
|
+
queue.enqueue(() => (current() ? storage.getItem(key) : undefined)),
|
|
271
|
+
setItem: (key: string, data: string) =>
|
|
272
|
+
this._handlePersisterOperation(
|
|
273
|
+
queue.enqueue(async () => {
|
|
274
|
+
if (current()) await storage.setItem(key, data);
|
|
275
|
+
}),
|
|
276
|
+
'save',
|
|
277
|
+
),
|
|
278
|
+
removeItem: (key: string) =>
|
|
279
|
+
this._handlePersisterOperation(
|
|
280
|
+
queue.enqueue(async () => {
|
|
281
|
+
if (current()) await storage.removeItem(key);
|
|
282
|
+
}),
|
|
283
|
+
'remove',
|
|
284
|
+
),
|
|
285
|
+
}
|
|
286
|
+
: storage;
|
|
287
|
+
return experimental_createQueryPersister({
|
|
288
|
+
storage: orderedStorage as any,
|
|
289
|
+
maxAge: persisterOptions.maxAge as number,
|
|
290
|
+
refetchOnRestore: persisterOptions.refetchOnRestore,
|
|
291
|
+
prefix: persisterOptions.prefix,
|
|
292
|
+
buster: persisterOptions.buster,
|
|
293
|
+
}).persisterFn(queryFn, context, query);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private _createDbPersister(options: QueryMetaPersister) {
|
|
298
|
+
const ctx = this.ctx;
|
|
299
|
+
const persistence = this.$modelPersistence;
|
|
300
|
+
// Do not use the upstream persister here: its deferred timestamp/refetch/save
|
|
301
|
+
// callbacks can run after a scope transition or owner disposal.
|
|
302
|
+
return async (queryFn, context, query: Query) => {
|
|
303
|
+
if (ctx?.disposed) throw new CancelledError({ silent: true, revert: false });
|
|
304
|
+
persistence.touch(query);
|
|
305
|
+
const token = persistence.capture();
|
|
306
|
+
const storage = this._getPersisterStorage(options, query);
|
|
307
|
+
const queue = storage && getPersistenceQueue(storage);
|
|
308
|
+
const storageKey = this._getPersisterStorageKey(options, query, undefined, token);
|
|
309
|
+
const generation = queue?.generation(storageKey);
|
|
310
|
+
const current = () =>
|
|
311
|
+
!ctx?.disposed &&
|
|
312
|
+
persistence.current(token) &&
|
|
313
|
+
generation === queue?.generation(storageKey);
|
|
314
|
+
const assertCurrent = () => {
|
|
315
|
+
if (!current()) throw new CancelledError({ silent: true, revert: false });
|
|
316
|
+
};
|
|
317
|
+
const bypass =
|
|
318
|
+
(query.state.fetchMeta as { bypassPersister?: boolean } | null)?.bypassPersister === true;
|
|
319
|
+
if (!bypass && storage && queue && query.state.data === undefined) {
|
|
320
|
+
this._persisterRestoreErrors.delete(storageKey);
|
|
321
|
+
const restored = await queue
|
|
322
|
+
.enqueue(async () => {
|
|
323
|
+
if (!current()) return undefined;
|
|
324
|
+
try {
|
|
325
|
+
const storedData = await storage.getItem(storageKey);
|
|
326
|
+
if (!current()) return undefined;
|
|
327
|
+
const result = this._persisterLoad_inner(storedData, query, options);
|
|
328
|
+
if (!current()) return undefined;
|
|
329
|
+
if (result.remove) await storage.removeItem(storageKey);
|
|
330
|
+
return current() ? result : undefined;
|
|
331
|
+
} catch {
|
|
332
|
+
if (current()) {
|
|
333
|
+
this._persisterRestoreErrors.add(storageKey);
|
|
334
|
+
await storage.removeItem(storageKey);
|
|
335
|
+
this._reportPersisterError('restore');
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
.catch(() => {
|
|
341
|
+
if (current()) {
|
|
342
|
+
this._persisterRestoreErrors.add(storageKey);
|
|
343
|
+
this._reportPersisterError('restore');
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
});
|
|
347
|
+
assertCurrent();
|
|
348
|
+
if (restored?.data !== undefined) {
|
|
349
|
+
notifyManager.schedule(() => {
|
|
350
|
+
if (!current()) return;
|
|
351
|
+
if (restored.timestamps) query.setState(restored.timestamps);
|
|
352
|
+
if (
|
|
353
|
+
options.refetchOnRestore === 'always' ||
|
|
354
|
+
(options.refetchOnRestore === true && query.isStale())
|
|
355
|
+
) {
|
|
356
|
+
void query.fetch().catch(() => undefined);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return restored.data;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
assertCurrent();
|
|
363
|
+
let result;
|
|
364
|
+
try {
|
|
365
|
+
result = await queryFn(context);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
assertCurrent();
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
assertCurrent();
|
|
371
|
+
if (!bypass && storage && queue) {
|
|
372
|
+
notifyManager.schedule(() => {
|
|
373
|
+
if (!current()) return;
|
|
374
|
+
let data: string;
|
|
375
|
+
try {
|
|
376
|
+
data = this._serializePersister(query, options, result);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this._handlePersisterOperation(
|
|
379
|
+
queue.enqueue(() => {
|
|
380
|
+
throw error;
|
|
381
|
+
}),
|
|
382
|
+
'save',
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
this._handlePersisterOperation(
|
|
387
|
+
queue.enqueue(async () => {
|
|
388
|
+
if (current()) await storage.setItem(storageKey, data);
|
|
389
|
+
}),
|
|
390
|
+
'save',
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
};
|
|
157
396
|
}
|
|
158
397
|
|
|
159
398
|
protected _adjustPersisterOptions(options?: QueryMetaPersister | boolean) {
|
|
160
399
|
if (options === false) return undefined;
|
|
161
|
-
|
|
162
|
-
options = {};
|
|
163
|
-
} else {
|
|
164
|
-
options = { ...options };
|
|
165
|
-
}
|
|
400
|
+
options = options === undefined || options === true ? {} : { ...options };
|
|
166
401
|
options.storage = options.storage ?? (options.sync ? 'local' : 'db');
|
|
402
|
+
// DB storage is always asynchronous, including custom explicitly synced options.
|
|
403
|
+
if (options.storage === 'db') options.sync = false;
|
|
167
404
|
options.maxAge = options.maxAge ?? this.scopeSelf.config.persister.maxAge[options.storage];
|
|
168
405
|
options.refetchOnRestore =
|
|
169
406
|
options.refetchOnRestore ?? this.scopeSelf.config.persister.refetchOnRestore;
|
|
@@ -174,22 +411,32 @@ export class BeanModelPersister extends BeanModelLast {
|
|
|
174
411
|
return options;
|
|
175
412
|
}
|
|
176
413
|
|
|
177
|
-
protected _getPersisterStorageKey(
|
|
178
|
-
|
|
179
|
-
|
|
414
|
+
protected _getPersisterStorageKey(
|
|
415
|
+
options: QueryMetaPersister,
|
|
416
|
+
query?: Query,
|
|
417
|
+
queryKey?: QueryKey,
|
|
418
|
+
token?: ModelPersistenceToken,
|
|
419
|
+
) {
|
|
420
|
+
const key = query?.queryKey ?? this._forceQueryKeyPrefix(queryKey);
|
|
421
|
+
const logicalKey = options.storageKeySimplify
|
|
422
|
+
? String(key[key.length - 1])
|
|
423
|
+
: `${options.prefix}-${query?.queryHash ?? hashKey(key)}`;
|
|
424
|
+
return options.storage === 'db'
|
|
425
|
+
? getModelPersistenceStorageKey(
|
|
426
|
+
this.sys.env.APP_NAME ?? '',
|
|
427
|
+
(token ?? this.$modelPersistence.capture()).scope,
|
|
428
|
+
logicalKey,
|
|
429
|
+
)
|
|
430
|
+
: logicalKey;
|
|
180
431
|
}
|
|
181
432
|
|
|
182
433
|
protected _getPersisterStorage(options?: QueryMetaPersister | boolean, query?: Query) {
|
|
183
434
|
options = this._adjustPersisterOptions(options);
|
|
184
435
|
if (!options) return undefined;
|
|
185
|
-
// cookie
|
|
186
436
|
if (options.storage === 'cookie')
|
|
187
437
|
return this.bean._newBeanSimple(CookieWrapper, false, options, query);
|
|
188
|
-
// check server
|
|
189
438
|
if (process.env.SERVER) return undefined;
|
|
190
|
-
// local
|
|
191
439
|
if (options.storage === 'local') return localStorage;
|
|
192
|
-
// db
|
|
193
440
|
if (options.storage === 'db') return localforage;
|
|
194
441
|
}
|
|
195
442
|
|
|
@@ -205,9 +452,7 @@ export class BeanModelPersister extends BeanModelLast {
|
|
|
205
452
|
if (!queryKey) queryKey = [];
|
|
206
453
|
if (!this._prefixIsBeanFullName(queryKey[0])) {
|
|
207
454
|
const prefixes = [this[SymbolBeanFullName]];
|
|
208
|
-
if (this.$onionOptions?.enableSelector)
|
|
209
|
-
prefixes.push(this.selector);
|
|
210
|
-
}
|
|
455
|
+
if (this.$onionOptions?.enableSelector) prefixes.push(this.selector);
|
|
211
456
|
queryKey = prefixes.concat(queryKey as any);
|
|
212
457
|
}
|
|
213
458
|
return queryKey;
|
|
@@ -215,6 +460,5 @@ export class BeanModelPersister extends BeanModelLast {
|
|
|
215
460
|
|
|
216
461
|
private _prefixIsBeanFullName(prefix?: any) {
|
|
217
462
|
return prefix === this[SymbolBeanFullName];
|
|
218
|
-
// return (prefix && typeof prefix === 'string' && prefix.split('.').length === 3);
|
|
219
463
|
}
|
|
220
464
|
}
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.query.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { cast } from 'zova';
|
|
|
17
17
|
|
|
18
18
|
import type { MaybeRefDeep, NoUnknown } from '../../common/types.js';
|
|
19
19
|
|
|
20
|
+
import { getPersistenceQueue } from '../../lib/persistenceQueue.js';
|
|
20
21
|
import { BeanModelCookie } from './bean.model.cookie.js';
|
|
21
22
|
|
|
22
23
|
export class BeanModelQuery extends BeanModelCookie {
|
|
@@ -83,12 +84,16 @@ export class BeanModelQuery extends BeanModelCookie {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
async $clear() {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
const queue = getPersistenceQueue(localforage);
|
|
88
|
+
// Invalidate in-flight restores and scheduled persisters before clearing memory.
|
|
89
|
+
queue.invalidateAll();
|
|
90
|
+
const clear = process.env.SERVER
|
|
91
|
+
? Promise.resolve()
|
|
92
|
+
: queue.enqueue(async () => {
|
|
93
|
+
await localforage.clear();
|
|
94
|
+
});
|
|
95
|
+
this.$modelPersistence.clear();
|
|
96
|
+
await clear;
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
$normalizeFilters<T extends {}>(filters?: T): T {
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useQuery.ts
CHANGED
|
@@ -43,7 +43,9 @@ export class BeanModelUseQuery extends BeanModelQuery {
|
|
|
43
43
|
$useQuery(options, queryClient) {
|
|
44
44
|
const queryKey = this.self._forceQueryKeyPrefix(options.queryKey);
|
|
45
45
|
const persister = this._createPersister(options.meta?.persister);
|
|
46
|
-
options =
|
|
46
|
+
options = this.$modelPersistence.guardOptions(
|
|
47
|
+
Object.assign({}, options, { queryKey, persister }),
|
|
48
|
+
);
|
|
47
49
|
// staleTime
|
|
48
50
|
const sync = typeof options.meta?.persister === 'object' && options.meta?.persister?.sync;
|
|
49
51
|
if (sync !== true) {
|
package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts
CHANGED
|
@@ -346,17 +346,33 @@ export class BeanModelUseState extends BeanModelUseQuery {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
private _handleAsyncDataGet(options, queryClient, persister) {
|
|
349
|
+
if (this.ctx?.disposed) return undefined;
|
|
350
|
+
// Track the context even when the disabled Query already contains data.
|
|
351
|
+
// A scope switch must invalidate the custom-ref consumer synchronously.
|
|
352
|
+
void this.$modelPersistence.epoch;
|
|
349
353
|
const query = this.$useStateData(options, queryClient);
|
|
350
354
|
if (query.data !== undefined) return query.data;
|
|
351
355
|
return this._handleAsyncDataGet_inner(options, queryClient, persister);
|
|
352
356
|
}
|
|
353
357
|
|
|
354
358
|
private async _handleAsyncDataGet_inner(options, queryClient, persister) {
|
|
359
|
+
const ctx = this.ctx;
|
|
360
|
+
if (ctx?.disposed) return undefined;
|
|
361
|
+
const persistence = this.$modelPersistence;
|
|
355
362
|
const queryKey = options.queryKey;
|
|
363
|
+
const token = persistence.capture();
|
|
356
364
|
const query = this.$useStateData(options, queryClient);
|
|
357
365
|
if (persister) {
|
|
366
|
+
const generation = this.$persisterGeneration(queryKey);
|
|
358
367
|
const data = await this.$persisterLoad(queryKey);
|
|
359
|
-
if (
|
|
368
|
+
if (
|
|
369
|
+
ctx?.disposed ||
|
|
370
|
+
!persistence.current(token) ||
|
|
371
|
+
generation !== this.$persisterGeneration(queryKey)
|
|
372
|
+
) {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
if (data !== undefined && query.data === undefined) {
|
|
360
376
|
this.$setQueryData(queryKey, data, false);
|
|
361
377
|
}
|
|
362
378
|
}
|