@konitif/workbench 0.285.6 → 0.285.7
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/README.md +4 -0
- package/dist/application/experience/experienceRepository.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/infrastructure/persistence/BrowserExperienceRepository.d.ts +14 -0
- package/dist/infrastructure/persistence/BrowserExperienceRepository.js +243 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,10 @@ when that base is still the current head, and archive, restore and tombstone
|
|
|
45
45
|
transitions are explicit. The repository contract is storage-neutral; browser,
|
|
46
46
|
remote, tenant and product-admission policies remain host adapters.
|
|
47
47
|
|
|
48
|
+
Workbench ships the generic browser adapter for that repository contract. A
|
|
49
|
+
host selects its storage key and keeps product migration, metadata admission
|
|
50
|
+
and domain specialization outside the adapter.
|
|
51
|
+
|
|
48
52
|
## Quick start
|
|
49
53
|
|
|
50
54
|
```ts
|
|
@@ -31,6 +31,7 @@ export interface WorkbenchExperienceRepository<TMetadata = unknown, TSpecializat
|
|
|
31
31
|
draft: WorkbenchExperienceDraft<TSpecialization>;
|
|
32
32
|
expectedHeadRevisionId: ExperienceRevisionIdentity;
|
|
33
33
|
revisionId: ExperienceRevisionIdentity;
|
|
34
|
+
derivedFrom?: ExperienceRevisionIdentity | null;
|
|
34
35
|
metadata?: TMetadata;
|
|
35
36
|
}): Promise<WorkbenchExperienceRepositoryResult<WorkbenchExperienceLifecycleValue<TMetadata, TSpecialization>>>;
|
|
36
37
|
transitionLifecycle(input: {
|
package/dist/index.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ export * from "./domain/workspace/validation.js";
|
|
|
114
114
|
export * from "./infrastructure/audio/WorkbenchAudioManager.js";
|
|
115
115
|
export * from "./infrastructure/backend/HttpWorkbenchBackendClient.js";
|
|
116
116
|
export * from "./infrastructure/persistence/LocalWorkspacePersistence.js";
|
|
117
|
+
export * from "./infrastructure/persistence/BrowserExperienceRepository.js";
|
|
117
118
|
export * from "./infrastructure/surfaces/InMemorySurfaceViewerRegistry.js";
|
|
118
119
|
export * from "./infrastructure/temporal/InMemoryTemporalDialectRegistry.js";
|
|
119
120
|
export * from "./infrastructure/tools/InMemoryToolRegistry.js";
|
package/dist/index.js
CHANGED
|
@@ -114,6 +114,7 @@ export * from "./domain/workspace/validation.js";
|
|
|
114
114
|
export * from "./infrastructure/audio/WorkbenchAudioManager.js";
|
|
115
115
|
export * from "./infrastructure/backend/HttpWorkbenchBackendClient.js";
|
|
116
116
|
export * from "./infrastructure/persistence/LocalWorkspacePersistence.js";
|
|
117
|
+
export * from "./infrastructure/persistence/BrowserExperienceRepository.js";
|
|
117
118
|
export * from "./infrastructure/surfaces/InMemorySurfaceViewerRegistry.js";
|
|
118
119
|
export * from "./infrastructure/temporal/InMemoryTemporalDialectRegistry.js";
|
|
119
120
|
export * from "./infrastructure/tools/InMemoryToolRegistry.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WorkbenchExperienceRepository } from "../../application/experience/experienceRepository.js";
|
|
2
|
+
export interface BrowserExperienceRepositoryStorage {
|
|
3
|
+
getItem(key: string): string | null;
|
|
4
|
+
setItem(key: string, value: string): void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Browser persistence adapter for the generic Workbench Experience authority.
|
|
8
|
+
* Products choose the storage key and keep migration and specialization policy
|
|
9
|
+
* outside this adapter.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createBrowserWorkbenchExperienceRepository<TMetadata = unknown, TSpecialization = unknown>(input: {
|
|
12
|
+
storageKey: string;
|
|
13
|
+
storage?: BrowserExperienceRepositoryStorage | null;
|
|
14
|
+
}): WorkbenchExperienceRepository<TMetadata, TSpecialization>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { WORKBENCH_EXPERIENCE_SCHEMA, WORKBENCH_EXPERIENCE_SCHEMA_VERSION, } from "../../application/experience/experienceArtifacts.js";
|
|
2
|
+
import { saveWorkbenchExperienceRevision, transitionWorkbenchExperienceLifecycle, } from "../../application/experience/experienceLifecycle.js";
|
|
3
|
+
/**
|
|
4
|
+
* Browser persistence adapter for the generic Workbench Experience authority.
|
|
5
|
+
* Products choose the storage key and keep migration and specialization policy
|
|
6
|
+
* outside this adapter.
|
|
7
|
+
*/
|
|
8
|
+
export function createBrowserWorkbenchExperienceRepository(input) {
|
|
9
|
+
const storage = input.storage === undefined ? resolveBrowserStorage() : input.storage;
|
|
10
|
+
const storageKey = requireStorageKey(input.storageKey);
|
|
11
|
+
return {
|
|
12
|
+
hydrate: () => hydrate(storage, storageKey),
|
|
13
|
+
async list() {
|
|
14
|
+
const snapshot = await hydrate(storage, storageKey);
|
|
15
|
+
return snapshot.ok ? accepted(snapshot.value.experiences) : snapshot;
|
|
16
|
+
},
|
|
17
|
+
async resolveRevision(reference) {
|
|
18
|
+
const snapshot = await hydrate(storage, storageKey);
|
|
19
|
+
if (!snapshot.ok)
|
|
20
|
+
return snapshot;
|
|
21
|
+
const revision = findRevision(snapshot.value, reference);
|
|
22
|
+
return revision
|
|
23
|
+
? accepted(revision)
|
|
24
|
+
: refused('experience.repository.not-found', `Experience revision ${reference.revisionId.value} was not found.`);
|
|
25
|
+
},
|
|
26
|
+
async create(value) {
|
|
27
|
+
const snapshot = await hydrate(storage, storageKey);
|
|
28
|
+
if (!snapshot.ok)
|
|
29
|
+
return snapshot;
|
|
30
|
+
if (snapshot.value.experiences.some(candidate => candidate.id.value === value.experience.id.value)) {
|
|
31
|
+
return refused('experience.repository.already-exists', `Experience ${value.experience.id.value} already exists.`);
|
|
32
|
+
}
|
|
33
|
+
if (snapshot.value.revisions.some(candidate => candidate.id.value === value.revision.id.value)) {
|
|
34
|
+
return refused('experience.repository.already-exists', `Experience revision ${value.revision.id.value} already exists.`);
|
|
35
|
+
}
|
|
36
|
+
const persisted = persist(storage, storageKey, {
|
|
37
|
+
experiences: [...snapshot.value.experiences, value.experience],
|
|
38
|
+
revisions: [...snapshot.value.revisions, value.revision],
|
|
39
|
+
});
|
|
40
|
+
return persisted.ok ? accepted(value) : persisted;
|
|
41
|
+
},
|
|
42
|
+
async saveRevision(value) {
|
|
43
|
+
const snapshot = await hydrate(storage, storageKey);
|
|
44
|
+
if (!snapshot.ok)
|
|
45
|
+
return snapshot;
|
|
46
|
+
const experience = snapshot.value.experiences.find(candidate => candidate.id.value === value.draft.experienceId.value);
|
|
47
|
+
if (!experience) {
|
|
48
|
+
return refused('experience.repository.not-found', `Experience ${value.draft.experienceId.value} was not found.`);
|
|
49
|
+
}
|
|
50
|
+
if (snapshot.value.revisions.some(candidate => candidate.id.value === value.revisionId.value)) {
|
|
51
|
+
return refused('experience.repository.already-exists', `Experience revision ${value.revisionId.value} already exists.`);
|
|
52
|
+
}
|
|
53
|
+
const saved = saveWorkbenchExperienceRevision({
|
|
54
|
+
experience,
|
|
55
|
+
draft: value.draft,
|
|
56
|
+
expectedHeadRevisionId: value.expectedHeadRevisionId,
|
|
57
|
+
revisionId: value.revisionId,
|
|
58
|
+
derivedFrom: value.derivedFrom,
|
|
59
|
+
metadata: value.metadata,
|
|
60
|
+
});
|
|
61
|
+
if (!saved.ok)
|
|
62
|
+
return lifecycleRefused(saved.diagnostics[0]);
|
|
63
|
+
const persisted = persist(storage, storageKey, {
|
|
64
|
+
experiences: snapshot.value.experiences.map(candidate => candidate.id.value === saved.value.experience.id.value ? saved.value.experience : candidate),
|
|
65
|
+
revisions: [...snapshot.value.revisions, saved.value.revision],
|
|
66
|
+
});
|
|
67
|
+
return persisted.ok ? accepted(saved.value) : persisted;
|
|
68
|
+
},
|
|
69
|
+
async transitionLifecycle(value) {
|
|
70
|
+
const snapshot = await hydrate(storage, storageKey);
|
|
71
|
+
if (!snapshot.ok)
|
|
72
|
+
return snapshot;
|
|
73
|
+
const experience = snapshot.value.experiences.find(candidate => candidate.id.value === value.experienceId.value);
|
|
74
|
+
if (!experience) {
|
|
75
|
+
return refused('experience.repository.not-found', `Experience ${value.experienceId.value} was not found.`);
|
|
76
|
+
}
|
|
77
|
+
const transitioned = transitionWorkbenchExperienceLifecycle({
|
|
78
|
+
experience,
|
|
79
|
+
expected: value.expected,
|
|
80
|
+
next: value.next,
|
|
81
|
+
});
|
|
82
|
+
if (!transitioned.ok)
|
|
83
|
+
return lifecycleRefused(transitioned.diagnostics[0]);
|
|
84
|
+
const persisted = persist(storage, storageKey, {
|
|
85
|
+
experiences: snapshot.value.experiences.map(candidate => candidate.id.value === transitioned.value.id.value ? transitioned.value : candidate),
|
|
86
|
+
revisions: snapshot.value.revisions,
|
|
87
|
+
});
|
|
88
|
+
return persisted.ok ? accepted(transitioned.value) : persisted;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function hydrate(storage, storageKey) {
|
|
93
|
+
if (!storage)
|
|
94
|
+
return refused('experience.repository.unavailable', 'Browser Experience storage is unavailable.');
|
|
95
|
+
let raw;
|
|
96
|
+
try {
|
|
97
|
+
raw = storage.getItem(storageKey);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return refused('experience.repository.unavailable', 'Browser Experience storage could not be read.');
|
|
101
|
+
}
|
|
102
|
+
if (!raw)
|
|
103
|
+
return accepted({ experiences: [], revisions: [] });
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
if (!isStoredRepository(parsed)) {
|
|
107
|
+
return refused('experience.repository.invalid-artifact', 'Stored Experience repository does not match the Workbench contract.');
|
|
108
|
+
}
|
|
109
|
+
return accepted({ experiences: parsed.experiences, revisions: parsed.revisions });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return refused('experience.repository.invalid-artifact', 'Stored Experience repository is not valid JSON.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function persist(storage, storageKey, snapshot) {
|
|
116
|
+
if (!storage)
|
|
117
|
+
return refused('experience.repository.unavailable', 'Browser Experience storage is unavailable.');
|
|
118
|
+
const stored = {
|
|
119
|
+
schema: 'konitif.workbench-experience-repository',
|
|
120
|
+
schemaVersion: 1,
|
|
121
|
+
experiences: [...snapshot.experiences],
|
|
122
|
+
revisions: [...snapshot.revisions],
|
|
123
|
+
};
|
|
124
|
+
try {
|
|
125
|
+
storage.setItem(storageKey, JSON.stringify(stored));
|
|
126
|
+
return accepted(snapshot);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return refused('experience.repository.unavailable', 'Browser Experience storage could not be updated.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function findRevision(snapshot, reference) {
|
|
133
|
+
return snapshot.revisions.find(candidate => candidate.experienceId.value === reference.experienceId.value
|
|
134
|
+
&& candidate.id.value === reference.revisionId.value);
|
|
135
|
+
}
|
|
136
|
+
function isStoredRepository(value) {
|
|
137
|
+
if (!isRecord(value))
|
|
138
|
+
return false;
|
|
139
|
+
if (value.schema !== 'konitif.workbench-experience-repository'
|
|
140
|
+
|| value.schemaVersion !== 1
|
|
141
|
+
|| !Array.isArray(value.experiences)
|
|
142
|
+
|| !Array.isArray(value.revisions))
|
|
143
|
+
return false;
|
|
144
|
+
const experiences = value.experiences;
|
|
145
|
+
const revisions = value.revisions;
|
|
146
|
+
if (!experiences.every(isWorkbenchExperience) || !revisions.every(isWorkbenchExperienceRevision))
|
|
147
|
+
return false;
|
|
148
|
+
return experiences.every(experience => revisions.some(revision => isRecord(revision)
|
|
149
|
+
&& isIdentity(revision.experienceId, 'experience')
|
|
150
|
+
&& isIdentity(revision.id, 'experience-revision')
|
|
151
|
+
&& revision.experienceId.value === experience.id.value
|
|
152
|
+
&& revision.id.value === experience.headRevisionId.value));
|
|
153
|
+
}
|
|
154
|
+
function isWorkbenchExperience(value) {
|
|
155
|
+
return isRecord(value)
|
|
156
|
+
&& isIdentity(value.id, 'experience')
|
|
157
|
+
&& isIdentity(value.headRevisionId, 'experience-revision')
|
|
158
|
+
&& isLifecycle(value.lifecycle)
|
|
159
|
+
&& Object.hasOwn(value, 'metadata');
|
|
160
|
+
}
|
|
161
|
+
function isWorkbenchExperienceRevision(value) {
|
|
162
|
+
return isRecord(value)
|
|
163
|
+
&& value.schema === WORKBENCH_EXPERIENCE_SCHEMA
|
|
164
|
+
&& value.schemaVersion === WORKBENCH_EXPERIENCE_SCHEMA_VERSION
|
|
165
|
+
&& isIdentity(value.id, 'experience-revision')
|
|
166
|
+
&& isIdentity(value.experienceId, 'experience')
|
|
167
|
+
&& isNullableIdentity(value.parentRevisionId, 'experience-revision')
|
|
168
|
+
&& isNullableIdentity(value.derivedFrom, 'experience-revision')
|
|
169
|
+
&& isPresetBinding(value.preset)
|
|
170
|
+
&& Object.hasOwn(value, 'specialization');
|
|
171
|
+
}
|
|
172
|
+
function isPresetBinding(value) {
|
|
173
|
+
if (!isRecord(value) || !isContentReference(value.source))
|
|
174
|
+
return false;
|
|
175
|
+
if (value.mode === 'reference')
|
|
176
|
+
return true;
|
|
177
|
+
return value.mode === 'override'
|
|
178
|
+
&& isContentReference(value.result)
|
|
179
|
+
&& Array.isArray(value.transformations)
|
|
180
|
+
&& value.transformations.length > 0
|
|
181
|
+
&& value.transformations.every((item, index) => isRecord(item)
|
|
182
|
+
&& item.order === index
|
|
183
|
+
&& typeof item.kind === 'string'
|
|
184
|
+
&& item.kind.trim().length > 0
|
|
185
|
+
&& typeof item.summary === 'string'
|
|
186
|
+
&& item.summary.trim().length > 0);
|
|
187
|
+
}
|
|
188
|
+
function isContentReference(value) {
|
|
189
|
+
return isRecord(value)
|
|
190
|
+
&& value.kind === 'workspace-preset'
|
|
191
|
+
&& typeof value.id === 'string'
|
|
192
|
+
&& value.id.trim().length > 0
|
|
193
|
+
&& typeof value.revision === 'string'
|
|
194
|
+
&& value.revision.trim().length > 0
|
|
195
|
+
&& typeof value.contentId === 'string'
|
|
196
|
+
&& /^[a-f0-9]{64}$/.test(value.contentId);
|
|
197
|
+
}
|
|
198
|
+
function isIdentity(value, kind) {
|
|
199
|
+
return isRecord(value)
|
|
200
|
+
&& value.kind === kind
|
|
201
|
+
&& typeof value.value === 'string'
|
|
202
|
+
&& value.value.trim().length > 0;
|
|
203
|
+
}
|
|
204
|
+
function isNullableIdentity(value, kind) {
|
|
205
|
+
return value === null || isIdentity(value, kind);
|
|
206
|
+
}
|
|
207
|
+
function isLifecycle(value) {
|
|
208
|
+
return value === 'active' || value === 'archived' || value === 'tombstoned';
|
|
209
|
+
}
|
|
210
|
+
function isRecord(value) {
|
|
211
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
212
|
+
}
|
|
213
|
+
function lifecycleRefused(diagnostic) {
|
|
214
|
+
const code = diagnostic?.code === 'experience.lifecycle.head-conflict'
|
|
215
|
+
? 'experience.repository.head-conflict'
|
|
216
|
+
: 'experience.repository.lifecycle-conflict';
|
|
217
|
+
return refused(code, diagnostic?.message ?? 'Experience lifecycle operation was refused.');
|
|
218
|
+
}
|
|
219
|
+
function accepted(value) {
|
|
220
|
+
return { ok: true, value: freezeDetached(value) };
|
|
221
|
+
}
|
|
222
|
+
function refused(code, message) {
|
|
223
|
+
return { ok: false, error: { code, message } };
|
|
224
|
+
}
|
|
225
|
+
function requireStorageKey(value) {
|
|
226
|
+
const normalized = value.trim();
|
|
227
|
+
if (!normalized)
|
|
228
|
+
throw new TypeError('Browser Experience repository storage key is required.');
|
|
229
|
+
return normalized;
|
|
230
|
+
}
|
|
231
|
+
function resolveBrowserStorage() {
|
|
232
|
+
return typeof localStorage === 'undefined' ? null : localStorage;
|
|
233
|
+
}
|
|
234
|
+
function freezeDetached(value) {
|
|
235
|
+
return deepFreeze(structuredClone(value));
|
|
236
|
+
}
|
|
237
|
+
function deepFreeze(value) {
|
|
238
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value))
|
|
239
|
+
return value;
|
|
240
|
+
for (const nested of Object.values(value))
|
|
241
|
+
deepFreeze(nested);
|
|
242
|
+
return Object.freeze(value);
|
|
243
|
+
}
|
package/package.json
CHANGED