@artkit-ai/react 0.1.0 → 0.2.0
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/dist/cache.d.ts +7 -11
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +32 -53
- package/dist/cache.js.map +1 -1
- package/dist/client.d.ts +4 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -29
- package/dist/client.js.map +1 -1
- package/dist/components/card.d.ts +2 -2
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -3
- package/dist/components/card.js.map +1 -1
- package/dist/components/editor.d.ts +31 -18
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +81 -131
- package/dist/components/editor.js.map +1 -1
- package/dist/components/icons.d.ts +2 -2
- package/dist/components/icons.d.ts.map +1 -1
- package/dist/components/icons.js +2 -2
- package/dist/components/icons.js.map +1 -1
- package/dist/components/panel.d.ts +13 -16
- package/dist/components/panel.d.ts.map +1 -1
- package/dist/components/panel.js +13 -97
- package/dist/components/panel.js.map +1 -1
- package/dist/components/shared.d.ts +2 -6
- package/dist/components/shared.d.ts.map +1 -1
- package/dist/components/shared.js +2 -6
- package/dist/components/shared.js.map +1 -1
- package/dist/components/view.d.ts +7 -8
- package/dist/components/view.d.ts.map +1 -1
- package/dist/components/view.js +16 -15
- package/dist/components/view.js.map +1 -1
- package/dist/context.d.ts +14 -9
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js.map +1 -1
- package/dist/editor-session.d.ts +77 -165
- package/dist/editor-session.d.ts.map +1 -1
- package/dist/editor-session.js +164 -586
- package/dist/editor-session.js.map +1 -1
- package/dist/hooks.d.ts +2 -23
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +24 -116
- package/dist/hooks.js.map +1 -1
- package/dist/i18n.d.ts +4 -66
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +6 -68
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -8
- package/dist/index.js.map +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +12 -12
- package/dist/provider.js.map +1 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +4 -4
- package/package.json +2 -2
- package/src/cache.ts +32 -55
- package/src/client.ts +7 -55
- package/src/components/card.tsx +6 -7
- package/src/components/editor.tsx +208 -308
- package/src/components/icons.tsx +2 -2
- package/src/components/panel.tsx +43 -300
- package/src/components/shared.tsx +3 -12
- package/src/components/view.tsx +26 -29
- package/src/context.ts +16 -11
- package/src/editor-session.ts +197 -675
- package/src/hooks.ts +25 -140
- package/src/i18n.ts +6 -68
- package/src/index.ts +7 -39
- package/src/provider.tsx +13 -13
- package/src/styles.ts +4 -4
- package/dist/components/approval.d.ts +0 -34
- package/dist/components/approval.d.ts.map +0 -1
- package/dist/components/approval.js +0 -80
- package/dist/components/approval.js.map +0 -1
- package/dist/components/version-diff.d.ts +0 -35
- package/dist/components/version-diff.d.ts.map +0 -1
- package/dist/components/version-diff.js +0 -85
- package/dist/components/version-diff.js.map +0 -1
- package/dist/line-diff.d.ts +0 -25
- package/dist/line-diff.d.ts.map +0 -1
- package/dist/line-diff.js +0 -96
- package/dist/line-diff.js.map +0 -1
- package/src/components/approval.tsx +0 -219
- package/src/components/version-diff.tsx +0 -266
- package/src/line-diff.ts +0 -111
package/dist/editor-session.js
CHANGED
|
@@ -1,614 +1,192 @@
|
|
|
1
|
-
import { ArtifactError, applyOperations,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
function sameEnvelope(a, b) {
|
|
11
|
-
return (a.title === b.title && a.themeId === b.themeId && a.locale === b.locale && deepEqual(a.content, b.content));
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Operations that turn `from` into `to`. Kinds do not provide inverse operations, so undo and redo send
|
|
15
|
-
* `replace_content` with the snapshot (plus envelope operations); the commit still becomes a new version
|
|
16
|
-
* (spec §8.5).
|
|
17
|
-
*/
|
|
18
|
-
export function operationsBetween(from, to) {
|
|
19
|
-
const ops = [];
|
|
20
|
-
if (!deepEqual(from.content, to.content))
|
|
21
|
-
ops.push({ op: 'replace_content', content: structuredClone(to.content) });
|
|
22
|
-
if (from.title !== to.title)
|
|
23
|
-
ops.push({ op: 'set_title', title: to.title });
|
|
24
|
-
if (from.themeId !== to.themeId)
|
|
25
|
-
ops.push({ op: 'set_theme', themeId: to.themeId });
|
|
26
|
-
if (from.locale !== to.locale)
|
|
27
|
-
ops.push({ op: 'set_locale', locale: to.locale });
|
|
28
|
-
return ops;
|
|
29
|
-
}
|
|
30
|
-
function toArtifactError(error) {
|
|
31
|
-
if (isArtifactError(error))
|
|
32
|
-
return error;
|
|
33
|
-
return new ArtifactError('INTERNAL_ERROR', 'Internal error while processing the request.', {
|
|
34
|
-
cause: error,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
function defaultKey() {
|
|
38
|
-
const cryptoApi = globalThis.crypto;
|
|
39
|
-
if (cryptoApi?.randomUUID)
|
|
40
|
-
return cryptoApi.randomUUID();
|
|
41
|
-
return `ak-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Framework-free editing session behind `ArtifactEditor` (spec §13.6): batching of typing operations,
|
|
45
|
-
* optimistic apply with the kind's own logic, rollback on rejection, local undo/redo over the user's own
|
|
46
|
-
* batches, rebase of pending operations on newer versions, conflict resolution and lock handling.
|
|
47
|
-
*
|
|
48
|
-
* Batches are sent one at a time, each against the version the previous one produced.
|
|
49
|
-
*/
|
|
50
|
-
export class EditorSession {
|
|
51
|
-
options;
|
|
52
|
-
windowMs;
|
|
53
|
-
maxGroupingMs;
|
|
1
|
+
import { ArtifactError, applyOperations, isArtifactError, } from '@artkit-ai/core';
|
|
2
|
+
const DEFAULT_COALESCE_MS = 1000;
|
|
3
|
+
const DEFAULT_MAX_UNDO = 200;
|
|
4
|
+
/** Draft of one artifact with local undo and redo. Batches are applied in order, one at a time. */
|
|
5
|
+
export class DraftSession {
|
|
6
|
+
registry;
|
|
7
|
+
locale;
|
|
8
|
+
coalesceMs;
|
|
54
9
|
maxUndo;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
bufferStartedAt = 0;
|
|
59
|
-
timer;
|
|
60
|
-
lockTimer;
|
|
61
|
-
queue = [];
|
|
62
|
-
inflight = false;
|
|
10
|
+
now;
|
|
11
|
+
base;
|
|
12
|
+
current;
|
|
63
13
|
undoStack = [];
|
|
64
14
|
redoStack = [];
|
|
15
|
+
queue = Promise.resolve();
|
|
16
|
+
pending = 0;
|
|
65
17
|
error;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
deferredRemote;
|
|
69
|
-
event;
|
|
70
|
-
seq = 0;
|
|
71
|
-
disposed = false;
|
|
72
|
-
listeners = new Set();
|
|
73
|
-
idleWaiters = [];
|
|
18
|
+
lint;
|
|
19
|
+
changes = [];
|
|
74
20
|
snapshot;
|
|
21
|
+
listeners = new Set();
|
|
22
|
+
disposed = false;
|
|
75
23
|
constructor(options) {
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.maxUndo = options.maxUndo ??
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
24
|
+
this.registry = options.registry;
|
|
25
|
+
this.locale = options.locale ?? 'en';
|
|
26
|
+
this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
27
|
+
this.maxUndo = options.maxUndo ?? DEFAULT_MAX_UNDO;
|
|
28
|
+
this.now = options.now ?? (() => Date.now());
|
|
29
|
+
this.base = options.artifact;
|
|
30
|
+
this.current = { artifact: options.artifact, operations: [], at: 0, typing: false };
|
|
31
|
+
this.lint = this.lintOf(options.artifact);
|
|
82
32
|
this.snapshot = this.buildSnapshot();
|
|
83
33
|
}
|
|
84
|
-
|
|
34
|
+
/** The current snapshot. Its identity changes only when the state changes. */
|
|
35
|
+
getSnapshot = () => this.snapshot;
|
|
85
36
|
subscribe = (listener) => {
|
|
86
37
|
this.listeners.add(listener);
|
|
87
38
|
return () => this.listeners.delete(listener);
|
|
88
39
|
};
|
|
89
|
-
getSnapshot = () => this.snapshot;
|
|
90
|
-
/** Resolves once nothing is buffered, queued or in flight (or a conflict is waiting for the user). */
|
|
91
|
-
whenIdle() {
|
|
92
|
-
if (this.isIdle())
|
|
93
|
-
return Promise.resolve();
|
|
94
|
-
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
95
|
-
}
|
|
96
|
-
isIdle() {
|
|
97
|
-
return (this.buffer.length === 0 && !this.inflight && (this.queue.length === 0 || this.conflict !== undefined));
|
|
98
|
-
}
|
|
99
|
-
buildSnapshot() {
|
|
100
|
-
const pendingEdits = this.queue.filter((item) => item.type === 'ops' && item.origin !== 'undo' && item.origin !== 'redo').length;
|
|
101
|
-
const queuedUndo = this.queue.filter((item) => item.type === 'undo').length;
|
|
102
|
-
const queuedRedo = this.queue.filter((item) => item.type === 'redo').length;
|
|
103
|
-
const dirty = this.buffer.length > 0 || this.queue.length > 0 || this.inflight;
|
|
104
|
-
let status = 'idle';
|
|
105
|
-
if (this.conflict)
|
|
106
|
-
status = 'conflict';
|
|
107
|
-
else if (this.inflight || this.queue.length > 0)
|
|
108
|
-
status = 'saving';
|
|
109
|
-
else if (this.buffer.length > 0)
|
|
110
|
-
status = 'editing';
|
|
111
|
-
const blocked = this.conflict !== undefined || this.isLocked();
|
|
112
|
-
return {
|
|
113
|
-
confirmed: this.confirmed,
|
|
114
|
-
view: this.view,
|
|
115
|
-
status,
|
|
116
|
-
dirty,
|
|
117
|
-
canUndo: !blocked && this.undoStack.length + pendingEdits + (this.buffer.length > 0 ? 1 : 0) > queuedUndo,
|
|
118
|
-
canRedo: !blocked && this.buffer.length === 0 && pendingEdits === 0 && this.redoStack.length > queuedRedo,
|
|
119
|
-
...(this.error ? { error: this.error } : {}),
|
|
120
|
-
...(this.conflict
|
|
121
|
-
? {
|
|
122
|
-
conflict: {
|
|
123
|
-
reason: this.conflict.reason,
|
|
124
|
-
changes: this.conflict.changes,
|
|
125
|
-
operations: this.conflict.operations,
|
|
126
|
-
latest: this.conflict.latest,
|
|
127
|
-
},
|
|
128
|
-
}
|
|
129
|
-
: {}),
|
|
130
|
-
...(this.lockedUntil ? { lockedUntil: this.lockedUntil } : {}),
|
|
131
|
-
...(this.event ? { event: this.event } : {}),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
notify() {
|
|
135
|
-
this.snapshot = this.buildSnapshot();
|
|
136
|
-
for (const listener of [...this.listeners])
|
|
137
|
-
listener();
|
|
138
|
-
if (this.isIdle() && this.idleWaiters.length > 0) {
|
|
139
|
-
const waiters = this.idleWaiters;
|
|
140
|
-
this.idleWaiters = [];
|
|
141
|
-
for (const resolve of waiters)
|
|
142
|
-
resolve();
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
announce(event) {
|
|
146
|
-
this.seq += 1;
|
|
147
|
-
this.event = { ...event, seq: this.seq };
|
|
148
|
-
}
|
|
149
|
-
isLocked() {
|
|
150
|
-
return this.lockedUntil !== undefined && Date.parse(this.lockedUntil) > Date.now();
|
|
151
|
-
}
|
|
152
|
-
// -- local application -----------------------------------------------------------------------------
|
|
153
|
-
applyLocal(state, ops) {
|
|
154
|
-
const result = applyOperations(this.options.registry, { kind: this.confirmed.kind, ...state }, ops, this.options.locale ? { locale: this.options.locale } : {});
|
|
155
|
-
return { content: result.content, title: result.title, themeId: result.themeId, locale: result.locale };
|
|
156
|
-
}
|
|
157
|
-
pendingOperations() {
|
|
158
|
-
const ops = [];
|
|
159
|
-
for (const item of this.queue)
|
|
160
|
-
if (item.type === 'ops')
|
|
161
|
-
ops.push(...item.ops);
|
|
162
|
-
ops.push(...this.buffer);
|
|
163
|
-
return ops;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Recomputes the optimistic view from the confirmed version. When the unconfirmed operations no
|
|
167
|
-
* longer apply, they move into a `rebase` conflict and the view falls back to the confirmed version.
|
|
168
|
-
*/
|
|
169
|
-
rebaseView(previous) {
|
|
170
|
-
let state = envelopeOf(this.confirmed);
|
|
171
|
-
try {
|
|
172
|
-
for (const item of this.queue)
|
|
173
|
-
if (item.type === 'ops')
|
|
174
|
-
state = this.applyLocal(state, item.ops);
|
|
175
|
-
if (this.buffer.length > 0)
|
|
176
|
-
state = this.applyLocal(state, this.buffer);
|
|
177
|
-
this.view = state;
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
const operations = this.pendingOperations();
|
|
181
|
-
this.clearTimer();
|
|
182
|
-
this.buffer = [];
|
|
183
|
-
// The in-flight batch (if any) is already at the server and settles on its own.
|
|
184
|
-
this.queue = this.inflight ? this.queue.slice(0, 1) : [];
|
|
185
|
-
this.view = envelopeOf(this.confirmed);
|
|
186
|
-
this.conflict = {
|
|
187
|
-
reason: 'rebase',
|
|
188
|
-
changes: this.changesBetween(previous, this.confirmed),
|
|
189
|
-
operations,
|
|
190
|
-
latest: this.confirmed,
|
|
191
|
-
origin: 'edit',
|
|
192
|
-
};
|
|
193
|
-
this.announce({ type: 'conflict', reason: 'rebase' });
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
changesBetween(before, after) {
|
|
197
|
-
const changes = [];
|
|
198
|
-
if (before.title !== after.title)
|
|
199
|
-
changes.push({
|
|
200
|
-
type: 'changed',
|
|
201
|
-
target: 'envelope:title',
|
|
202
|
-
summary: `'${before.title}' → '${after.title}'`,
|
|
203
|
-
});
|
|
204
|
-
if (before.themeId !== after.themeId)
|
|
205
|
-
changes.push({
|
|
206
|
-
type: 'changed',
|
|
207
|
-
target: 'envelope:themeId',
|
|
208
|
-
summary: `${before.themeId} → ${after.themeId}`,
|
|
209
|
-
});
|
|
210
|
-
if (before.locale !== after.locale)
|
|
211
|
-
changes.push({
|
|
212
|
-
type: 'changed',
|
|
213
|
-
target: 'envelope:locale',
|
|
214
|
-
summary: `${before.locale} → ${after.locale}`,
|
|
215
|
-
});
|
|
216
|
-
try {
|
|
217
|
-
changes.push(...this.options.registry.get(this.confirmed.kind).diff(before.content, after.content));
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
// A diff failure must not hide the conflict itself.
|
|
221
|
-
}
|
|
222
|
-
return changes;
|
|
223
|
-
}
|
|
224
|
-
// -- editing ---------------------------------------------------------------------------------------
|
|
225
40
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
41
|
+
* Applies a batch to the draft. Resolves to `true` when the batch was accepted. A rejected batch leaves the draft
|
|
42
|
+
* untouched and sets `snapshot.error`.
|
|
228
43
|
*/
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
44
|
+
apply(operations, options = {}) {
|
|
45
|
+
if (operations.length === 0)
|
|
46
|
+
return Promise.resolve(true);
|
|
47
|
+
this.pending += 1;
|
|
48
|
+
this.publish();
|
|
49
|
+
const run = this.queue.then(async () => {
|
|
50
|
+
if (this.disposed)
|
|
51
|
+
return false;
|
|
52
|
+
const typing = options.kind === 'typing' || options.kind === 'continuous';
|
|
53
|
+
try {
|
|
54
|
+
const result = await applyOperations(this.registry, this.current.artifact, operations, {
|
|
55
|
+
locale: this.locale,
|
|
56
|
+
});
|
|
57
|
+
const next = {
|
|
58
|
+
...this.current.artifact,
|
|
59
|
+
formatVersion: result.formatVersion,
|
|
60
|
+
title: result.title,
|
|
61
|
+
locale: result.locale,
|
|
62
|
+
themeId: result.themeId,
|
|
63
|
+
content: result.content,
|
|
64
|
+
};
|
|
65
|
+
const now = this.now();
|
|
66
|
+
const coalesce = typing && this.current.typing && now - this.current.at <= this.coalesceMs;
|
|
67
|
+
if (!coalesce) {
|
|
68
|
+
this.undoStack.push(this.current);
|
|
69
|
+
if (this.undoStack.length > this.maxUndo)
|
|
70
|
+
this.undoStack.shift();
|
|
71
|
+
}
|
|
72
|
+
this.redoStack = [];
|
|
73
|
+
this.current = {
|
|
74
|
+
artifact: next,
|
|
75
|
+
operations: [...this.current.operations, ...operations],
|
|
76
|
+
at: now,
|
|
77
|
+
typing,
|
|
78
|
+
};
|
|
79
|
+
this.lint = result.lint;
|
|
80
|
+
this.changes = result.diff;
|
|
81
|
+
this.error = undefined;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
this.error = errorOf(error);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
this.queue = run.finally(() => {
|
|
90
|
+
this.pending -= 1;
|
|
91
|
+
this.publish();
|
|
92
|
+
});
|
|
93
|
+
return run.then((accepted) => {
|
|
94
|
+
this.publish();
|
|
95
|
+
return accepted;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Steps back one edit (local). Returns false when there is nothing to undo. */
|
|
99
|
+
undo() {
|
|
100
|
+
const previous = this.undoStack.pop();
|
|
101
|
+
if (!previous)
|
|
243
102
|
return false;
|
|
244
|
-
|
|
245
|
-
this.
|
|
246
|
-
this.redoStack = [];
|
|
247
|
-
const maxOps = this.options.registry.batch.maxOperations;
|
|
248
|
-
if (options.kind === 'typing') {
|
|
249
|
-
if (this.buffer.length + operations.length > maxOps)
|
|
250
|
-
this.flushBuffer();
|
|
251
|
-
if (this.buffer.length === 0)
|
|
252
|
-
this.bufferStartedAt = Date.now();
|
|
253
|
-
this.buffer.push(...operations);
|
|
254
|
-
if (Date.now() - this.bufferStartedAt >= this.maxGroupingMs)
|
|
255
|
-
this.flushBuffer();
|
|
256
|
-
else
|
|
257
|
-
this.schedule();
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
// Discrete operations and the end of continuous gestures flush right away, after pending typing.
|
|
261
|
-
this.flushBuffer();
|
|
262
|
-
this.queue.push({ type: 'ops', ops: operations, key: this.key(), origin: 'edit' });
|
|
263
|
-
}
|
|
264
|
-
this.pump();
|
|
265
|
-
this.notify();
|
|
103
|
+
this.redoStack.push(this.current);
|
|
104
|
+
this.restore(previous);
|
|
266
105
|
return true;
|
|
267
|
-
};
|
|
268
|
-
/** Sends buffered typing operations now (e.g. on blur or before navigation). */
|
|
269
|
-
flush() {
|
|
270
|
-
if (this.buffer.length === 0)
|
|
271
|
-
return;
|
|
272
|
-
this.flushBuffer();
|
|
273
|
-
this.pump();
|
|
274
|
-
this.notify();
|
|
275
|
-
}
|
|
276
|
-
key() {
|
|
277
|
-
return this.options.createKey?.() ?? defaultKey();
|
|
278
|
-
}
|
|
279
|
-
schedule() {
|
|
280
|
-
this.clearTimer();
|
|
281
|
-
this.timer = setTimeout(() => {
|
|
282
|
-
this.timer = undefined;
|
|
283
|
-
this.flush();
|
|
284
|
-
}, this.windowMs);
|
|
285
106
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
107
|
+
/** Steps forward after an undo. Returns false when there is nothing to redo. */
|
|
108
|
+
redo() {
|
|
109
|
+
const next = this.redoStack.pop();
|
|
110
|
+
if (!next)
|
|
111
|
+
return false;
|
|
112
|
+
this.undoStack.push(this.current);
|
|
113
|
+
this.restore(next);
|
|
114
|
+
return true;
|
|
290
115
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Adopts an artifact loaded or saved by the application. A clean draft follows the new artifact. A draft that has
|
|
118
|
+
* changes keeps its content, and the base moves to the new version so the next save is a complete one.
|
|
119
|
+
*/
|
|
120
|
+
load(artifact) {
|
|
121
|
+
if (artifact.id !== this.base.id)
|
|
122
|
+
throw new ArtifactError('INVALID_OPERATION', 'Load the same artifact.');
|
|
123
|
+
const clean = !this.buildSnapshot().dirty;
|
|
124
|
+
const sameContent = JSON.stringify(artifact.content) === JSON.stringify(this.current.artifact.content);
|
|
125
|
+
this.base = artifact;
|
|
126
|
+
if (clean || sameContent) {
|
|
127
|
+
this.current = { artifact, operations: [], at: 0, typing: false };
|
|
128
|
+
this.undoStack = [];
|
|
129
|
+
this.redoStack = [];
|
|
130
|
+
this.lint = this.lintOf(artifact);
|
|
131
|
+
this.changes = [];
|
|
132
|
+
}
|
|
133
|
+
this.publish();
|
|
134
|
+
}
|
|
135
|
+
/** Stops the session: later batches are ignored and listeners are dropped. */
|
|
136
|
+
dispose() {
|
|
137
|
+
this.disposed = true;
|
|
138
|
+
this.listeners.clear();
|
|
297
139
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.
|
|
302
|
-
this.queue.push({ type: 'undo' });
|
|
303
|
-
this.pump();
|
|
304
|
-
this.notify();
|
|
305
|
-
};
|
|
306
|
-
redo = () => {
|
|
307
|
-
if (!this.snapshot.canRedo)
|
|
308
|
-
return;
|
|
309
|
-
this.queue.push({ type: 'redo' });
|
|
310
|
-
this.pump();
|
|
311
|
-
this.notify();
|
|
312
|
-
};
|
|
313
|
-
dismissError = () => {
|
|
314
|
-
if (!this.error)
|
|
315
|
-
return;
|
|
140
|
+
restore(step) {
|
|
141
|
+
this.current = step;
|
|
142
|
+
this.lint = this.lintOf(step.artifact);
|
|
143
|
+
this.changes = [];
|
|
316
144
|
this.error = undefined;
|
|
317
|
-
this.
|
|
318
|
-
};
|
|
319
|
-
// -- sending ---------------------------------------------------------------------------------------
|
|
320
|
-
pump() {
|
|
321
|
-
if (this.inflight || this.conflict)
|
|
322
|
-
return;
|
|
323
|
-
const item = this.queue[0];
|
|
324
|
-
if (!item)
|
|
325
|
-
return;
|
|
326
|
-
if (item.type === 'undo' || item.type === 'redo') {
|
|
327
|
-
this.resolveHistoryCommand(item.type);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
this.inflight = true;
|
|
331
|
-
const base = this.confirmed;
|
|
332
|
-
this.options
|
|
333
|
-
.send({ baseVersion: base.version, operations: item.ops, idempotencyKey: item.key })
|
|
334
|
-
.then((result) => this.onCommitted(item, base, result), (error) => this.onRejected(item, base, error))
|
|
335
|
-
.finally(() => this.notify());
|
|
336
|
-
}
|
|
337
|
-
resolveHistoryCommand(direction) {
|
|
338
|
-
const stack = direction === 'undo' ? this.undoStack : this.redoStack;
|
|
339
|
-
const entry = stack.pop();
|
|
340
|
-
if (!entry) {
|
|
341
|
-
this.queue.shift();
|
|
342
|
-
this.pump();
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
if (!entry.before) {
|
|
346
|
-
// Rebased commit: the version before it is only known by the server.
|
|
347
|
-
this.inflight = true;
|
|
348
|
-
this.options
|
|
349
|
-
.load(entry.beforeVersion)
|
|
350
|
-
.then((artifact) => {
|
|
351
|
-
entry.before = envelopeOf(artifact);
|
|
352
|
-
stack.push(entry);
|
|
353
|
-
}, (error) => {
|
|
354
|
-
this.queue.shift();
|
|
355
|
-
this.error = toArtifactError(error);
|
|
356
|
-
this.announce({ type: 'rolled_back', error: this.error });
|
|
357
|
-
})
|
|
358
|
-
.finally(() => {
|
|
359
|
-
this.inflight = false;
|
|
360
|
-
this.applyDeferred();
|
|
361
|
-
this.pump();
|
|
362
|
-
this.notify();
|
|
363
|
-
});
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const current = envelopeOf(this.confirmed);
|
|
367
|
-
const expected = direction === 'undo' ? entry.after : entry.before;
|
|
368
|
-
const target = direction === 'undo' ? entry.before : entry.after;
|
|
369
|
-
this.queue.shift();
|
|
370
|
-
if (!sameEnvelope(current, expected)) {
|
|
371
|
-
// Someone else changed the artifact after this edit: undoing blindly would discard their work.
|
|
372
|
-
this.conflict = {
|
|
373
|
-
reason: direction,
|
|
374
|
-
changes: this.changesBetween(expected, current),
|
|
375
|
-
operations: operationsBetween(current, target),
|
|
376
|
-
latest: this.confirmed,
|
|
377
|
-
origin: direction,
|
|
378
|
-
entry,
|
|
379
|
-
};
|
|
380
|
-
this.view = current;
|
|
381
|
-
this.announce({ type: 'conflict', reason: direction });
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const ops = operationsBetween(current, target);
|
|
385
|
-
if (ops.length === 0) {
|
|
386
|
-
(direction === 'undo' ? this.redoStack : this.undoStack).push(entry);
|
|
387
|
-
this.pump();
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
this.queue.unshift({ type: 'ops', ops, key: this.key(), origin: direction, entry });
|
|
391
|
-
this.rebaseView(this.confirmed);
|
|
392
|
-
this.pump();
|
|
393
|
-
}
|
|
394
|
-
onCommitted(item, base, result) {
|
|
395
|
-
this.inflight = false;
|
|
396
|
-
this.queue.shift();
|
|
397
|
-
const previous = this.confirmed;
|
|
398
|
-
if (result.artifact.version > this.confirmed.version)
|
|
399
|
-
this.confirmed = result.artifact;
|
|
400
|
-
const after = envelopeOf(result.artifact);
|
|
401
|
-
if (item.origin === 'undo' && item.entry) {
|
|
402
|
-
this.redoStack.push(item.entry);
|
|
403
|
-
this.announce({ type: 'undone', version: result.version });
|
|
404
|
-
}
|
|
405
|
-
else if (item.origin === 'redo' && item.entry) {
|
|
406
|
-
this.pushUndo(item.entry);
|
|
407
|
-
this.announce({ type: 'redone', version: result.version });
|
|
408
|
-
}
|
|
409
|
-
else {
|
|
410
|
-
this.pushUndo({
|
|
411
|
-
...(result.rebased ? {} : { before: envelopeOf(base) }),
|
|
412
|
-
beforeVersion: result.version - 1,
|
|
413
|
-
after,
|
|
414
|
-
});
|
|
415
|
-
this.announce({ type: 'saved', version: result.version });
|
|
416
|
-
}
|
|
417
|
-
try {
|
|
418
|
-
this.options.onCommitted?.(result);
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
// A failing host callback must not break the session.
|
|
422
|
-
}
|
|
423
|
-
if (!this.disposed)
|
|
424
|
-
this.rebaseView(previous);
|
|
425
|
-
this.applyDeferred();
|
|
426
|
-
this.pump();
|
|
145
|
+
this.publish();
|
|
427
146
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
147
|
+
lintOf(artifact) {
|
|
148
|
+
const adapter = this.registry.get(artifact.format, this.locale);
|
|
149
|
+
return adapter.lint(artifact.content, {
|
|
150
|
+
locale: artifact.locale,
|
|
151
|
+
theme: this.registry.theme(artifact.themeId, this.locale),
|
|
152
|
+
});
|
|
434
153
|
}
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
onRejected(item, base, raw) {
|
|
441
|
-
const error = toArtifactError(raw);
|
|
442
|
-
this.queue.shift();
|
|
443
|
-
if (error.code === 'VERSION_CONFLICT') {
|
|
444
|
-
const operations = [...item.ops, ...this.pendingOperations()];
|
|
445
|
-
this.clearTimer();
|
|
446
|
-
this.queue = [];
|
|
447
|
-
this.buffer = [];
|
|
448
|
-
const changes = Array.isArray(error.details.changesSinceBase)
|
|
449
|
-
? error.details.changesSinceBase
|
|
450
|
-
: undefined;
|
|
451
|
-
// Keep `inflight` set while loading so nothing else is sent against a stale base.
|
|
452
|
-
return this.options
|
|
453
|
-
.load()
|
|
454
|
-
.then((latest) => {
|
|
455
|
-
if (latest.version > this.confirmed.version)
|
|
456
|
-
this.confirmed = latest;
|
|
457
|
-
}, () => undefined)
|
|
458
|
-
.then(() => {
|
|
459
|
-
this.inflight = false;
|
|
460
|
-
const remote = this.deferredRemote;
|
|
461
|
-
this.deferredRemote = undefined;
|
|
462
|
-
if (remote && remote.version > this.confirmed.version)
|
|
463
|
-
this.confirmed = remote;
|
|
464
|
-
const reason = item.origin === 'undo' || item.origin === 'redo' ? item.origin : 'server';
|
|
465
|
-
this.conflict = {
|
|
466
|
-
reason,
|
|
467
|
-
changes: changes ?? this.changesBetween(base, this.confirmed),
|
|
468
|
-
operations: item.origin === 'undo' || item.origin === 'redo' ? item.ops : operations,
|
|
469
|
-
latest: this.confirmed,
|
|
470
|
-
origin: item.origin,
|
|
471
|
-
...(item.entry ? { entry: item.entry } : {}),
|
|
472
|
-
};
|
|
473
|
-
this.view = envelopeOf(this.confirmed);
|
|
474
|
-
this.announce({ type: 'conflict', reason });
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
this.inflight = false;
|
|
478
|
-
// Any other rejection rolls back to the last confirmed version (spec §13.6).
|
|
479
|
-
this.clearTimer();
|
|
480
|
-
this.queue = [];
|
|
481
|
-
this.buffer = [];
|
|
482
|
-
if (item.entry)
|
|
483
|
-
(item.origin === 'undo' ? this.undoStack : this.redoStack).push(item.entry);
|
|
484
|
-
if (error.code === 'LOCKED')
|
|
485
|
-
this.setLocked(error);
|
|
486
|
-
this.error = error;
|
|
487
|
-
this.view = envelopeOf(this.confirmed);
|
|
488
|
-
this.announce({ type: 'rolled_back', error });
|
|
489
|
-
const remote = this.deferredRemote;
|
|
490
|
-
this.deferredRemote = undefined;
|
|
491
|
-
if (remote && remote.version > this.confirmed.version) {
|
|
492
|
-
this.confirmed = remote;
|
|
493
|
-
this.view = envelopeOf(remote);
|
|
494
|
-
}
|
|
154
|
+
publish() {
|
|
155
|
+
this.snapshot = this.buildSnapshot();
|
|
156
|
+
for (const listener of this.listeners)
|
|
157
|
+
listener();
|
|
495
158
|
}
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.
|
|
506
|
-
this.
|
|
507
|
-
this.
|
|
508
|
-
|
|
159
|
+
buildSnapshot() {
|
|
160
|
+
const dirty = this.current.operations.length > 0 ||
|
|
161
|
+
JSON.stringify(this.current.artifact.content) !== JSON.stringify(this.base.content) ||
|
|
162
|
+
this.current.artifact.title !== this.base.title;
|
|
163
|
+
return {
|
|
164
|
+
baseVersion: this.base.version,
|
|
165
|
+
artifact: this.current.artifact,
|
|
166
|
+
operations: this.current.operations,
|
|
167
|
+
dirty,
|
|
168
|
+
canUndo: this.undoStack.length > 0,
|
|
169
|
+
canRedo: this.redoStack.length > 0,
|
|
170
|
+
pending: this.pending > 0,
|
|
171
|
+
...(this.error ? { error: this.error } : {}),
|
|
172
|
+
lint: this.lint,
|
|
173
|
+
changes: this.changes,
|
|
174
|
+
};
|
|
509
175
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return;
|
|
520
|
-
if (artifact.version === this.confirmed.version) {
|
|
521
|
-
if (artifact.status !== this.confirmed.status ||
|
|
522
|
-
artifact.meta.publishedVersion !== this.confirmed.meta.publishedVersion) {
|
|
523
|
-
this.confirmed = { ...this.confirmed, status: artifact.status, meta: artifact.meta };
|
|
524
|
-
this.notify();
|
|
525
|
-
}
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
if (this.inflight) {
|
|
529
|
-
// Wait for the in-flight batch: applying it on top of a version that may already contain it
|
|
530
|
-
// would show its operations twice.
|
|
531
|
-
if (!this.deferredRemote || artifact.version > this.deferredRemote.version)
|
|
532
|
-
this.deferredRemote = artifact;
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
this.incorporate(artifact);
|
|
536
|
-
this.notify();
|
|
537
|
-
};
|
|
538
|
-
incorporate(artifact) {
|
|
539
|
-
const previous = this.confirmed;
|
|
540
|
-
this.confirmed = artifact;
|
|
541
|
-
if (this.conflict) {
|
|
542
|
-
this.conflict.latest = artifact;
|
|
543
|
-
this.view = envelopeOf(artifact);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
this.rebaseView(previous);
|
|
547
|
-
if (!this.conflict)
|
|
548
|
-
this.announce({ type: 'rebased', version: artifact.version });
|
|
549
|
-
}
|
|
176
|
+
}
|
|
177
|
+
function errorOf(error) {
|
|
178
|
+
if (isArtifactError(error)) {
|
|
179
|
+
const payload = error.toPayload();
|
|
180
|
+
return {
|
|
181
|
+
code: payload.error.code,
|
|
182
|
+
message: payload.error.message,
|
|
183
|
+
issues: payload.error.issues ?? [],
|
|
184
|
+
};
|
|
550
185
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.conflict = undefined;
|
|
556
|
-
this.error = undefined;
|
|
557
|
-
this.view = envelopeOf(this.confirmed);
|
|
558
|
-
if (choice === 'reapply') {
|
|
559
|
-
if (conflict.origin === 'undo' || conflict.origin === 'redo') {
|
|
560
|
-
const entry = conflict.entry;
|
|
561
|
-
const target = entry ? (conflict.origin === 'undo' ? entry.before : entry.after) : undefined;
|
|
562
|
-
const ops = target ? operationsBetween(envelopeOf(this.confirmed), target) : [];
|
|
563
|
-
if (ops.length > 0) {
|
|
564
|
-
this.queue.unshift({
|
|
565
|
-
type: 'ops',
|
|
566
|
-
ops,
|
|
567
|
-
key: this.key(),
|
|
568
|
-
origin: conflict.origin,
|
|
569
|
-
...(entry ? { entry } : {}),
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
try {
|
|
575
|
-
this.applyLocal(envelopeOf(this.confirmed), conflict.operations);
|
|
576
|
-
this.queue.unshift({ type: 'ops', ops: conflict.operations, key: this.key(), origin: 'reapply' });
|
|
577
|
-
}
|
|
578
|
-
catch (error) {
|
|
579
|
-
this.error = toArtifactError(error);
|
|
580
|
-
this.announce({ type: 'rolled_back', error: this.error });
|
|
581
|
-
this.pump();
|
|
582
|
-
this.notify();
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
this.rebaseView(this.confirmed);
|
|
587
|
-
}
|
|
588
|
-
this.announce({ type: 'conflict_resolved', choice });
|
|
589
|
-
this.pump();
|
|
590
|
-
this.notify();
|
|
186
|
+
return {
|
|
187
|
+
code: 'INTERNAL_ERROR',
|
|
188
|
+
message: error instanceof Error ? error.message : String(error),
|
|
189
|
+
issues: [],
|
|
591
190
|
};
|
|
592
|
-
/**
|
|
593
|
-
* Stops timers and listeners. Buffered typing is still sent unless `discard` is set, so closing the
|
|
594
|
-
* editor right after typing does not lose the last keystrokes.
|
|
595
|
-
*/
|
|
596
|
-
dispose(options = {}) {
|
|
597
|
-
if (this.disposed)
|
|
598
|
-
return;
|
|
599
|
-
if (options.discard) {
|
|
600
|
-
this.buffer = [];
|
|
601
|
-
this.queue = this.inflight ? this.queue.slice(0, 1) : [];
|
|
602
|
-
}
|
|
603
|
-
else {
|
|
604
|
-
this.flushBuffer();
|
|
605
|
-
this.pump();
|
|
606
|
-
}
|
|
607
|
-
this.clearTimer();
|
|
608
|
-
if (this.lockTimer !== undefined)
|
|
609
|
-
clearTimeout(this.lockTimer);
|
|
610
|
-
this.disposed = true;
|
|
611
|
-
this.listeners.clear();
|
|
612
|
-
}
|
|
613
191
|
}
|
|
614
192
|
//# sourceMappingURL=editor-session.js.map
|