@things-factory/figure-ui 10.1.243 → 10.1.245
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/client/graphql/index.ts +15 -0
- package/client/modeller/editor.ts +48 -2
- package/client/modeller/figure-save-conflict.ts +73 -0
- package/client/modeller/figure-section-editor.ts +3 -0
- package/client/modeller/product-session.ts +60 -6
- package/client/modeller/save-conflict.ts +91 -0
- package/client/pages/figure-list-page.ts +15 -5
- package/client/pages/figure-modeller-page.ts +27 -9
- package/dist-client/graphql/index.d.ts +2 -0
- package/dist-client/graphql/index.js +15 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +7 -0
- package/dist-client/modeller/editor.js +55 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-save-conflict.d.ts +19 -0
- package/dist-client/modeller/figure-save-conflict.js +76 -0
- package/dist-client/modeller/figure-save-conflict.js.map +1 -0
- package/dist-client/modeller/figure-section-editor.d.ts +2 -0
- package/dist-client/modeller/figure-section-editor.js +8 -0
- package/dist-client/modeller/figure-section-editor.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +234 -1
- package/dist-client/modeller/product-session.js +67 -7
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/save-conflict.d.ts +12 -0
- package/dist-client/modeller/save-conflict.js +63 -0
- package/dist-client/modeller/save-conflict.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +1 -0
- package/dist-client/pages/figure-list-page.js +12 -3
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +4 -3
- package/dist-client/pages/figure-modeller-page.js +29 -9
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/test/browser/product-page.check.mjs +176 -0
- package/test/browser/product-page.js +4 -1
- package/test/browser/section.check.mjs +21 -0
- package/test/product-session.test.ts +73 -6
- package/test/save-conflict.test.ts +41 -0
- package/test/save-landed.test.ts +75 -0
- package/translations/en.json +12 -1
- package/translations/es.json +12 -1
- package/translations/ja.json +12 -1
- package/translations/ko.json +12 -1
- package/translations/ms.json +12 -1
- package/translations/zh.json +12 -1
|
@@ -52,6 +52,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
52
52
|
category?: string;
|
|
53
53
|
tags?: string[];
|
|
54
54
|
};
|
|
55
|
+
catalogWritten: {
|
|
56
|
+
type?: string;
|
|
57
|
+
category?: string;
|
|
58
|
+
tags?: string[];
|
|
59
|
+
};
|
|
60
|
+
conflict: {
|
|
61
|
+
latest: string | undefined;
|
|
62
|
+
} | undefined;
|
|
55
63
|
id: string | undefined;
|
|
56
64
|
creationId: string | undefined;
|
|
57
65
|
loading: boolean;
|
|
@@ -74,6 +82,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
74
82
|
category?: string;
|
|
75
83
|
tags?: string[];
|
|
76
84
|
};
|
|
85
|
+
catalogWritten: {
|
|
86
|
+
type?: string;
|
|
87
|
+
category?: string;
|
|
88
|
+
tags?: string[];
|
|
89
|
+
};
|
|
90
|
+
conflict: {
|
|
91
|
+
latest: string | undefined;
|
|
92
|
+
} | undefined;
|
|
77
93
|
id: string | undefined;
|
|
78
94
|
creationId: string | undefined;
|
|
79
95
|
loading: boolean;
|
|
@@ -101,7 +117,10 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
101
117
|
confirmedSource: () => string | undefined;
|
|
102
118
|
/**
|
|
103
119
|
* A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no
|
|
104
|
-
* change. Only a saved figure has a row to write: a new figure's type goes with its first save.
|
|
120
|
+
* change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save
|
|
121
|
+
* is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save
|
|
122
|
+
* is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;
|
|
123
|
+
* the save done lets go only of a value it wrote (2026-11-01 second re-check).
|
|
105
124
|
*/
|
|
106
125
|
editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch): {
|
|
107
126
|
dirty: boolean;
|
|
@@ -110,6 +129,148 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
110
129
|
category?: string;
|
|
111
130
|
tags?: string[];
|
|
112
131
|
};
|
|
132
|
+
catalogWritten: {
|
|
133
|
+
type?: string;
|
|
134
|
+
category?: string;
|
|
135
|
+
tags?: string[];
|
|
136
|
+
};
|
|
137
|
+
conflict: {
|
|
138
|
+
latest: string | undefined;
|
|
139
|
+
} | undefined;
|
|
140
|
+
id: string | undefined;
|
|
141
|
+
creationId: string | undefined;
|
|
142
|
+
loading: boolean;
|
|
143
|
+
saving: boolean;
|
|
144
|
+
error: string | undefined;
|
|
145
|
+
context: null;
|
|
146
|
+
name: null;
|
|
147
|
+
fields: never[];
|
|
148
|
+
geometry: never[];
|
|
149
|
+
bindings: never[];
|
|
150
|
+
revision: null;
|
|
151
|
+
canUndo?: undefined;
|
|
152
|
+
canRedo?: undefined;
|
|
153
|
+
palette?: undefined;
|
|
154
|
+
asset?: undefined;
|
|
155
|
+
} | {
|
|
156
|
+
dirty: boolean;
|
|
157
|
+
catalog: {
|
|
158
|
+
type?: string;
|
|
159
|
+
category?: string;
|
|
160
|
+
tags?: string[];
|
|
161
|
+
};
|
|
162
|
+
catalogWritten: {
|
|
163
|
+
type?: string;
|
|
164
|
+
category?: string;
|
|
165
|
+
tags?: string[];
|
|
166
|
+
};
|
|
167
|
+
conflict: {
|
|
168
|
+
latest: string | undefined;
|
|
169
|
+
} | undefined;
|
|
170
|
+
id: string | undefined;
|
|
171
|
+
creationId: string | undefined;
|
|
172
|
+
loading: boolean;
|
|
173
|
+
saving: boolean;
|
|
174
|
+
error: string | undefined;
|
|
175
|
+
context: string;
|
|
176
|
+
name: string;
|
|
177
|
+
revision: number;
|
|
178
|
+
canUndo: boolean;
|
|
179
|
+
canRedo: boolean;
|
|
180
|
+
fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
|
|
181
|
+
value: number;
|
|
182
|
+
}>;
|
|
183
|
+
asset: import("@hatiolab/figure-model").FigureAsset;
|
|
184
|
+
geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
|
|
185
|
+
bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
|
|
186
|
+
palette: Record<string, string>;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes
|
|
190
|
+
* through only while the server still holds it -- a third change meanwhile is a conflict again.
|
|
191
|
+
*/
|
|
192
|
+
overwriteConflict(): {
|
|
193
|
+
dirty: boolean;
|
|
194
|
+
catalog: {
|
|
195
|
+
type?: string;
|
|
196
|
+
category?: string;
|
|
197
|
+
tags?: string[];
|
|
198
|
+
};
|
|
199
|
+
catalogWritten: {
|
|
200
|
+
type?: string;
|
|
201
|
+
category?: string;
|
|
202
|
+
tags?: string[];
|
|
203
|
+
};
|
|
204
|
+
conflict: {
|
|
205
|
+
latest: string | undefined;
|
|
206
|
+
} | undefined;
|
|
207
|
+
id: string | undefined;
|
|
208
|
+
creationId: string | undefined;
|
|
209
|
+
loading: boolean;
|
|
210
|
+
saving: boolean;
|
|
211
|
+
error: string | undefined;
|
|
212
|
+
context: null;
|
|
213
|
+
name: null;
|
|
214
|
+
fields: never[];
|
|
215
|
+
geometry: never[];
|
|
216
|
+
bindings: never[];
|
|
217
|
+
revision: null;
|
|
218
|
+
canUndo?: undefined;
|
|
219
|
+
canRedo?: undefined;
|
|
220
|
+
palette?: undefined;
|
|
221
|
+
asset?: undefined;
|
|
222
|
+
} | {
|
|
223
|
+
dirty: boolean;
|
|
224
|
+
catalog: {
|
|
225
|
+
type?: string;
|
|
226
|
+
category?: string;
|
|
227
|
+
tags?: string[];
|
|
228
|
+
};
|
|
229
|
+
catalogWritten: {
|
|
230
|
+
type?: string;
|
|
231
|
+
category?: string;
|
|
232
|
+
tags?: string[];
|
|
233
|
+
};
|
|
234
|
+
conflict: {
|
|
235
|
+
latest: string | undefined;
|
|
236
|
+
} | undefined;
|
|
237
|
+
id: string | undefined;
|
|
238
|
+
creationId: string | undefined;
|
|
239
|
+
loading: boolean;
|
|
240
|
+
saving: boolean;
|
|
241
|
+
error: string | undefined;
|
|
242
|
+
context: string;
|
|
243
|
+
name: string;
|
|
244
|
+
revision: number;
|
|
245
|
+
canUndo: boolean;
|
|
246
|
+
canRedo: boolean;
|
|
247
|
+
fields: Array<import("@hatiolab/figure-model").FigureAsset["document"]["model"]["inputs"][number] & {
|
|
248
|
+
value: number;
|
|
249
|
+
}>;
|
|
250
|
+
asset: import("@hatiolab/figure-model").FigureAsset;
|
|
251
|
+
geometry: import("@hatiolab/figure-model").FigureAssetGeometry[];
|
|
252
|
+
bindings: import("@hatiolab/figure-model").FigureEvaluatedBinding[];
|
|
253
|
+
palette: Record<string, string>;
|
|
254
|
+
};
|
|
255
|
+
/**
|
|
256
|
+
* After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation
|
|
257
|
+
* id, saved by the first save's question; the figure they came from is left as the server holds it.
|
|
258
|
+
*/
|
|
259
|
+
detachAsCopy(): {
|
|
260
|
+
dirty: boolean;
|
|
261
|
+
catalog: {
|
|
262
|
+
type?: string;
|
|
263
|
+
category?: string;
|
|
264
|
+
tags?: string[];
|
|
265
|
+
};
|
|
266
|
+
catalogWritten: {
|
|
267
|
+
type?: string;
|
|
268
|
+
category?: string;
|
|
269
|
+
tags?: string[];
|
|
270
|
+
};
|
|
271
|
+
conflict: {
|
|
272
|
+
latest: string | undefined;
|
|
273
|
+
} | undefined;
|
|
113
274
|
id: string | undefined;
|
|
114
275
|
creationId: string | undefined;
|
|
115
276
|
loading: boolean;
|
|
@@ -132,6 +293,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
132
293
|
category?: string;
|
|
133
294
|
tags?: string[];
|
|
134
295
|
};
|
|
296
|
+
catalogWritten: {
|
|
297
|
+
type?: string;
|
|
298
|
+
category?: string;
|
|
299
|
+
tags?: string[];
|
|
300
|
+
};
|
|
301
|
+
conflict: {
|
|
302
|
+
latest: string | undefined;
|
|
303
|
+
} | undefined;
|
|
135
304
|
id: string | undefined;
|
|
136
305
|
creationId: string | undefined;
|
|
137
306
|
loading: boolean;
|
|
@@ -161,6 +330,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
161
330
|
category?: string;
|
|
162
331
|
tags?: string[];
|
|
163
332
|
};
|
|
333
|
+
catalogWritten: {
|
|
334
|
+
type?: string;
|
|
335
|
+
category?: string;
|
|
336
|
+
tags?: string[];
|
|
337
|
+
};
|
|
338
|
+
conflict: {
|
|
339
|
+
latest: string | undefined;
|
|
340
|
+
} | undefined;
|
|
164
341
|
id: string | undefined;
|
|
165
342
|
creationId: string | undefined;
|
|
166
343
|
loading: boolean;
|
|
@@ -183,6 +360,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
183
360
|
category?: string;
|
|
184
361
|
tags?: string[];
|
|
185
362
|
};
|
|
363
|
+
catalogWritten: {
|
|
364
|
+
type?: string;
|
|
365
|
+
category?: string;
|
|
366
|
+
tags?: string[];
|
|
367
|
+
};
|
|
368
|
+
conflict: {
|
|
369
|
+
latest: string | undefined;
|
|
370
|
+
} | undefined;
|
|
186
371
|
id: string | undefined;
|
|
187
372
|
creationId: string | undefined;
|
|
188
373
|
loading: boolean;
|
|
@@ -210,6 +395,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
210
395
|
category?: string;
|
|
211
396
|
tags?: string[];
|
|
212
397
|
};
|
|
398
|
+
catalogWritten: {
|
|
399
|
+
type?: string;
|
|
400
|
+
category?: string;
|
|
401
|
+
tags?: string[];
|
|
402
|
+
};
|
|
403
|
+
conflict: {
|
|
404
|
+
latest: string | undefined;
|
|
405
|
+
} | undefined;
|
|
213
406
|
id: string | undefined;
|
|
214
407
|
creationId: string | undefined;
|
|
215
408
|
loading: boolean;
|
|
@@ -232,6 +425,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
232
425
|
category?: string;
|
|
233
426
|
tags?: string[];
|
|
234
427
|
};
|
|
428
|
+
catalogWritten: {
|
|
429
|
+
type?: string;
|
|
430
|
+
category?: string;
|
|
431
|
+
tags?: string[];
|
|
432
|
+
};
|
|
433
|
+
conflict: {
|
|
434
|
+
latest: string | undefined;
|
|
435
|
+
} | undefined;
|
|
235
436
|
id: string | undefined;
|
|
236
437
|
creationId: string | undefined;
|
|
237
438
|
loading: boolean;
|
|
@@ -259,6 +460,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
259
460
|
category?: string;
|
|
260
461
|
tags?: string[];
|
|
261
462
|
};
|
|
463
|
+
catalogWritten: {
|
|
464
|
+
type?: string;
|
|
465
|
+
category?: string;
|
|
466
|
+
tags?: string[];
|
|
467
|
+
};
|
|
468
|
+
conflict: {
|
|
469
|
+
latest: string | undefined;
|
|
470
|
+
} | undefined;
|
|
262
471
|
id: string | undefined;
|
|
263
472
|
creationId: string | undefined;
|
|
264
473
|
loading: boolean;
|
|
@@ -281,6 +490,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
281
490
|
category?: string;
|
|
282
491
|
tags?: string[];
|
|
283
492
|
};
|
|
493
|
+
catalogWritten: {
|
|
494
|
+
type?: string;
|
|
495
|
+
category?: string;
|
|
496
|
+
tags?: string[];
|
|
497
|
+
};
|
|
498
|
+
conflict: {
|
|
499
|
+
latest: string | undefined;
|
|
500
|
+
} | undefined;
|
|
284
501
|
id: string | undefined;
|
|
285
502
|
creationId: string | undefined;
|
|
286
503
|
loading: boolean;
|
|
@@ -574,6 +791,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
574
791
|
category?: string;
|
|
575
792
|
tags?: string[];
|
|
576
793
|
};
|
|
794
|
+
catalogWritten: {
|
|
795
|
+
type?: string;
|
|
796
|
+
category?: string;
|
|
797
|
+
tags?: string[];
|
|
798
|
+
};
|
|
799
|
+
conflict: {
|
|
800
|
+
latest: string | undefined;
|
|
801
|
+
} | undefined;
|
|
577
802
|
id: string | undefined;
|
|
578
803
|
creationId: string | undefined;
|
|
579
804
|
loading: boolean;
|
|
@@ -596,6 +821,14 @@ export declare function createFigureProductSession(store: FigureProductStore): {
|
|
|
596
821
|
category?: string;
|
|
597
822
|
tags?: string[];
|
|
598
823
|
};
|
|
824
|
+
catalogWritten: {
|
|
825
|
+
type?: string;
|
|
826
|
+
category?: string;
|
|
827
|
+
tags?: string[];
|
|
828
|
+
};
|
|
829
|
+
conflict: {
|
|
830
|
+
latest: string | undefined;
|
|
831
|
+
} | undefined;
|
|
599
832
|
id: string | undefined;
|
|
600
833
|
creationId: string | undefined;
|
|
601
834
|
loading: boolean;
|
|
@@ -22,8 +22,21 @@ export function createFigureProductSession(store) {
|
|
|
22
22
|
undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.
|
|
23
23
|
*/
|
|
24
24
|
let done = [], undone = [];
|
|
25
|
+
/*
|
|
26
|
+
A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of
|
|
27
|
+
this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person
|
|
28
|
+
says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.
|
|
29
|
+
*/
|
|
30
|
+
let conflict;
|
|
25
31
|
/* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */
|
|
26
32
|
let catalog = {};
|
|
33
|
+
/* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */
|
|
34
|
+
let catalogSending;
|
|
35
|
+
/*
|
|
36
|
+
The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares
|
|
37
|
+
with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.
|
|
38
|
+
*/
|
|
39
|
+
let catalogWritten = {};
|
|
27
40
|
const catalogHeld = () => Object.keys(catalog).length > 0;
|
|
28
41
|
/* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */
|
|
29
42
|
let fitted = [];
|
|
@@ -51,7 +64,7 @@ export function createFigureProductSession(store) {
|
|
|
51
64
|
*/
|
|
52
65
|
function read() {
|
|
53
66
|
const view = workspace.read();
|
|
54
|
-
return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, id, creationId, loading, saving, error };
|
|
67
|
+
return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error };
|
|
55
68
|
}
|
|
56
69
|
/*
|
|
57
70
|
The version on screen, one object for as long as it is the version: the workspace hands out one asset per version
|
|
@@ -109,6 +122,8 @@ export function createFigureProductSession(store) {
|
|
|
109
122
|
saving = false;
|
|
110
123
|
creationId = undefined;
|
|
111
124
|
catalog = {};
|
|
125
|
+
catalogWritten = {};
|
|
126
|
+
conflict = undefined;
|
|
112
127
|
done = [];
|
|
113
128
|
undone = [];
|
|
114
129
|
}
|
|
@@ -129,16 +144,17 @@ export function createFigureProductSession(store) {
|
|
|
129
144
|
confirmedSource: () => (id ? baseline : undefined),
|
|
130
145
|
/**
|
|
131
146
|
* A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no
|
|
132
|
-
* change. Only a saved figure has a row to write: a new figure's type goes with its first save.
|
|
147
|
+
* change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save
|
|
148
|
+
* is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save
|
|
149
|
+
* is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;
|
|
150
|
+
* the save done lets go only of a value it wrote (2026-11-01 second re-check).
|
|
133
151
|
*/
|
|
134
152
|
editCatalog(patch, saved) {
|
|
135
153
|
if (!id)
|
|
136
154
|
throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.');
|
|
137
|
-
if (saving)
|
|
138
|
-
throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
|
|
139
155
|
const next = { ...catalog };
|
|
140
156
|
for (const key of Object.keys(patch)) {
|
|
141
|
-
if (sameValue(patch[key], saved[key]))
|
|
157
|
+
if (!(catalogSending && key in catalogSending) && sameValue(patch[key], saved[key]))
|
|
142
158
|
delete next[key];
|
|
143
159
|
else
|
|
144
160
|
next[key] = patch[key];
|
|
@@ -146,6 +162,39 @@ export function createFigureProductSession(store) {
|
|
|
146
162
|
catalog = next;
|
|
147
163
|
return read();
|
|
148
164
|
},
|
|
165
|
+
/**
|
|
166
|
+
* After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes
|
|
167
|
+
* through only while the server still holds it -- a third change meanwhile is a conflict again.
|
|
168
|
+
*/
|
|
169
|
+
overwriteConflict() {
|
|
170
|
+
if (saving)
|
|
171
|
+
throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
|
|
172
|
+
if (!conflict?.source)
|
|
173
|
+
throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.');
|
|
174
|
+
baseline = conflict.source;
|
|
175
|
+
conflict = undefined;
|
|
176
|
+
unconfirmed.clear();
|
|
177
|
+
return read();
|
|
178
|
+
},
|
|
179
|
+
/**
|
|
180
|
+
* After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation
|
|
181
|
+
* id, saved by the first save's question; the figure they came from is left as the server holds it.
|
|
182
|
+
*/
|
|
183
|
+
detachAsCopy() {
|
|
184
|
+
if (saving)
|
|
185
|
+
throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.');
|
|
186
|
+
if (!conflict)
|
|
187
|
+
throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.');
|
|
188
|
+
id = undefined;
|
|
189
|
+
baseline = undefined;
|
|
190
|
+
creationId = crypto.randomUUID();
|
|
191
|
+
conflict = undefined;
|
|
192
|
+
/* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */
|
|
193
|
+
const { type: _type, ...kept } = catalog;
|
|
194
|
+
catalog = kept;
|
|
195
|
+
unconfirmed.clear();
|
|
196
|
+
return read();
|
|
197
|
+
},
|
|
149
198
|
/** A new figure, named in the screen's language by the page: the session has no words of its own. */
|
|
150
199
|
startNew({ discard = false, name }) {
|
|
151
200
|
canLeave(discard);
|
|
@@ -294,6 +343,8 @@ export function createFigureProductSession(store) {
|
|
|
294
343
|
const proposalKeys = inEffect();
|
|
295
344
|
const sentCatalog = { ...catalog };
|
|
296
345
|
saving = true;
|
|
346
|
+
catalogSending = sentCatalog;
|
|
347
|
+
catalogWritten = {};
|
|
297
348
|
try {
|
|
298
349
|
/* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */
|
|
299
350
|
if (id === undefined || workspace.read().dirty)
|
|
@@ -304,13 +355,19 @@ export function createFigureProductSession(store) {
|
|
|
304
355
|
await store.updateCatalog(id, sentCatalog);
|
|
305
356
|
if (requestEpoch !== epoch)
|
|
306
357
|
return read();
|
|
307
|
-
|
|
308
|
-
|
|
358
|
+
catalogWritten = sentCatalog;
|
|
359
|
+
/* What was sent is saved; a field changed again while it was on its way stays held. */
|
|
360
|
+
const next = { ...catalog };
|
|
361
|
+
for (const key of Object.keys(sentCatalog))
|
|
362
|
+
if (sameValue(next[key], sentCatalog[key]))
|
|
363
|
+
delete next[key];
|
|
364
|
+
catalog = next;
|
|
309
365
|
}
|
|
310
366
|
}
|
|
311
367
|
finally {
|
|
312
368
|
if (requestEpoch === epoch)
|
|
313
369
|
saving = false;
|
|
370
|
+
catalogSending = undefined;
|
|
314
371
|
}
|
|
315
372
|
return read();
|
|
316
373
|
async function saveSource() {
|
|
@@ -354,6 +411,8 @@ export function createFigureProductSession(store) {
|
|
|
354
411
|
else {
|
|
355
412
|
if (code === 'SAVE_UNREACHABLE')
|
|
356
413
|
unconfirmed.set(exported.text, { thumbnail });
|
|
414
|
+
if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch)
|
|
415
|
+
conflict = { source: landed?.source };
|
|
357
416
|
throw error;
|
|
358
417
|
}
|
|
359
418
|
}
|
|
@@ -367,6 +426,7 @@ export function createFigureProductSession(store) {
|
|
|
367
426
|
id = record.id;
|
|
368
427
|
baseline = record.source;
|
|
369
428
|
unconfirmed.clear();
|
|
429
|
+
conflict = undefined;
|
|
370
430
|
/*
|
|
371
431
|
Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays
|
|
372
432
|
unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/**\n * The row's catalogue fields -- the type (before release), the category, the tags. Kept on the row, not in the asset,\n * and saved with it by 「저장」: written at once on change, they stayed after 「저장하지 않고 떠나기」 and the save\n * button never lit for them (2026-11-01 audit item 5).\n */\nexport interface FigureCatalogPatch {\n type?: string\n category?: string\n tags?: string[]\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n /** Writes the row's catalogue fields of a saved figure. */\n updateCatalog(id: string, patch: FigureCatalogPatch): Promise<unknown>\n}\n\nconst sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */\n let catalog: FigureCatalogPatch = {}\n const catalogHeld = () => Object.keys(catalog).length > 0\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n const view = workspace.read()\n return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n catalog = {}\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if ((workspace.read().dirty || catalogHeld()) && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /**\n * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no\n * change. Only a saved figure has a row to write: a new figure's type goes with its first save.\n */\n editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {\n if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n const next = { ...catalog }\n for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {\n if (sameValue(patch[key], saved[key])) delete next[key]\n else (next as Record<string, unknown>)[key] = patch[key]\n }\n catalog = next\n return read()\n },\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n const sentCatalog = { ...catalog }\n saving = true\n try {\n /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */\n if (id === undefined || workspace.read().dirty) await saveSource()\n if (requestEpoch !== epoch) return read()\n if (id !== undefined && Object.keys(sentCatalog).length) {\n await store.updateCatalog(id, sentCatalog)\n if (requestEpoch !== epoch) return read()\n /* Nothing could change it on the way: an edit waits for the save (editCatalog). */\n catalog = {}\n }\n } finally {\n if (requestEpoch === epoch) saving = false\n }\n return read()\n\n async function saveSource() {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n }\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
|
|
1
|
+
{"version":3,"file":"product-session.js","sourceRoot":"","sources":["../../client/modeller/product-session.ts"],"names":[],"mappings":"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","sourcesContent":["import { thumbnailHashOf } from './thumbnail-hash.js'\nimport { revisionFromView, type FigureRevision } from './revision.js'\nimport {\n createFigureAsset,\n createFigureEditorWorkspace,\n serializeFigureAsset,\n parseFigureAsset,\n figureSizeRangeFitsOf,\n type FigureSizeRangeFit,\n type FigureInputChange,\n type FigurePrimitiveCreation,\n type FigurePartEdit,\n type FigureAuthoringAction,\n type FigureProposalStep,\n type FigureClipConsent\n} from '@hatiolab/figure-model'\n\n/** Consent to let clips go, and the figure and version it was given on. */\n\n/**\n * A request the session refuses, with its code: a screen says it from the code (`sessionRefusalOf`), never from the\n * message, which stays for logs (ADR-0054; figure audit 2026-10-16 N6 -- an AI card put the message in its note).\n */\nfunction refusal(code: string, message: string): Error & { code: string } {\n return Object.assign(Error(message), { code })\n}\n\nexport interface FigureClipConsentFor extends FigureClipConsent {\n context: string\n revision: number\n}\n\nexport interface FigureProductRecord {\n id: string\n source: string\n}\n\n/**\n * The row's catalogue fields -- the type (before release), the category, the tags. Kept on the row, not in the asset,\n * and saved with it by 「저장」: written at once on change, they stayed after 「저장하지 않고 떠나기」 and the save\n * button never lit for them (2026-11-01 audit item 5).\n */\nexport interface FigureCatalogPatch {\n type?: string\n category?: string\n tags?: string[]\n}\n\n/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */\nexport interface FigureProductStore {\n load(id: string): Promise<FigureProductRecord>\n /** Every field a save writes, as the server holds it now: to tell whether a save whose answer was lost landed. */\n readBack?(id: string): Promise<FigureProductRecord & { thumbnailHash?: string; type?: string }>\n save(request: {\n id?: string\n source: string\n expectedSource?: string\n creationId?: string\n /** The card's picture. Absent when the browser could not draw one; the source is saved either way. */\n thumbnail?: string\n /** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */\n type?: string\n /** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */\n namespace?: string\n /** Keys of the AI proposals whose steps the source holds: the server records them with the save. */\n proposalKeys?: string[]\n }): Promise<FigureProductRecord>\n /** Writes the row's catalogue fields of a saved figure. */\n updateCatalog(id: string, patch: FigureCatalogPatch): Promise<unknown>\n}\n\nconst sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)\n\n/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */\nexport function createFigureProductSession(store: FigureProductStore) {\n const workspace = createFigureEditorWorkspace()\n let epoch = 0,\n id: string | undefined,\n baseline: string | undefined\n let loading = false,\n saving = false,\n error: string | undefined\n let creationId: string | undefined\n /*\n The history as steps, each tagged with the AI proposal it came from (or nothing): mirrored here, since the\n workspace's history does not say where a step came from. A proposal is in the model while its step is on `done` --\n undone, it is not; undone and written past, it never comes back. What a save sends as `proposalKeys`.\n */\n let done: Array<string | undefined> = [],\n undone: Array<string | undefined> = []\n /*\n A save refused because the server holds another source than the one opened (FIGURE_SOURCE_CONFLICT, not one of\n this session's own sends): the edits stay, and the server's latest is kept here to compare with and, if the person\n says so, to write over (2026-11-01 audit item 6). Undefined source: the latest could not be read.\n */\n let conflict: { source?: string } | undefined\n /* The catalogue fields changed and not saved yet; none of the history -- undo is the figure's steps. */\n let catalog: FigureCatalogPatch = {}\n /* The catalogue fields a save is writing now: what the row will hold, not what it held, until it is done. */\n let catalogSending: FigureCatalogPatch | undefined\n /*\n The catalogue fields the last save wrote, known to be on the row from the moment it answered: a screen compares\n with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.\n */\n let catalogWritten: FigureCatalogPatch = {}\n const catalogHeld = () => Object.keys(catalog).length > 0\n /* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */\n let fitted: FigureSizeRangeFit[] = []\n function tracked<T>(kind: 'do' | 'undo' | 'redo', run: () => T, tag?: string, setsRange = false): T {\n const was = workspace.read()\n const result = run()\n const now = workspace.read()\n if (now.revision === was.revision) return result\n fitted = kind === 'do' && !setsRange && was.asset && now.asset ? figureSizeRangeFitsOf(was.asset, now.asset) : []\n if (kind === 'do') {\n done.push(tag)\n undone = []\n } else if (kind === 'undo') undone.push(done.pop())\n else done.push(undone.pop())\n return result\n }\n const inEffect = () => [...new Set(done.filter((tag): tag is string => !!tag))]\n /*\n The creation id is read with the rest: a figure not saved yet is named by it (ADR-0061 decision 3) -- its AI\n conversation is anchored on it, and its first save inserts the row under it, so the name holds across that save.\n */\n function read() {\n const view = workspace.read()\n return { ...view, dirty: view.dirty || catalogHeld(), catalog: { ...catalog }, catalogWritten: { ...catalogWritten }, conflict: conflict ? { latest: conflict.source } : undefined, id, creationId, loading, saving, error }\n }\n /*\n The version on screen, one object for as long as it is the version: the workspace hands out one asset per version\n (and another once a save is acknowledged, or the figure moves on), so that object says whether the last one holds.\n */\n let shown: FigureRevision | undefined\n function revisionNow(): FigureRevision | undefined {\n const view = workspace.read()\n if (!view.context || view.revision === null) return (shown = undefined)\n const context = view.context\n if (shown?.asset !== view.asset) shown = revisionFromView(view, () => workspace.export({ context }).text)\n return shown\n }\n /* Texts sent whose save was never answered: found on the server, the server is confirmed to hold what was sent. */\n const unconfirmed = new Map<string, { thumbnail?: string }>()\n /*\n Consent to let clips go: given for one edit, on the figure and the version it was asked on. The first edit made under\n it takes it; an edit on another figure or another version is refused rather than let clips go that were never named\n for it (designer 2026-09-27).\n */\n let consent: FigureClipConsentFor | undefined\n const consented = () => {\n if (!consent) return {}\n const given = consent\n consent = undefined\n const view = workspace.read()\n if (view.context !== given.context || view.revision !== given.revision) {\n const stale = Object.assign(Error('확인한 뒤 모델이 바뀌어 적용하지 않았습니다. 다시 해 주세요.'), { code: 'CONSENT_STALE' })\n throw stale\n }\n return { dropClips: given.dropClips }\n }\n /* A model open to read: the draft is read during a save too -- the dock reads it when the person sends (audit N5). */\n function readable() {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return { context: view.context, expectedRevision: view.revision, origin: 'ui' as const }\n }\n function writable() {\n const open = readable()\n // Serializing writes also makes create retries safe: no second create while the first is pending.\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n return open\n }\n function clear() {\n workspace.close({ discardDesignChanges: true })\n id = undefined\n baseline = undefined\n error = undefined\n saving = false\n creationId = undefined\n catalog = {}\n catalogWritten = {}\n conflict = undefined\n done = []\n undone = []\n }\n function canLeave(discard: boolean) {\n if (saving) throw Error('저장이 끝난 뒤 모델을 전환해 주세요.')\n if ((workspace.read().dirty || catalogHeld()) && !discard) throw Error('저장하지 않은 변경이 있습니다.')\n }\n return {\n read,\n /** The figure on screen at its version, for every panel and helper to read; none while nothing is open. Readable while saving. */\n revisionNow,\n /**\n * The source the server is confirmed to hold: loaded, or answered by a save. What a revert sends as `expectedSource`\n * -- the server writes over it only while it still holds this. Nothing for a figure not saved yet.\n */\n confirmedSource: () => (id ? baseline : undefined),\n /**\n * A catalogue field changed, held for the next save; one changed back to what the row holds (`saved`) is no\n * change. Only a saved figure has a row to write: a new figure's type goes with its first save. Taken while a save\n * is on its way too: the field shows it, so it is held for the save after (2026-11-01 re-check). A field that save\n * is writing is held whatever it is changed to -- back to the old value too, which the save is about to replace;\n * the save done lets go only of a value it wrote (2026-11-01 second re-check).\n */\n editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {\n if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')\n const next = { ...catalog }\n for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {\n if (!(catalogSending && key in catalogSending) && sameValue(patch[key], saved[key])) delete next[key]\n else (next as Record<string, unknown>)[key] = patch[key]\n }\n catalog = next\n return read()\n },\n /**\n * After a conflict, the edits written over the server's latest: the next save expects that latest, so it goes\n * through only while the server still holds it -- a third change meanwhile is a conflict again.\n */\n overwriteConflict() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict?.source) throw refusal('SESSION_NO_CONFLICT', '덮어쓸 최신본을 읽지 못했습니다.')\n baseline = conflict.source\n conflict = undefined\n unconfirmed.clear()\n return read()\n },\n /**\n * After a conflict, the edits kept as a figure of their own: from here they are a new figure, under a new creation\n * id, saved by the first save's question; the figure they came from is left as the server holds it.\n */\n detachAsCopy() {\n if (saving) throw refusal('SESSION_SAVING', '저장이 끝난 뒤 편집해 주세요.')\n if (!conflict) throw refusal('SESSION_NO_CONFLICT', '충돌한 저장이 없습니다.')\n id = undefined\n baseline = undefined\n creationId = crypto.randomUUID()\n conflict = undefined\n /* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */\n const { type: _type, ...kept } = catalog\n catalog = kept\n unconfirmed.clear()\n return read()\n },\n /** A new figure, named in the screen's language by the page: the session has no words of its own. */\n startNew({ discard = false, name }: { discard?: boolean; name: string }) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n workspace.open(serializeFigureAsset(createFigureAsset(name)))\n creationId = crypto.randomUUID()\n return read()\n },\n async open(nextId: string, { discard = false } = {}) {\n canLeave(discard)\n const requestEpoch = ++epoch\n clear()\n loading = true\n try {\n const record = await store.load(nextId)\n if (requestEpoch !== epoch) return read()\n if (record.id !== nextId) throw Error('요청한 Figure와 응답의 대상이 다릅니다.')\n workspace.open(record.source)\n id = nextId\n baseline = record.source\n } catch (cause) {\n if (requestEpoch === epoch) error = cause instanceof Error ? cause.message : String(cause)\n } finally {\n if (requestEpoch === epoch) loading = false\n }\n return read()\n },\n close({ discard = false } = {}) {\n canLeave(discard)\n ++epoch\n clear()\n loading = false\n return read()\n },\n apply(changes: FigureInputChange[], origin: 'ui' | 'ai' = 'ui', expectedRevision?: number, context?: string) {\n const control = writable()\n // AI must supply the context AND revision captured when it started. Never substitute current values.\n if (origin === 'ai' && (context === undefined || expectedRevision === undefined))\n throw refusal('SESSION_VERSION_MISSING', 'AI 편집에는 요청 당시의 모델 문맥과 리비전이 필요합니다.')\n return tracked('do', () =>\n workspace.apply({\n ...control,\n origin,\n context: context ?? control.context,\n expectedRevision: expectedRevision ?? control.expectedRevision,\n changes\n })\n )\n },\n /**\n * An AI proposal (ADR-0094): the modeller's commands, as one step of the history, on the version the model saw.\n * Never on the current one instead -- a person's edit in between makes the proposal stale, and it is refused.\n */\n applyProposal(steps: readonly FigureProposalStep[], expectedRevision: number, context: string, key?: string) {\n writable()\n /* Never with consent: only the person may let a clip go, and an AI proposal is not the person (designer 2026-09-27). */\n return tracked('do', () => workspace.applyProposal({ origin: 'ai', context, expectedRevision, steps }), key)\n },\n /**\n * A new figure opened for an AI proposal from another screen is created under the proposal's key (ADR-0097 §5.6\n * item 5). Only before the modeller names the figure to the AI dock: once a conversation is anchored on the creation\n * id it stays (ADR-0061 decision 3), and the save's record of the proposal keeps one proposal one figure. Only on a\n * new figure not yet touched -- one being made is the person's, and keeps its own.\n */\n createUnder(key: string) {\n if (id || workspace.read().dirty) throw refusal('SESSION_NEW_FIGURE_TOUCHED', '손대지 않은 새 모델에만 쓸 수 있습니다.')\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(key)) throw refusal('SESSION_PROPOSAL_KEY', '제안 식별자가 올바르지 않습니다.')\n creationId = key\n },\n /** The AI proposals whose steps are in the model now, each once. */\n proposalsInEffect: inEffect,\n /**\n * A recipe put in by the person (ADR-0095): its resolved commands, as one step of the history, on the version open\n * now. The same path as a proposal; the person is its origin.\n */\n applyRecipe(steps: readonly FigureProposalStep[]) {\n const at = writable()\n return tracked('do', () => workspace.applyProposal({ origin: 'ui', context: at.context, expectedRevision: at.expectedRevision, steps, ...consented() }))\n },\n add(part: FigurePrimitiveCreation) {\n return tracked('do', () => workspace.addPrimitive({ ...writable(), part }))\n },\n edit(edit: FigurePartEdit) {\n return tracked('do', () => workspace.editPart({ ...writable(), edit, ...consented() }))\n },\n /** What a command would do, on a copy (ADR-0097 §5.5): nothing is written, and the answer names its version. */\n previewCommand(action: FigureAuthoringAction) {\n const view = workspace.read()\n if (loading || error || !view.context || view.revision === null) throw refusal('SESSION_NOT_OPEN', '먼저 모델을 정상적으로 열어 주세요.')\n return workspace.previewCommand({ context: view.context, action })\n },\n /** One authoring command — what a person means, not a graph edit — as one step of the same history. */\n command(action: FigureAuthoringAction) {\n return tracked('do', () => workspace.command({ ...writable(), action, ...consented() }), undefined, action.kind === 'declare-instance-size')\n },\n /** The ends of the size on a drawing the last step moved in to what the parts allow, once: then nothing until the next. */\n takeSizeRangeFits(): FigureSizeRangeFit[] {\n const out = fitted\n fitted = []\n return out\n },\n /**\n * The first edit made inside `run` may let go of exactly these clips -- the ones a CLIPS_DROPPED refusal named and\n * the person agreed to -- on the figure and version they were named on (designer 2026-09-27). Any other edit that\n * would stop a clip is refused and asked about.\n */\n withConsent<T>(given: FigureClipConsentFor, run: () => T): T {\n consent = given\n try {\n return run()\n } finally {\n consent = undefined\n }\n },\n undo() {\n return tracked('undo', () => workspace.undo(writable()))\n },\n redo() {\n return tracked('redo', () => workspace.redo(writable()))\n },\n source() {\n const { context } = readable()\n return workspace.export({ context }).text\n },\n /**\n * Saves the model, and the card's picture when the caller managed to take one.\n *\n * The picture is the caller's because taking it needs a canvas, and this session is the place that knows\n * nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the\n * source matters more than the card (see `thumbnail.ts`).\n */\n async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {\n const control = writable(),\n requestEpoch = epoch\n const exported = workspace.export(control)\n /* Read with the text it goes with: an edit while saving is not what this save holds. */\n const proposalKeys = inEffect()\n const sentCatalog = { ...catalog }\n saving = true\n catalogSending = sentCatalog\n catalogWritten = {}\n try {\n /* Only the catalogue changed: the source is not written again -- a released figure would be made a draft for nothing. */\n if (id === undefined || workspace.read().dirty) await saveSource()\n if (requestEpoch !== epoch) return read()\n if (id !== undefined && Object.keys(sentCatalog).length) {\n await store.updateCatalog(id, sentCatalog)\n if (requestEpoch !== epoch) return read()\n catalogWritten = sentCatalog\n /* What was sent is saved; a field changed again while it was on its way stays held. */\n const next = { ...catalog }\n for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[]) if (sameValue(next[key], sentCatalog[key])) delete next[key]\n catalog = next\n }\n } finally {\n if (requestEpoch === epoch) saving = false\n catalogSending = undefined\n }\n return read()\n\n async function saveSource() {\n let record: FigureProductRecord\n try {\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } catch (error) {\n /*\n A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second\n try refused because the first did land -- its expected source is the old one (FIGURE_SOURCE_CONFLICT). Only\n those are read back; a refusal on grounds -- permission, validation -- stays a failure whatever the row\n holds. Read back, every field the save writes must be what was sent: the source, the picture, the type\n (designer 2026-09-27: a network failure alone does not say nothing was saved; nor does a matching source\n alone say it was).\n */\n const code = (error as { code?: string })?.code\n const unknown = code === 'SAVE_UNREACHABLE' || code === 'FIGURE_SOURCE_CONFLICT'\n const landed = unknown && id !== undefined && requestEpoch === epoch && store.readBack ? await store.readBack(id).catch(() => undefined) : undefined\n const held = landed ? serializeFigureAsset(parseFigureAsset(landed.source)) : undefined\n /* The picture is compared by the name the server keeps it under: the hash of its bytes (ADR-0104). */\n const holds = async (sent?: string) => sent === undefined || (!!landed && landed.thumbnailHash !== undefined && landed.thumbnailHash === (await thumbnailHashOf(sent)))\n const same = !!landed && held === exported.text && (await holds(thumbnail)) && (type === undefined || landed.type === type)\n if (same) record = landed!\n else if (\n code === 'FIGURE_SOURCE_CONFLICT' &&\n landed &&\n held !== undefined &&\n unconfirmed.has(held) &&\n /* Every field that earlier save wrote, not the source alone: nothing not compared is written over. */\n (await holds(unconfirmed.get(held)!.thumbnail))\n ) {\n /*\n The row holds a text this session sent and never heard back about -- edited further since, the new text\n is not it. The server is known to hold what was sent: this save is sent again, once, conditional on that\n server text as the expected source (designer 2026-09-27: \"the sent content is confirmed on the server\").\n */\n baseline = landed.source\n unconfirmed.clear()\n record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })\n } else {\n if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })\n if (code === 'FIGURE_SOURCE_CONFLICT' && requestEpoch === epoch) conflict = { source: landed?.source }\n throw error\n }\n }\n if (requestEpoch !== epoch) return read()\n if (!record.id || (id !== undefined && record.id !== id)) throw Error('저장 응답의 Figure 대상이 다릅니다.')\n if (serializeFigureAsset(parseFigureAsset(record.source)) !== exported.text)\n throw Error('저장 응답이 보낸 모델과 다릅니다. 저장 상태를 다시 확인해 주세요.')\n /* The server has it: the next save expects it, whatever happened on screen meanwhile. */\n id = record.id\n baseline = record.source\n unconfirmed.clear()\n conflict = undefined\n /*\n Edited while it was saving: what was sent is saved, the edits since are not -- they stay, and the model stays\n unsaved (the workspace refuses a stale acknowledgement). Never marked saved, never wiped.\n */\n if (workspace.read().revision === exported.revision) workspace.acknowledgeSaved(exported)\n }\n }\n }\n}\nexport type FigureProductSession = ReturnType<typeof createFigureProductSession>\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FigureAsset } from '@hatiolab/figure-model';
|
|
2
|
+
export interface SaveConflictChanges {
|
|
3
|
+
/** Parts on both sides, made differently. */
|
|
4
|
+
changed: string[];
|
|
5
|
+
/** Parts only the edits on screen have. */
|
|
6
|
+
mineOnly: string[];
|
|
7
|
+
/** Parts only the server's latest has. */
|
|
8
|
+
theirsOnly: string[];
|
|
9
|
+
/** Something not of one part differs too: the name, the figure's inputs, its capabilities, its clips... */
|
|
10
|
+
otherChanged: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function saveConflictChangesOf(mine: FigureAsset, theirs: FigureAsset): SaveConflictChanges;
|