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,13 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { registerHooks } from 'node:module';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
registerHooks({
|
|
6
|
+
resolve(specifier, context, nextResolve) {
|
|
7
|
+
if (specifier.startsWith('.') && specifier.endsWith('.js') && context.parentURL) {
|
|
8
|
+
const target = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
9
|
+
if (existsSync(fileURLToPath(target))) return nextResolve(target.href, context);
|
|
10
|
+
}
|
|
11
|
+
return nextResolve(specifier, context);
|
|
12
|
+
},
|
|
13
|
+
});
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { stripTypeScriptTypes } from 'node:module';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
function deferred() {
|
|
7
|
+
let resolve!: () => void;
|
|
8
|
+
const promise = new Promise<void>(done => {
|
|
9
|
+
resolve = done;
|
|
10
|
+
});
|
|
11
|
+
return { promise, resolve };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function createRuntime(options: { load?: Promise<void>; write?: Promise<void> } = {}) {
|
|
15
|
+
const database = new Map<string, any>();
|
|
16
|
+
const owners = new Map<string, any>();
|
|
17
|
+
const accesses = {
|
|
18
|
+
localState: 0,
|
|
19
|
+
selector: 0,
|
|
20
|
+
storage: 0,
|
|
21
|
+
discardListener: 0,
|
|
22
|
+
discardCleanup: 0,
|
|
23
|
+
};
|
|
24
|
+
const discardListeners = new Set<(event: any, next: () => void) => void>();
|
|
25
|
+
let queue: Promise<void> = Promise.resolve();
|
|
26
|
+
let failRemove = false;
|
|
27
|
+
let failSave = false;
|
|
28
|
+
let failRestore = false;
|
|
29
|
+
let failLoad = false;
|
|
30
|
+
let ModelDraft: any;
|
|
31
|
+
const passport = { isAuthenticated: true, user: { id: 1, anonymous: false } };
|
|
32
|
+
const environment = { instanceId: '2', site: 'admin' };
|
|
33
|
+
const contextListeners = new Set<() => void>();
|
|
34
|
+
let epoch = 0;
|
|
35
|
+
let previousScope = '';
|
|
36
|
+
function capture() {
|
|
37
|
+
const scope = [environment.instanceId, environment.site, String(passport.user.id)];
|
|
38
|
+
const serialized = JSON.stringify(scope);
|
|
39
|
+
if (serialized !== previousScope) {
|
|
40
|
+
if (previousScope) ++epoch;
|
|
41
|
+
previousScope = serialized;
|
|
42
|
+
for (const listener of contextListeners) listener();
|
|
43
|
+
}
|
|
44
|
+
return { scope, epoch };
|
|
45
|
+
}
|
|
46
|
+
const persistence = {
|
|
47
|
+
capture,
|
|
48
|
+
current(token: any) {
|
|
49
|
+
const current = capture();
|
|
50
|
+
return (
|
|
51
|
+
token.epoch === current.epoch &&
|
|
52
|
+
JSON.stringify(token.scope) === JSON.stringify(current.scope)
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
subscribe(listener: () => void) {
|
|
56
|
+
contextListeners.add(listener);
|
|
57
|
+
return () => contextListeners.delete(listener);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const app: any = {
|
|
61
|
+
meta: {
|
|
62
|
+
event: {
|
|
63
|
+
on(name: string, listener: (event: any, next: () => void) => void) {
|
|
64
|
+
assert.equal(name, 'a-router:routeDiscard');
|
|
65
|
+
accesses.discardListener++;
|
|
66
|
+
discardListeners.add(listener);
|
|
67
|
+
return () => {
|
|
68
|
+
accesses.discardCleanup++;
|
|
69
|
+
discardListeners.delete(listener);
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
bean: {
|
|
75
|
+
async _getBeanSelector(_name: string, _cache: boolean, key: string) {
|
|
76
|
+
accesses.selector++;
|
|
77
|
+
if (!owners.has(key)) {
|
|
78
|
+
const owner = new ModelDraft();
|
|
79
|
+
await owner.__init__(key);
|
|
80
|
+
owners.set(key, owner);
|
|
81
|
+
}
|
|
82
|
+
return owners.get(key);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
class BeanBase {
|
|
87
|
+
app = app;
|
|
88
|
+
sys = { env: { SITE_ID: 'admin' } };
|
|
89
|
+
scope = { config: { draft: { maxAge: 86400000 } } };
|
|
90
|
+
$passport = passport;
|
|
91
|
+
$modelPersistence = persistence;
|
|
92
|
+
async __init__() {}
|
|
93
|
+
}
|
|
94
|
+
class BeanModelBase extends BeanBase {
|
|
95
|
+
snapshot: any;
|
|
96
|
+
keyValue?: string;
|
|
97
|
+
async __init__(key?: string) {
|
|
98
|
+
await super.__init__();
|
|
99
|
+
this.keyValue = key;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
$useStateLocalAsync(stateOptions: any) {
|
|
103
|
+
accesses.localState++;
|
|
104
|
+
assert.deepEqual(stateOptions, {
|
|
105
|
+
queryKey: ['snapshot'],
|
|
106
|
+
meta: { defaultData: null, maxAge: 86400000 },
|
|
107
|
+
});
|
|
108
|
+
// Like the production custom ref, initialization does not read storage.
|
|
109
|
+
return {
|
|
110
|
+
then: (resolve: (value: any) => void, reject: (error: unknown) => void) =>
|
|
111
|
+
(options.load ?? Promise.resolve())
|
|
112
|
+
.then(() => {
|
|
113
|
+
accesses.storage++;
|
|
114
|
+
if (failLoad) throw new Error('Unavailable');
|
|
115
|
+
return database.get(this.keyValue!) ?? null;
|
|
116
|
+
})
|
|
117
|
+
.then(resolve, reject),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async $ensureStateLocalAsync(snapshot: any) {
|
|
122
|
+
this.snapshot = await snapshot;
|
|
123
|
+
return this.snapshot;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$useStateData(stateOptions: any) {
|
|
127
|
+
assert.deepEqual(stateOptions, {
|
|
128
|
+
queryKey: ['snapshot'],
|
|
129
|
+
enabled: false,
|
|
130
|
+
staleTime: Infinity,
|
|
131
|
+
meta: { persister: { storage: 'db', sync: false }, ssr: { dehydrate: false } },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$persisterError(key: string[]) {
|
|
136
|
+
assert.deepEqual(key, ['snapshot']);
|
|
137
|
+
return failRestore;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
$setQueryData(key: string[], value: any, persist: boolean) {
|
|
141
|
+
assert.deepEqual(key, ['snapshot']);
|
|
142
|
+
assert.equal(persist, false);
|
|
143
|
+
this.snapshot = value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
$persisterSave(key: string[]) {
|
|
147
|
+
assert.deepEqual(key, ['snapshot']);
|
|
148
|
+
accesses.storage++;
|
|
149
|
+
const snapshot = structuredClone(this.snapshot);
|
|
150
|
+
const operation = queue.then(async () => {
|
|
151
|
+
await options.write;
|
|
152
|
+
if (failSave) throw new Error('Unavailable');
|
|
153
|
+
database.set(this.keyValue!, snapshot);
|
|
154
|
+
});
|
|
155
|
+
queue = operation.catch(() => {});
|
|
156
|
+
return operation;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
$persisterRemove(key: string[]) {
|
|
160
|
+
assert.deepEqual(key, ['snapshot']);
|
|
161
|
+
accesses.storage++;
|
|
162
|
+
const operation = queue.then(() => {
|
|
163
|
+
if (failRemove) throw new Error('Unavailable');
|
|
164
|
+
database.delete(this.keyValue!);
|
|
165
|
+
});
|
|
166
|
+
queue = operation.catch(() => {});
|
|
167
|
+
return operation;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function evaluateClass(path: string, name: string, baseName: string, base: any) {
|
|
171
|
+
const source = readFileSync(new URL(path, import.meta.url), 'utf8')
|
|
172
|
+
.replace(/^import[\s\S]*?;\n/gm, '')
|
|
173
|
+
.replace(/^@(Model|Bean)[^\n]*\n/gm, '');
|
|
174
|
+
const javascript = stripTypeScriptTypes(source).replace(
|
|
175
|
+
`export class ${name}`,
|
|
176
|
+
`class ${name}`,
|
|
177
|
+
);
|
|
178
|
+
return new Function(baseName, `${javascript}\nreturn ${name};`)(base);
|
|
179
|
+
}
|
|
180
|
+
ModelDraft = evaluateClass(
|
|
181
|
+
'../../src/model/draft.ts',
|
|
182
|
+
'ModelDraft',
|
|
183
|
+
'BeanModelBase',
|
|
184
|
+
BeanModelBase,
|
|
185
|
+
);
|
|
186
|
+
const BeanDraftCoordinator = evaluateClass(
|
|
187
|
+
'../../src/bean/bean.draftCoordinator.ts',
|
|
188
|
+
'BeanDraftCoordinator',
|
|
189
|
+
'BeanBase',
|
|
190
|
+
BeanBase,
|
|
191
|
+
);
|
|
192
|
+
const coordinator = new BeanDraftCoordinator();
|
|
193
|
+
await coordinator.__init__();
|
|
194
|
+
const router = {
|
|
195
|
+
resolve(fullPath: string) {
|
|
196
|
+
const path = fullPath.split('?')[0];
|
|
197
|
+
const [, resource, id, scene] = /\/resource\/([^/]+)\/([^/]+)(?:\/([^/]+))?/.exec(path) ?? [];
|
|
198
|
+
return {
|
|
199
|
+
path,
|
|
200
|
+
name: id === 'create' ? 'rest-resource:entryCreate' : 'rest-resource:entry',
|
|
201
|
+
params: { resource: decodeURIComponent(resource ?? ''), id, formScene: scene },
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const identity = coordinator.identity(
|
|
206
|
+
router,
|
|
207
|
+
'/admin/rest/resource/training-student%3Astudent/create',
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
coordinator,
|
|
211
|
+
accesses,
|
|
212
|
+
get listenerCount() {
|
|
213
|
+
return discardListeners.size;
|
|
214
|
+
},
|
|
215
|
+
dispose() {
|
|
216
|
+
coordinator.__dispose__();
|
|
217
|
+
},
|
|
218
|
+
async initializeSelected(key: string) {
|
|
219
|
+
const selected = new ModelDraft();
|
|
220
|
+
await selected.__init__(key);
|
|
221
|
+
return selected;
|
|
222
|
+
},
|
|
223
|
+
database,
|
|
224
|
+
owners,
|
|
225
|
+
passport,
|
|
226
|
+
environment,
|
|
227
|
+
persistence,
|
|
228
|
+
router,
|
|
229
|
+
identity,
|
|
230
|
+
setFailRestore(value: boolean) {
|
|
231
|
+
failRestore = value;
|
|
232
|
+
},
|
|
233
|
+
setFailLoad(value: boolean) {
|
|
234
|
+
failLoad = value;
|
|
235
|
+
},
|
|
236
|
+
setFailSave(value: boolean) {
|
|
237
|
+
failSave = value;
|
|
238
|
+
},
|
|
239
|
+
setFailRemove(value: boolean) {
|
|
240
|
+
failRemove = value;
|
|
241
|
+
},
|
|
242
|
+
discard(fullPaths: string[]) {
|
|
243
|
+
const cleanups: Promise<void>[] = [];
|
|
244
|
+
for (const listener of discardListeners) {
|
|
245
|
+
listener(
|
|
246
|
+
{
|
|
247
|
+
router,
|
|
248
|
+
items: fullPaths.map(fullPath => ({ fullPath })),
|
|
249
|
+
waitUntil(cleanup: Promise<void>) {
|
|
250
|
+
cleanups.push(cleanup);
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
() => {},
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return Promise.all(cleanups);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const envelope = {
|
|
262
|
+
version: 1,
|
|
263
|
+
savedAt: 1,
|
|
264
|
+
schemaSignature: 'schema',
|
|
265
|
+
projection: { values: { name: 'draft' }, relations: {}, undefinedPaths: [] },
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
async function withBrowser(run: () => Promise<void>) {
|
|
269
|
+
const environment = process.env as Record<string, string | undefined>;
|
|
270
|
+
const client = environment.CLIENT;
|
|
271
|
+
environment.CLIENT = 'true';
|
|
272
|
+
try {
|
|
273
|
+
await run();
|
|
274
|
+
} finally {
|
|
275
|
+
if (client === undefined) delete environment.CLIENT;
|
|
276
|
+
else environment.CLIENT = client;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
test(
|
|
281
|
+
'BeanDraftCoordinator and ModelDraft SSR initialization never accesses browser draft state',
|
|
282
|
+
{ concurrency: false },
|
|
283
|
+
async () => {
|
|
284
|
+
const environment = process.env as Record<string, string | undefined>;
|
|
285
|
+
const client = environment.CLIENT;
|
|
286
|
+
delete environment.CLIENT;
|
|
287
|
+
try {
|
|
288
|
+
const runtime = await createRuntime();
|
|
289
|
+
const selected = await runtime.initializeSelected('server-rendered-owner');
|
|
290
|
+
assert.equal(runtime.identity, undefined);
|
|
291
|
+
assert.equal('snapshot' in runtime.coordinator, false);
|
|
292
|
+
assert.equal(selected.snapshot, undefined);
|
|
293
|
+
assert.equal(runtime.owners.size, 0);
|
|
294
|
+
assert.equal(runtime.database.size, 0);
|
|
295
|
+
assert.deepEqual(runtime.accesses, {
|
|
296
|
+
localState: 0,
|
|
297
|
+
selector: 0,
|
|
298
|
+
storage: 0,
|
|
299
|
+
discardListener: 0,
|
|
300
|
+
discardCleanup: 0,
|
|
301
|
+
});
|
|
302
|
+
runtime.dispose();
|
|
303
|
+
assert.equal(runtime.accesses.discardCleanup, 0);
|
|
304
|
+
} finally {
|
|
305
|
+
if (client !== undefined) environment.CLIENT = client;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
test(
|
|
311
|
+
'BeanDraftCoordinator CLIENT initialization registers once without owning local draft state',
|
|
312
|
+
{ concurrency: false },
|
|
313
|
+
async () =>
|
|
314
|
+
withBrowser(async () => {
|
|
315
|
+
const runtime = await createRuntime();
|
|
316
|
+
assert.deepEqual(runtime.accesses, {
|
|
317
|
+
localState: 0,
|
|
318
|
+
selector: 0,
|
|
319
|
+
storage: 0,
|
|
320
|
+
discardListener: 1,
|
|
321
|
+
discardCleanup: 0,
|
|
322
|
+
});
|
|
323
|
+
assert.equal(runtime.listenerCount, 1);
|
|
324
|
+
assert.equal('snapshot' in runtime.coordinator, false);
|
|
325
|
+
assert.equal('$useStateLocalAsync' in runtime.coordinator, false);
|
|
326
|
+
const selected = await runtime.initializeSelected('selected-owner');
|
|
327
|
+
assert.equal(selected.failed, false);
|
|
328
|
+
assert.deepEqual(runtime.accesses, {
|
|
329
|
+
localState: 1,
|
|
330
|
+
selector: 0,
|
|
331
|
+
storage: 0,
|
|
332
|
+
discardListener: 1,
|
|
333
|
+
discardCleanup: 0,
|
|
334
|
+
});
|
|
335
|
+
assert.equal('identity' in selected, false);
|
|
336
|
+
assert.equal('current' in selected, false);
|
|
337
|
+
assert.equal('__dispose__' in selected, false);
|
|
338
|
+
runtime.dispose();
|
|
339
|
+
assert.equal(runtime.accesses.discardCleanup, 1);
|
|
340
|
+
assert.equal(runtime.listenerCount, 0);
|
|
341
|
+
await runtime.discard([runtime.identity.path]);
|
|
342
|
+
assert.equal(runtime.accesses.selector, 0);
|
|
343
|
+
assert.equal(runtime.accesses.storage, 0);
|
|
344
|
+
}),
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
test(
|
|
348
|
+
'BeanDraftCoordinator keeps resource business identity independent of persistence context',
|
|
349
|
+
{ concurrency: false },
|
|
350
|
+
async () =>
|
|
351
|
+
withBrowser(async () => {
|
|
352
|
+
const runtime = await createRuntime();
|
|
353
|
+
const { coordinator, identity, router } = runtime;
|
|
354
|
+
const key = coordinator.key(identity);
|
|
355
|
+
assert.equal(Object.hasOwn(identity, 'origin'), false);
|
|
356
|
+
assert.deepEqual(JSON.parse(key), [
|
|
357
|
+
'training-student:student',
|
|
358
|
+
'create',
|
|
359
|
+
null,
|
|
360
|
+
identity.path,
|
|
361
|
+
]);
|
|
362
|
+
assert.equal(
|
|
363
|
+
key,
|
|
364
|
+
coordinator.key(coordinator.identity(router, `${identity.path}?display=compact`)),
|
|
365
|
+
);
|
|
366
|
+
for (const changed of [
|
|
367
|
+
{ resource: 'another:resource' },
|
|
368
|
+
{ mode: 'edit', id: '1' },
|
|
369
|
+
{ path: '/different' },
|
|
370
|
+
])
|
|
371
|
+
assert.notEqual(key, coordinator.key({ ...identity, ...changed }));
|
|
372
|
+
runtime.passport.user.anonymous = true;
|
|
373
|
+
assert.equal(coordinator.identity(router, identity.path), undefined);
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
test(
|
|
378
|
+
'BeanDraftCoordinator distinguishes missing snapshots from failed restoration',
|
|
379
|
+
{ concurrency: false },
|
|
380
|
+
async () =>
|
|
381
|
+
withBrowser(async () => {
|
|
382
|
+
const runtime = await createRuntime();
|
|
383
|
+
const missing = await runtime.coordinator.open(runtime.identity);
|
|
384
|
+
assert.equal(missing.owner.snapshot, null);
|
|
385
|
+
assert.equal(missing.owner.failed, false);
|
|
386
|
+
runtime.setFailRestore(true);
|
|
387
|
+
const failed = await runtime.coordinator.open(runtime.identity);
|
|
388
|
+
assert.equal(failed.owner.snapshot, null);
|
|
389
|
+
assert.equal(failed.owner.failed, true);
|
|
390
|
+
runtime.setFailRestore(false);
|
|
391
|
+
const retried = await runtime.coordinator.open(runtime.identity);
|
|
392
|
+
assert.equal(retried.owner.failed, false);
|
|
393
|
+
|
|
394
|
+
const loadFailure = await createRuntime();
|
|
395
|
+
loadFailure.setFailLoad(true);
|
|
396
|
+
const rejected = await loadFailure.coordinator.open(loadFailure.identity);
|
|
397
|
+
assert.equal(rejected.owner.failed, true);
|
|
398
|
+
loadFailure.setFailLoad(false);
|
|
399
|
+
const recovered = await loadFailure.coordinator.open(loadFailure.identity);
|
|
400
|
+
assert.equal(recovered.owner.snapshot, null);
|
|
401
|
+
assert.equal(recovered.owner.failed, false);
|
|
402
|
+
}),
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
test(
|
|
406
|
+
'BeanDraftCoordinator account changes invalidate pending restoration and outgoing saves',
|
|
407
|
+
{ concurrency: false },
|
|
408
|
+
async () =>
|
|
409
|
+
withBrowser(async () => {
|
|
410
|
+
const load = deferred();
|
|
411
|
+
const delayed = await createRuntime({ load: load.promise });
|
|
412
|
+
const opening = delayed.coordinator.open(delayed.identity);
|
|
413
|
+
await Promise.resolve();
|
|
414
|
+
delayed.passport.user.id = 2;
|
|
415
|
+
load.resolve();
|
|
416
|
+
assert.equal(await opening, undefined);
|
|
417
|
+
assert.equal(delayed.database.size, 0);
|
|
418
|
+
|
|
419
|
+
const runtime = await createRuntime();
|
|
420
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
421
|
+
assert.equal(editor.owner.failed, false);
|
|
422
|
+
runtime.passport.user.id = 2;
|
|
423
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
424
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
425
|
+
assert.equal(runtime.database.size, 0);
|
|
426
|
+
const replacement = runtime.coordinator.identity(runtime.router, runtime.identity.path);
|
|
427
|
+
const current = await runtime.coordinator.open(replacement);
|
|
428
|
+
assert.equal(current.key, editor.key);
|
|
429
|
+
assert.notDeepEqual(current.identity.context, editor.identity.context);
|
|
430
|
+
await runtime.coordinator.save(current, replacement, envelope);
|
|
431
|
+
assert.equal(runtime.database.has(current.key), true);
|
|
432
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
433
|
+
}),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
test(
|
|
437
|
+
'BeanDraftCoordinator invalidates instance/site switches and never revives an old editor after returning',
|
|
438
|
+
{ concurrency: false },
|
|
439
|
+
async () =>
|
|
440
|
+
withBrowser(async () => {
|
|
441
|
+
for (const dimension of ['instanceId', 'site'] as const) {
|
|
442
|
+
const runtime = await createRuntime();
|
|
443
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
444
|
+
const original = runtime.environment[dimension];
|
|
445
|
+
runtime.environment[dimension] = 'another';
|
|
446
|
+
const switched = runtime.coordinator.identity(runtime.router, runtime.identity.path);
|
|
447
|
+
assert.equal(runtime.coordinator.key(switched), editor.key);
|
|
448
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
449
|
+
assert.equal(runtime.coordinator.isCurrent(runtime.identity), false);
|
|
450
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
451
|
+
assert.equal(runtime.database.size, 0);
|
|
452
|
+
runtime.environment[dimension] = original;
|
|
453
|
+
const returned = runtime.coordinator.identity(runtime.router, runtime.identity.path);
|
|
454
|
+
assert.deepEqual(returned.context.scope, runtime.identity.context.scope);
|
|
455
|
+
assert.notEqual(returned.context.epoch, runtime.identity.context.epoch);
|
|
456
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
457
|
+
assert.equal(runtime.coordinator.isCurrent(switched), false);
|
|
458
|
+
const current = await runtime.coordinator.open(returned);
|
|
459
|
+
assert.equal(runtime.coordinator.current(current), true);
|
|
460
|
+
await runtime.coordinator.save(current, returned, envelope);
|
|
461
|
+
const accesses = runtime.accesses.storage;
|
|
462
|
+
await runtime.coordinator.remove(runtime.identity);
|
|
463
|
+
assert.equal(runtime.accesses.storage, accesses);
|
|
464
|
+
assert.equal(runtime.database.has(current.key), true);
|
|
465
|
+
runtime.dispose();
|
|
466
|
+
}
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
test(
|
|
471
|
+
'BeanDraftCoordinator skips queued removal when its context becomes obsolete',
|
|
472
|
+
{ concurrency: false },
|
|
473
|
+
async () =>
|
|
474
|
+
withBrowser(async () => {
|
|
475
|
+
const runtime = await createRuntime();
|
|
476
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
477
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
478
|
+
const accesses = runtime.accesses.storage;
|
|
479
|
+
// Both removals are accepted before the first barrier callback executes.
|
|
480
|
+
const first = runtime.coordinator.remove(runtime.identity);
|
|
481
|
+
const queued = runtime.coordinator.remove(runtime.identity);
|
|
482
|
+
runtime.environment.instanceId = 'another';
|
|
483
|
+
const replacement = runtime.coordinator.identity(runtime.router, runtime.identity.path);
|
|
484
|
+
const current = await runtime.coordinator.open(replacement);
|
|
485
|
+
await Promise.all([first, queued]);
|
|
486
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
487
|
+
assert.equal(runtime.coordinator.current(current), true);
|
|
488
|
+
assert.equal(runtime.database.has(current.key), true);
|
|
489
|
+
assert.equal(runtime.accesses.storage, accesses);
|
|
490
|
+
runtime.dispose();
|
|
491
|
+
}),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
test(
|
|
495
|
+
'BeanDraftCoordinator invalidates pending loads and orders deletion after accepted writes',
|
|
496
|
+
{ concurrency: false },
|
|
497
|
+
async () =>
|
|
498
|
+
withBrowser(async () => {
|
|
499
|
+
const write = deferred();
|
|
500
|
+
const runtime = await createRuntime({ write: write.promise });
|
|
501
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
502
|
+
const saving = runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
503
|
+
await Promise.resolve();
|
|
504
|
+
const removing = runtime.coordinator.remove(runtime.identity);
|
|
505
|
+
assert.equal(runtime.coordinator.current(editor), false);
|
|
506
|
+
write.resolve();
|
|
507
|
+
await Promise.all([saving, removing]);
|
|
508
|
+
assert.equal(runtime.database.size, 0);
|
|
509
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
510
|
+
assert.equal(runtime.database.size, 0);
|
|
511
|
+
|
|
512
|
+
const load = deferred();
|
|
513
|
+
const delayed = await createRuntime({ load: load.promise });
|
|
514
|
+
const opening = delayed.coordinator.open(delayed.identity);
|
|
515
|
+
await Promise.resolve();
|
|
516
|
+
const removal = delayed.coordinator.remove(delayed.identity);
|
|
517
|
+
load.resolve();
|
|
518
|
+
await removal;
|
|
519
|
+
assert.equal(await opening, undefined);
|
|
520
|
+
assert.equal(delayed.database.size, 0);
|
|
521
|
+
}),
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
test(
|
|
525
|
+
'BeanDraftCoordinator deletes unopened inactive routes and leaves unrelated drafts intact',
|
|
526
|
+
{ concurrency: false },
|
|
527
|
+
async () =>
|
|
528
|
+
withBrowser(async () => {
|
|
529
|
+
const runtime = await createRuntime();
|
|
530
|
+
const inactivePath = '/admin/rest/resource/training-student%3Astudent/42/edit';
|
|
531
|
+
const inactive = runtime.coordinator.identity(runtime.router, inactivePath);
|
|
532
|
+
const inactiveKey = runtime.coordinator.key(inactive);
|
|
533
|
+
runtime.database.set(inactiveKey, envelope);
|
|
534
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
535
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
536
|
+
assert.equal(runtime.owners.has(inactiveKey), false);
|
|
537
|
+
const storageBeforeDiscard = runtime.accesses.storage;
|
|
538
|
+
runtime.setFailLoad(true);
|
|
539
|
+
await runtime.discard([inactivePath]);
|
|
540
|
+
assert.equal(runtime.accesses.storage, storageBeforeDiscard + 1);
|
|
541
|
+
assert.equal(runtime.accesses.discardListener, 1);
|
|
542
|
+
assert.equal(runtime.owners.get(inactiveKey).snapshot, null);
|
|
543
|
+
assert.equal(runtime.database.has(inactiveKey), false);
|
|
544
|
+
assert.equal(runtime.database.has(editor.key), true);
|
|
545
|
+
assert.equal(runtime.coordinator.current(editor), true);
|
|
546
|
+
}),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
test(
|
|
550
|
+
'BeanDraftCoordinator storage failures remain visible without poisoning later operations',
|
|
551
|
+
{ concurrency: false },
|
|
552
|
+
async () =>
|
|
553
|
+
withBrowser(async () => {
|
|
554
|
+
const runtime = await createRuntime();
|
|
555
|
+
const editor = await runtime.coordinator.open(runtime.identity);
|
|
556
|
+
runtime.setFailSave(true);
|
|
557
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
558
|
+
assert.equal(editor.owner.failed, true);
|
|
559
|
+
runtime.setFailSave(false);
|
|
560
|
+
await runtime.coordinator.save(editor, runtime.identity, envelope);
|
|
561
|
+
assert.equal(editor.owner.failed, false);
|
|
562
|
+
runtime.setFailRemove(true);
|
|
563
|
+
await assert.rejects(
|
|
564
|
+
runtime.coordinator.remove(runtime.identity),
|
|
565
|
+
/Draft storage removal failed/,
|
|
566
|
+
);
|
|
567
|
+
assert.equal(editor.owner.failed, true);
|
|
568
|
+
runtime.setFailRemove(false);
|
|
569
|
+
await runtime.coordinator.remove(runtime.identity);
|
|
570
|
+
assert.equal(runtime.database.size, 0);
|
|
571
|
+
}),
|
|
572
|
+
);
|