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
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { dehydrate, hydrate, QueryClient, QueryObserver } from '@tanstack/query-core';
|
|
2
|
+
import localforage from 'localforage';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { computed, markRaw, reactive, shallowReactive } from 'vue';
|
|
6
|
+
import { SymbolBeanFullName } from 'zova';
|
|
7
|
+
|
|
8
|
+
import { BeanModelUseState } from '../../src/bean/bean.model/bean.model.useState.js';
|
|
9
|
+
import {
|
|
10
|
+
getModelPersistenceStorageKey,
|
|
11
|
+
ModelPersistenceContext,
|
|
12
|
+
} from '../../src/lib/modelPersistence.js';
|
|
13
|
+
import { getPersistenceQueue } from '../../src/lib/persistenceQueue.js';
|
|
14
|
+
|
|
15
|
+
function deferred<T>() {
|
|
16
|
+
let resolve!: (value: T) => void;
|
|
17
|
+
let reject!: (error: unknown) => void;
|
|
18
|
+
const promise = new Promise<T>((done, fail) => {
|
|
19
|
+
resolve = done;
|
|
20
|
+
reject = fail;
|
|
21
|
+
});
|
|
22
|
+
return { promise, resolve, reject };
|
|
23
|
+
}
|
|
24
|
+
const tick = () => new Promise<void>(resolve => setTimeout(resolve, 5));
|
|
25
|
+
const records = new Map<string, string>();
|
|
26
|
+
let readBarrier: ReturnType<typeof deferred<void>> | undefined;
|
|
27
|
+
let writeBarrier: ReturnType<typeof deferred<void>> | undefined;
|
|
28
|
+
const calls: string[] = [];
|
|
29
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
30
|
+
configurable: true,
|
|
31
|
+
value: {
|
|
32
|
+
getItem: (key: string) => records.get(key),
|
|
33
|
+
setItem: (key: string, value: string) => records.set(key, value),
|
|
34
|
+
removeItem: (key: string) => records.delete(key),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
Object.assign(localforage, {
|
|
38
|
+
async getItem(key: string) {
|
|
39
|
+
calls.push(`get:${key}`);
|
|
40
|
+
const value = records.get(key);
|
|
41
|
+
await readBarrier?.promise;
|
|
42
|
+
return value;
|
|
43
|
+
},
|
|
44
|
+
async setItem(key: string, value: string) {
|
|
45
|
+
calls.push(`set:${key}`);
|
|
46
|
+
await writeBarrier?.promise;
|
|
47
|
+
records.set(key, value);
|
|
48
|
+
},
|
|
49
|
+
async removeItem(key: string) {
|
|
50
|
+
calls.push(`remove:${key}`);
|
|
51
|
+
records.delete(key);
|
|
52
|
+
},
|
|
53
|
+
async clear() {
|
|
54
|
+
calls.push('clear');
|
|
55
|
+
records.clear();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function fixture(appName = 'app') {
|
|
60
|
+
const client = new QueryClient({
|
|
61
|
+
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
|
|
62
|
+
});
|
|
63
|
+
const persistence = new ModelPersistenceContext(client, 'site');
|
|
64
|
+
const model = new BeanModelUseState() as any;
|
|
65
|
+
const ctx = { disposed: false };
|
|
66
|
+
Object.defineProperties(model, {
|
|
67
|
+
ctx: { value: ctx },
|
|
68
|
+
$queryClient: { value: client },
|
|
69
|
+
$modelPersistence: { value: persistence },
|
|
70
|
+
scopeSelf: {
|
|
71
|
+
get() {
|
|
72
|
+
assert.equal(ctx.disposed, false, 'disposed owner must not resolve persistence config');
|
|
73
|
+
return {
|
|
74
|
+
config: {
|
|
75
|
+
persister: { maxAge: { db: Infinity, local: Infinity }, refetchOnRestore: false },
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
sys: { value: { env: { APP_NAME: appName, APP_VERSION: '1' } } },
|
|
81
|
+
[SymbolBeanFullName]: { value: 'test.model.example' },
|
|
82
|
+
});
|
|
83
|
+
function observer(key: string, options: Record<string, any> = {}) {
|
|
84
|
+
const persisterOptions = options.meta?.persister;
|
|
85
|
+
return new QueryObserver(
|
|
86
|
+
client,
|
|
87
|
+
persistence.guardOptions({
|
|
88
|
+
enabled: false,
|
|
89
|
+
...options,
|
|
90
|
+
queryKey: model._forceQueryKeyPrefix([key]),
|
|
91
|
+
persister: model._createPersister(persisterOptions),
|
|
92
|
+
} as any),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
// Exercise the real $useStateData wrapper cache and async-local get/ensure paths
|
|
96
|
+
// with QueryObserver instead of requiring a mounted Vue application container.
|
|
97
|
+
model.$useQuery = options => {
|
|
98
|
+
const logicalKey = options.queryKey.at(-1);
|
|
99
|
+
const observed = observer(logicalKey, options);
|
|
100
|
+
const unsubscribe = observed.subscribe(() => undefined);
|
|
101
|
+
const result = {
|
|
102
|
+
get data() {
|
|
103
|
+
return observed.getCurrentResult().data;
|
|
104
|
+
},
|
|
105
|
+
suspense: () => observed.refetch(),
|
|
106
|
+
};
|
|
107
|
+
void unsubscribe;
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
function storageKey(key: string, options = {}) {
|
|
111
|
+
const query = client.getQueryCache().find({ queryKey: model._forceQueryKeyPrefix([key]) });
|
|
112
|
+
return model._getPersisterStorageKey(model._adjustPersisterOptions(options), query, [key]);
|
|
113
|
+
}
|
|
114
|
+
return { model, client, persistence, observer, storageKey };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reset() {
|
|
118
|
+
records.clear();
|
|
119
|
+
calls.length = 0;
|
|
120
|
+
readBarrier = undefined;
|
|
121
|
+
writeBarrier = undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function persisted(query: any, data: unknown, timestamp = Date.now()) {
|
|
125
|
+
return JSON.stringify({
|
|
126
|
+
state: { data, dataUpdatedAt: timestamp, errorUpdatedAt: 0 },
|
|
127
|
+
queryKey: query.queryKey,
|
|
128
|
+
queryHash: query.queryHash,
|
|
129
|
+
buster: '1',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
test('normalization, immutable capture, reactive epoch, no-op tuples and switch-away/back', async () => {
|
|
134
|
+
const f = fixture();
|
|
135
|
+
try {
|
|
136
|
+
const initial = f.persistence.capture();
|
|
137
|
+
assert.deepEqual(initial.scope, [null, 'site', null]);
|
|
138
|
+
assert.ok(Object.isFrozen(initial));
|
|
139
|
+
assert.ok(Object.isFrozen(initial.scope));
|
|
140
|
+
const epoch = computed(() => f.persistence.epoch);
|
|
141
|
+
assert.equal(epoch.value, 0);
|
|
142
|
+
let changes = 0;
|
|
143
|
+
const stop = f.persistence.subscribe(() => changes++);
|
|
144
|
+
f.persistence.update({ instanceId: 0, userId: 0 }, { initial: true });
|
|
145
|
+
const token = f.persistence.capture();
|
|
146
|
+
assert.deepEqual(token.scope, ['0', null, '0']);
|
|
147
|
+
assert.equal(epoch.value, 1);
|
|
148
|
+
f.persistence.update({ instanceId: '0', userId: '0' });
|
|
149
|
+
assert.equal(f.persistence.current(token), true);
|
|
150
|
+
assert.equal(changes, 1);
|
|
151
|
+
f.persistence.update({ instanceId: 1, siteId: null, userId: 0 });
|
|
152
|
+
f.persistence.update({ instanceId: 0, siteId: 'site', userId: 0 });
|
|
153
|
+
assert.equal(f.persistence.current(token), false);
|
|
154
|
+
stop();
|
|
155
|
+
} finally {
|
|
156
|
+
f.client.clear();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('all DB physical paths isolate app, instance, site, user and custom/simplified logical keys', async () => {
|
|
161
|
+
reset();
|
|
162
|
+
const f = fixture();
|
|
163
|
+
const other = fixture('other');
|
|
164
|
+
try {
|
|
165
|
+
const a = f.observer('draft', {
|
|
166
|
+
meta: { persister: { prefix: 'custom', storageKeySimplify: true } },
|
|
167
|
+
});
|
|
168
|
+
const query = a.getCurrentQuery();
|
|
169
|
+
query.setData('anonymous');
|
|
170
|
+
await f.model.$persisterSave(['draft']);
|
|
171
|
+
const anonymous = f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true });
|
|
172
|
+
assert.deepEqual(JSON.parse(anonymous), [
|
|
173
|
+
'zova-model-db',
|
|
174
|
+
1,
|
|
175
|
+
'app',
|
|
176
|
+
[null, 'site', null],
|
|
177
|
+
'draft',
|
|
178
|
+
]);
|
|
179
|
+
const keys = new Set([anonymous]);
|
|
180
|
+
for (const identity of [
|
|
181
|
+
{ instanceId: 'i', siteId: 'site', userId: 'u' },
|
|
182
|
+
{ instanceId: 'other', siteId: 'site', userId: 'u' },
|
|
183
|
+
{ instanceId: 'other', siteId: 'other', userId: 'u' },
|
|
184
|
+
{ instanceId: 'other', siteId: 'other', userId: 'other' },
|
|
185
|
+
]) {
|
|
186
|
+
f.persistence.update(identity);
|
|
187
|
+
assert.equal(query.state.data, undefined);
|
|
188
|
+
assert.equal(await f.model.$persisterLoad(['draft']), undefined);
|
|
189
|
+
query.setData(JSON.stringify(identity));
|
|
190
|
+
await f.model.$persisterSave(['draft']);
|
|
191
|
+
keys.add(f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true }));
|
|
192
|
+
}
|
|
193
|
+
assert.equal(keys.size, 5);
|
|
194
|
+
assert.equal(records.size, 5);
|
|
195
|
+
other.observer('draft', {
|
|
196
|
+
meta: { persister: { prefix: 'custom', storageKeySimplify: true } },
|
|
197
|
+
});
|
|
198
|
+
assert.equal(await other.model.$persisterLoad(['draft']), undefined);
|
|
199
|
+
const physical = f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true });
|
|
200
|
+
const generation = f.model.$persisterGeneration(['draft']);
|
|
201
|
+
assert.ok(generation.includes(physical));
|
|
202
|
+
records.set(physical, 'bad-json');
|
|
203
|
+
assert.equal(await f.model.$persisterLoad(['draft']), undefined);
|
|
204
|
+
assert.equal(f.model.$persisterError(['draft']), true);
|
|
205
|
+
f.persistence.update({ instanceId: 'another', siteId: 'other', userId: 'other' });
|
|
206
|
+
assert.equal(f.model.$persisterError(['draft']), false);
|
|
207
|
+
f.persistence.update({ instanceId: 'other', siteId: 'other', userId: 'other' });
|
|
208
|
+
query.setData('remove me');
|
|
209
|
+
await f.model.$persisterSave(['draft']);
|
|
210
|
+
f.client.removeQueries({ queryKey: query.queryKey });
|
|
211
|
+
await f.model.$persisterRemove(['draft'], { prefix: 'custom', storageKeySimplify: true });
|
|
212
|
+
assert.equal(records.has(physical), false);
|
|
213
|
+
assert.equal(records.has(anonymous), true);
|
|
214
|
+
} finally {
|
|
215
|
+
f.client.clear();
|
|
216
|
+
other.client.clear();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('scope reset keeps Query/observers but empties canonical state; old initial and placeholder options stay gated', async () => {
|
|
221
|
+
reset();
|
|
222
|
+
const f = fixture();
|
|
223
|
+
try {
|
|
224
|
+
const observer = f.observer('private', {
|
|
225
|
+
initialData: 'initial-user',
|
|
226
|
+
initialDataUpdatedAt: 123,
|
|
227
|
+
placeholderData: previous => previous ?? 'private-placeholder',
|
|
228
|
+
});
|
|
229
|
+
const options = observer.options;
|
|
230
|
+
const query = observer.getCurrentQuery();
|
|
231
|
+
const unsubscribe = observer.subscribe(() => undefined);
|
|
232
|
+
query.setState({ error: new Error('old'), errorUpdatedAt: 123, fetchFailureCount: 3 });
|
|
233
|
+
f.persistence.update({ instanceId: 'i', userId: 'u' });
|
|
234
|
+
assert.equal(observer.getCurrentQuery(), query);
|
|
235
|
+
assert.equal(query.getObserversCount(), 1);
|
|
236
|
+
assert.equal(query.state.data, undefined);
|
|
237
|
+
assert.equal(query.state.error, null);
|
|
238
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
239
|
+
assert.equal(query.state.dataUpdateCount, 0);
|
|
240
|
+
assert.equal(query.state.errorUpdateCount, 0);
|
|
241
|
+
assert.equal(query.state.fetchFailureCount, 0);
|
|
242
|
+
assert.equal(query.state.fetchMeta, null);
|
|
243
|
+
assert.equal(query.state.status, 'pending');
|
|
244
|
+
assert.equal(query.state.fetchStatus, 'idle');
|
|
245
|
+
observer.setOptions(options);
|
|
246
|
+
assert.equal(observer.getCurrentResult().data, undefined);
|
|
247
|
+
assert.equal(query.state.data, undefined);
|
|
248
|
+
unsubscribe();
|
|
249
|
+
} finally {
|
|
250
|
+
f.client.clear();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('only actually hydrated Queries adopt first provider identity; non-DB and logical keys unchanged', async () => {
|
|
255
|
+
reset();
|
|
256
|
+
const f = fixture();
|
|
257
|
+
const server = new QueryClient();
|
|
258
|
+
try {
|
|
259
|
+
server.setQueryData(['test.model.example', 'hydrated'], 'SSR');
|
|
260
|
+
const state = dehydrate(server);
|
|
261
|
+
hydrate(f.client, state);
|
|
262
|
+
const hydrated = f.client.getQueryCache().get(state.queries[0].queryHash)!;
|
|
263
|
+
f.persistence.markHydrated([hydrated]);
|
|
264
|
+
const ordinary = f.observer('ordinary', { initialData: 'null-scope' }).getCurrentQuery();
|
|
265
|
+
const local = f
|
|
266
|
+
.observer('local', { initialData: 'local', meta: { persister: { sync: true } } })
|
|
267
|
+
.getCurrentQuery();
|
|
268
|
+
const mem = f
|
|
269
|
+
.observer('mem', { initialData: 'mem', meta: { persister: false } })
|
|
270
|
+
.getCurrentQuery();
|
|
271
|
+
const implicit = f
|
|
272
|
+
.observer('implicit', { initialData: 'db', meta: { persister: true } })
|
|
273
|
+
.getCurrentQuery();
|
|
274
|
+
f.persistence.update({ instanceId: 'i', userId: 'u' }, { initial: true });
|
|
275
|
+
assert.equal(hydrated.state.data, 'SSR');
|
|
276
|
+
assert.equal(ordinary.state.data, undefined);
|
|
277
|
+
assert.equal(implicit.state.data, undefined);
|
|
278
|
+
assert.equal(local.state.data, 'local');
|
|
279
|
+
assert.equal(mem.state.data, 'mem');
|
|
280
|
+
assert.deepEqual(hydrated.queryKey, ['test.model.example', 'hydrated']);
|
|
281
|
+
f.persistence.update({ instanceId: 'i', userId: 'b' }, { initial: true });
|
|
282
|
+
assert.equal(hydrated.state.data, undefined);
|
|
283
|
+
} finally {
|
|
284
|
+
f.client.clear();
|
|
285
|
+
server.clear();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('automatic persistence restores/saves custom simplified keys, maxAge and buster', async () => {
|
|
290
|
+
reset();
|
|
291
|
+
const f = fixture();
|
|
292
|
+
try {
|
|
293
|
+
let network = 0;
|
|
294
|
+
const observer = f.observer('draft', {
|
|
295
|
+
queryFn: async () => `network-${++network}`,
|
|
296
|
+
meta: { persister: { prefix: 'custom', storageKeySimplify: true } },
|
|
297
|
+
});
|
|
298
|
+
const query = observer.getCurrentQuery();
|
|
299
|
+
const key = f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true });
|
|
300
|
+
records.set(key, persisted(query, 'stored', Date.now() - 100));
|
|
301
|
+
assert.equal((await observer.refetch()).data, 'stored');
|
|
302
|
+
assert.equal(network, 0);
|
|
303
|
+
await tick();
|
|
304
|
+
assert.ok(query.state.dataUpdatedAt < Date.now());
|
|
305
|
+
f.persistence.update({ userId: 'u' });
|
|
306
|
+
assert.equal((await observer.refetch()).data, 'network-1');
|
|
307
|
+
await tick();
|
|
308
|
+
await f.model.$persisterFlush();
|
|
309
|
+
assert.equal(
|
|
310
|
+
JSON.parse(
|
|
311
|
+
records.get(f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true }))!,
|
|
312
|
+
).state.data,
|
|
313
|
+
'network-1',
|
|
314
|
+
);
|
|
315
|
+
assert.equal(JSON.parse(records.get(key)!).state.data, 'stored');
|
|
316
|
+
f.persistence.update({ userId: 'b' });
|
|
317
|
+
const bkey = f.storageKey('draft', { prefix: 'custom', storageKeySimplify: true });
|
|
318
|
+
records.set(
|
|
319
|
+
bkey,
|
|
320
|
+
JSON.stringify({ state: { data: 'busted', dataUpdatedAt: Date.now() }, buster: 'bad' }),
|
|
321
|
+
);
|
|
322
|
+
assert.equal((await observer.refetch()).data, 'network-2');
|
|
323
|
+
} finally {
|
|
324
|
+
await tick();
|
|
325
|
+
f.client.clear();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('stale explicit restore cannot change timestamps/defaults in a different epoch', async () => {
|
|
330
|
+
reset();
|
|
331
|
+
const f = fixture();
|
|
332
|
+
try {
|
|
333
|
+
const observer = f.observer('draft');
|
|
334
|
+
const query = observer.getCurrentQuery();
|
|
335
|
+
records.set(f.storageKey('draft'), persisted(query, 'anonymous', 123));
|
|
336
|
+
readBarrier = deferred<void>();
|
|
337
|
+
const load = f.model.$persisterLoad(['draft']);
|
|
338
|
+
await Promise.resolve();
|
|
339
|
+
f.persistence.update({ userId: 'u' });
|
|
340
|
+
readBarrier.resolve();
|
|
341
|
+
assert.equal(await load, undefined);
|
|
342
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
343
|
+
assert.equal(query.state.data, undefined);
|
|
344
|
+
} finally {
|
|
345
|
+
readBarrier = undefined;
|
|
346
|
+
f.client.clear();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('active QueryObservers refetch new identity; ignored abort old successes and rejections never publish or persist', async () => {
|
|
351
|
+
for (const rejectOld of [false, true]) {
|
|
352
|
+
reset();
|
|
353
|
+
const f = fixture();
|
|
354
|
+
const old = deferred<string>();
|
|
355
|
+
let calls = 0;
|
|
356
|
+
try {
|
|
357
|
+
const observer = f.observer('remote', {
|
|
358
|
+
enabled: true,
|
|
359
|
+
queryFn: () => (++calls === 1 ? old.promise : Promise.resolve(`new-${calls}`)),
|
|
360
|
+
});
|
|
361
|
+
const unsubscribe = observer.subscribe(() => undefined);
|
|
362
|
+
const query = observer.getCurrentQuery();
|
|
363
|
+
await tick();
|
|
364
|
+
f.persistence.update({ instanceId: 'i', userId: 'u' });
|
|
365
|
+
assert.equal(query.state.data, undefined);
|
|
366
|
+
await tick();
|
|
367
|
+
assert.equal(query.state.data, 'new-2');
|
|
368
|
+
if (rejectOld) old.reject(new Error('obsolete'));
|
|
369
|
+
else old.resolve('obsolete');
|
|
370
|
+
await tick();
|
|
371
|
+
await f.model.$persisterFlush();
|
|
372
|
+
assert.equal(query.state.data, 'new-2');
|
|
373
|
+
assert.equal(query.state.error, null);
|
|
374
|
+
assert.equal(query.state.status, 'success');
|
|
375
|
+
assert.equal(
|
|
376
|
+
[...records.values()].some(value => value.includes('obsolete')),
|
|
377
|
+
false,
|
|
378
|
+
);
|
|
379
|
+
unsubscribe();
|
|
380
|
+
} finally {
|
|
381
|
+
f.client.clear();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test('bypass network results are also scope guarded and never persisted', async () => {
|
|
387
|
+
reset();
|
|
388
|
+
const f = fixture();
|
|
389
|
+
const old = deferred<string>();
|
|
390
|
+
try {
|
|
391
|
+
const observer = f.observer('remote', { queryFn: () => old.promise });
|
|
392
|
+
const query = observer.getCurrentQuery();
|
|
393
|
+
const fetch = query
|
|
394
|
+
.fetch(undefined, { meta: { bypassPersister: true } as any })
|
|
395
|
+
.catch(() => undefined);
|
|
396
|
+
f.persistence.update({ userId: 'u' });
|
|
397
|
+
old.resolve('old');
|
|
398
|
+
await fetch;
|
|
399
|
+
await tick();
|
|
400
|
+
assert.equal(query.state.data, undefined);
|
|
401
|
+
assert.equal(records.size, 0);
|
|
402
|
+
assert.equal(calls.length, 0);
|
|
403
|
+
} finally {
|
|
404
|
+
f.client.clear();
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('global clear drains accepted saves across all scopes and invalidates stale automatic callbacks', async () => {
|
|
409
|
+
reset();
|
|
410
|
+
const f = fixture();
|
|
411
|
+
const remote = deferred<string>();
|
|
412
|
+
try {
|
|
413
|
+
const draft = f.observer('draft').getCurrentQuery();
|
|
414
|
+
draft.setData('accepted');
|
|
415
|
+
writeBarrier = deferred<void>();
|
|
416
|
+
const save = f.model.$persisterSave(['draft']);
|
|
417
|
+
await Promise.resolve();
|
|
418
|
+
f.persistence.update({ userId: 'u' });
|
|
419
|
+
records.set('legacy-and-other-scope', 'other');
|
|
420
|
+
const observer = f.observer('remote', { queryFn: () => remote.promise });
|
|
421
|
+
const fetch = observer.refetch();
|
|
422
|
+
const clear = f.model.$clear();
|
|
423
|
+
writeBarrier.resolve();
|
|
424
|
+
remote.resolve('late');
|
|
425
|
+
await Promise.all([save, clear, fetch]);
|
|
426
|
+
await tick();
|
|
427
|
+
await f.model.$persisterFlush();
|
|
428
|
+
assert.equal(records.size, 0);
|
|
429
|
+
assert.equal(draft.state.data, undefined);
|
|
430
|
+
assert.equal(observer.getCurrentQuery().state.data, undefined);
|
|
431
|
+
assert.ok(calls.findIndex(value => value.startsWith('set:')) < calls.indexOf('clear'));
|
|
432
|
+
} finally {
|
|
433
|
+
writeBarrier = undefined;
|
|
434
|
+
f.client.clear();
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('async-local cached wrapper reacquires new scope and stale loads never install defaults', async () => {
|
|
439
|
+
reset();
|
|
440
|
+
const f = fixture();
|
|
441
|
+
const options = {
|
|
442
|
+
queryKey: ['draft'],
|
|
443
|
+
enabled: false,
|
|
444
|
+
meta: {
|
|
445
|
+
disableSuspenseOnInit: true,
|
|
446
|
+
defaultData: () => ({ text: 'default' }),
|
|
447
|
+
persister: { storage: 'db' },
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
try {
|
|
451
|
+
const wrapper = f.model.$useStateData(options);
|
|
452
|
+
const query = f.model.$queryFind({ queryKey: ['draft'] });
|
|
453
|
+
records.set(f.storageKey('draft'), persisted(query, { text: 'old' }));
|
|
454
|
+
readBarrier = deferred<void>();
|
|
455
|
+
const pending = f.model._handleAsyncDataGet(options, undefined, true);
|
|
456
|
+
await Promise.resolve();
|
|
457
|
+
const value = computed(() => f.model._handleAsyncDataGet(options, undefined, true));
|
|
458
|
+
void value.value;
|
|
459
|
+
f.persistence.update({ instanceId: 'i', userId: 'a' });
|
|
460
|
+
const nextKey = f.storageKey('draft');
|
|
461
|
+
records.set(nextKey, persisted(query, { text: 'new' }));
|
|
462
|
+
readBarrier.resolve();
|
|
463
|
+
assert.equal(await pending, undefined);
|
|
464
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
465
|
+
assert.equal(f.model.$useStateData(options), wrapper);
|
|
466
|
+
assert.deepEqual(await value.value, { text: 'new' });
|
|
467
|
+
assert.deepEqual(wrapper.data, { text: 'new' });
|
|
468
|
+
assert.equal(JSON.parse(records.get(nextKey)!).state.data.text, 'new');
|
|
469
|
+
} finally {
|
|
470
|
+
readBarrier = undefined;
|
|
471
|
+
f.client.clear();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('disposed async-local owners abandon restores before config, timestamps and defaults', async () => {
|
|
476
|
+
reset();
|
|
477
|
+
const f = fixture();
|
|
478
|
+
let defaults = 0;
|
|
479
|
+
const options = {
|
|
480
|
+
queryKey: ['disposed'],
|
|
481
|
+
enabled: false,
|
|
482
|
+
meta: {
|
|
483
|
+
disableSuspenseOnInit: true,
|
|
484
|
+
defaultData: () => {
|
|
485
|
+
defaults++;
|
|
486
|
+
return [];
|
|
487
|
+
},
|
|
488
|
+
persister: { storage: 'db' },
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
try {
|
|
492
|
+
f.model.$useStateData(options);
|
|
493
|
+
const query = f.model.$queryFind({ queryKey: ['disposed'] });
|
|
494
|
+
records.set(f.storageKey('disposed'), persisted(query, ['old tabs'], 123));
|
|
495
|
+
readBarrier = deferred<void>();
|
|
496
|
+
const pending = f.model._handleAsyncDataGet(options, undefined, true);
|
|
497
|
+
await tick();
|
|
498
|
+
f.model.ctx.disposed = true;
|
|
499
|
+
readBarrier.resolve();
|
|
500
|
+
assert.equal(await pending, undefined);
|
|
501
|
+
assert.equal(query.state.data, undefined);
|
|
502
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
503
|
+
assert.equal(defaults, 0);
|
|
504
|
+
const reads = calls.length;
|
|
505
|
+
assert.equal(await f.model._handleAsyncDataGet(options, undefined, true), undefined);
|
|
506
|
+
assert.equal(calls.length, reads);
|
|
507
|
+
} finally {
|
|
508
|
+
readBarrier = undefined;
|
|
509
|
+
f.client.clear();
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test('disposed automatic DB restores cannot update timestamps or start restore refetch', async () => {
|
|
514
|
+
reset();
|
|
515
|
+
const f = fixture();
|
|
516
|
+
let network = 0;
|
|
517
|
+
try {
|
|
518
|
+
const observer = f.observer('disposed-remote', {
|
|
519
|
+
queryFn: async () => `network-${++network}`,
|
|
520
|
+
meta: { persister: { refetchOnRestore: 'always' } },
|
|
521
|
+
});
|
|
522
|
+
const query = observer.getCurrentQuery();
|
|
523
|
+
records.set(f.storageKey('disposed-remote'), persisted(query, 'old', 123));
|
|
524
|
+
readBarrier = deferred<void>();
|
|
525
|
+
const pending = observer.refetch();
|
|
526
|
+
await tick();
|
|
527
|
+
f.model.ctx.disposed = true;
|
|
528
|
+
readBarrier.resolve();
|
|
529
|
+
await pending;
|
|
530
|
+
await tick();
|
|
531
|
+
assert.equal(network, 0);
|
|
532
|
+
assert.equal(query.state.data, undefined);
|
|
533
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
534
|
+
assert.equal(calls.filter(call => call.startsWith('set:')).length, 0);
|
|
535
|
+
} finally {
|
|
536
|
+
readBarrier = undefined;
|
|
537
|
+
f.client.clear();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('disposed owners cannot publish pending network successes or failures', async () => {
|
|
542
|
+
reset();
|
|
543
|
+
for (const fails of [false, true]) {
|
|
544
|
+
const f = fixture();
|
|
545
|
+
const response = deferred<string>();
|
|
546
|
+
let started = false;
|
|
547
|
+
try {
|
|
548
|
+
const observer = f.observer('network', {
|
|
549
|
+
queryFn: () => {
|
|
550
|
+
started = true;
|
|
551
|
+
return response.promise;
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
const pending = observer.refetch();
|
|
555
|
+
await tick();
|
|
556
|
+
assert.equal(started, true);
|
|
557
|
+
f.model.ctx.disposed = true;
|
|
558
|
+
if (fails) response.reject(new Error('late network failure'));
|
|
559
|
+
else response.resolve('private result');
|
|
560
|
+
await pending;
|
|
561
|
+
await tick();
|
|
562
|
+
const query = observer.getCurrentQuery();
|
|
563
|
+
assert.equal(query.state.data, undefined);
|
|
564
|
+
assert.equal(query.state.error, null);
|
|
565
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
566
|
+
assert.equal(calls.filter(call => call.startsWith('set:')).length, 0);
|
|
567
|
+
} finally {
|
|
568
|
+
response.resolve('private result');
|
|
569
|
+
f.client.clear();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test('disposed owners suppress deferred restore callbacks and automatic saves without dropping accepted writes', async () => {
|
|
575
|
+
reset();
|
|
576
|
+
const f = fixture();
|
|
577
|
+
const saved = fixture('accepted');
|
|
578
|
+
const remote = fixture('remote');
|
|
579
|
+
let network = 0;
|
|
580
|
+
try {
|
|
581
|
+
const observer = f.observer('restored', {
|
|
582
|
+
queryFn: async () => `network-${++network}`,
|
|
583
|
+
meta: { persister: { refetchOnRestore: 'always' } },
|
|
584
|
+
});
|
|
585
|
+
const query = observer.getCurrentQuery();
|
|
586
|
+
records.set(f.storageKey('restored'), persisted(query, 'restored', 123));
|
|
587
|
+
assert.equal((await observer.refetch()).data, 'restored');
|
|
588
|
+
f.model.ctx.disposed = true;
|
|
589
|
+
await tick();
|
|
590
|
+
assert.equal(network, 0);
|
|
591
|
+
assert.notEqual(query.state.dataUpdatedAt, 123);
|
|
592
|
+
|
|
593
|
+
const saving = remote.observer('automatic', { queryFn: async () => 'fresh' });
|
|
594
|
+
assert.equal((await saving.refetch()).data, 'fresh');
|
|
595
|
+
const automaticKey = remote.storageKey('automatic');
|
|
596
|
+
remote.model.ctx.disposed = true;
|
|
597
|
+
await tick();
|
|
598
|
+
assert.equal(records.has(automaticKey), false);
|
|
599
|
+
|
|
600
|
+
saved.observer('explicit');
|
|
601
|
+
saved.model.$setQueryData(['explicit'], 'accepted', false);
|
|
602
|
+
const key = saved.storageKey('explicit');
|
|
603
|
+
writeBarrier = deferred<void>();
|
|
604
|
+
const accepted = saved.model.$persisterSave(['explicit']);
|
|
605
|
+
saved.model.ctx.disposed = true;
|
|
606
|
+
writeBarrier.resolve();
|
|
607
|
+
await accepted;
|
|
608
|
+
assert.equal(JSON.parse(records.get(key)!).state.data, 'accepted');
|
|
609
|
+
} finally {
|
|
610
|
+
writeBarrier = undefined;
|
|
611
|
+
f.client.clear();
|
|
612
|
+
saved.client.clear();
|
|
613
|
+
remote.client.clear();
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test('deferred restore timestamps/refetch and automatic save cannot escape an epoch or clear', async () => {
|
|
618
|
+
reset();
|
|
619
|
+
const f = fixture();
|
|
620
|
+
try {
|
|
621
|
+
let network = 0;
|
|
622
|
+
const observer = f.observer('remote', {
|
|
623
|
+
queryFn: async () => `network-${++network}`,
|
|
624
|
+
meta: { persister: { refetchOnRestore: 'always' } },
|
|
625
|
+
});
|
|
626
|
+
const query = observer.getCurrentQuery();
|
|
627
|
+
records.set(f.storageKey('remote'), persisted(query, 'restored', 123));
|
|
628
|
+
assert.equal((await observer.refetch()).data, 'restored');
|
|
629
|
+
f.persistence.update({ userId: 'next' });
|
|
630
|
+
await tick();
|
|
631
|
+
assert.equal(network, 0);
|
|
632
|
+
assert.equal(query.state.dataUpdatedAt, 0);
|
|
633
|
+
assert.equal(query.state.data, undefined);
|
|
634
|
+
assert.equal((await observer.refetch()).data, 'network-1');
|
|
635
|
+
// The scheduled persistence callback has not run yet.
|
|
636
|
+
const clear = f.model.$clear();
|
|
637
|
+
await clear;
|
|
638
|
+
await tick();
|
|
639
|
+
await f.model.$persisterFlush();
|
|
640
|
+
assert.equal(records.size, 0);
|
|
641
|
+
assert.equal(query.state.data, undefined);
|
|
642
|
+
} finally {
|
|
643
|
+
f.client.clear();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test('retry cancellation blocks old attempts and explicit removal invalidates pending restores', async () => {
|
|
648
|
+
reset();
|
|
649
|
+
const f = fixture();
|
|
650
|
+
try {
|
|
651
|
+
let attempts = 0;
|
|
652
|
+
const observer = f.observer('retry', {
|
|
653
|
+
retry: 3,
|
|
654
|
+
retryDelay: 50,
|
|
655
|
+
queryFn: async () => {
|
|
656
|
+
attempts++;
|
|
657
|
+
throw new Error('temporary');
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
const fetching = observer.refetch();
|
|
661
|
+
await tick();
|
|
662
|
+
assert.equal(attempts, 1);
|
|
663
|
+
f.persistence.update({ userId: 'next' });
|
|
664
|
+
await fetching;
|
|
665
|
+
await new Promise(resolve => setTimeout(resolve, 60));
|
|
666
|
+
assert.equal(attempts, 1);
|
|
667
|
+
assert.equal(observer.getCurrentQuery().state.error, null);
|
|
668
|
+
const draft = f.observer('draft').getCurrentQuery();
|
|
669
|
+
const key = f.storageKey('draft');
|
|
670
|
+
records.set(key, persisted(draft, 'old'));
|
|
671
|
+
readBarrier = deferred<void>();
|
|
672
|
+
const loading = f.model.$persisterLoad(['draft']);
|
|
673
|
+
await Promise.resolve();
|
|
674
|
+
const removing = f.model.$persisterRemove(['draft']);
|
|
675
|
+
readBarrier.resolve();
|
|
676
|
+
assert.equal(await loading, undefined);
|
|
677
|
+
await removing;
|
|
678
|
+
assert.equal(records.has(key), false);
|
|
679
|
+
} finally {
|
|
680
|
+
readBarrier = undefined;
|
|
681
|
+
f.client.clear();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test('reactive context preserves frozen scope identity and exposes epoch invalidation', () => {
|
|
686
|
+
const client = markRaw(new QueryClient());
|
|
687
|
+
try {
|
|
688
|
+
for (const wrap of [reactive, shallowReactive]) {
|
|
689
|
+
const context = wrap(new ModelPersistenceContext(client, 'site'));
|
|
690
|
+
const token = context.capture();
|
|
691
|
+
assert.equal(context.current(token), true);
|
|
692
|
+
context.update({ siteId: undefined });
|
|
693
|
+
assert.deepEqual(context.capture().scope, [null, null, null]);
|
|
694
|
+
assert.equal(context.current(token), false);
|
|
695
|
+
const current = context.capture();
|
|
696
|
+
context.update({ siteId: null });
|
|
697
|
+
assert.equal(context.current(current), true);
|
|
698
|
+
}
|
|
699
|
+
} finally {
|
|
700
|
+
client.clear();
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('DB work before provider registration prevents hydrated Query scope adoption', async () => {
|
|
705
|
+
reset();
|
|
706
|
+
const f = fixture();
|
|
707
|
+
try {
|
|
708
|
+
const query = f.observer('hydrated', { initialData: 'SSR' }).getCurrentQuery();
|
|
709
|
+
f.persistence.markHydrated([query]);
|
|
710
|
+
await f.model.$persisterSave(['hydrated']);
|
|
711
|
+
const oldKey = f.storageKey('hydrated');
|
|
712
|
+
f.persistence.update({ instanceId: 'i', siteId: 'site', userId: 'u' }, { initial: true });
|
|
713
|
+
assert.equal(query.state.data, undefined);
|
|
714
|
+
assert.equal(JSON.parse(records.get(oldKey)!).state.data, 'SSR');
|
|
715
|
+
assert.equal(await f.model.$persisterLoad(['hydrated']), undefined);
|
|
716
|
+
} finally {
|
|
717
|
+
f.client.clear();
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
test('storage queue identity remains the shared localforage singleton', () => {
|
|
722
|
+
assert.equal(getPersistenceQueue(localforage), getPersistenceQueue(localforage));
|
|
723
|
+
assert.notEqual(
|
|
724
|
+
getModelPersistenceStorageKey('a', [null, null, null], 'x'),
|
|
725
|
+
getModelPersistenceStorageKey('b', [null, null, null], 'x'),
|
|
726
|
+
);
|
|
727
|
+
});
|