@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/src/editor-session.ts
CHANGED
|
@@ -4,735 +4,257 @@ import {
|
|
|
4
4
|
type ArtifactRegistry,
|
|
5
5
|
applyOperations,
|
|
6
6
|
type ChangeSummary,
|
|
7
|
-
type CommitResult,
|
|
8
|
-
deepEqual,
|
|
9
|
-
type EmitOptions,
|
|
10
7
|
isArtifactError,
|
|
8
|
+
type LintIssue,
|
|
11
9
|
} from '@artkit-ai/core';
|
|
12
10
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
locale: string;
|
|
19
|
-
}
|
|
11
|
+
/**
|
|
12
|
+
* Local draft of one artifact (spec §11.6). The editor never talks to the server while the user edits: every batch is
|
|
13
|
+
* applied here, with the same pure engine as the server (`applyOperations`), and kept until the application saves.
|
|
14
|
+
* Saving is the application's decision: the editor emits the draft and the application persists it (or not).
|
|
15
|
+
*/
|
|
20
16
|
|
|
21
|
-
export interface
|
|
17
|
+
export interface DraftSnapshot {
|
|
18
|
+
/** Version of the artifact the draft started from (the last one the application loaded or saved). */
|
|
22
19
|
baseVersion: number;
|
|
23
|
-
|
|
24
|
-
idempotencyKey: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface EditorSessionOptions {
|
|
28
|
-
registry: ArtifactRegistry;
|
|
29
|
-
/** Last confirmed version. */
|
|
20
|
+
/** The draft as the user sees it. */
|
|
30
21
|
artifact: Artifact;
|
|
31
|
-
/**
|
|
32
|
-
send(request: SendRequest): Promise<CommitResult>;
|
|
33
|
-
/** Loads the latest version, or a specific one, after a conflict or a rebased commit. */
|
|
34
|
-
load(version?: number): Promise<Artifact>;
|
|
35
|
-
/** Typing operations emitted within this window become one batch (default 600 ms). */
|
|
36
|
-
groupingWindowMs?: number;
|
|
37
|
-
/** Upper bound for one typing batch while the user keeps typing (default 5 s). */
|
|
38
|
-
maxGroupingMs?: number;
|
|
39
|
-
/** Undo entries kept (default 100). */
|
|
40
|
-
maxUndo?: number;
|
|
41
|
-
/** Locale of error messages produced locally. */
|
|
42
|
-
locale?: string;
|
|
43
|
-
createKey?(): string;
|
|
44
|
-
onCommitted?(result: CommitResult): void;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type SessionStatus = 'idle' | 'editing' | 'saving' | 'conflict';
|
|
48
|
-
|
|
49
|
-
export interface SessionConflict {
|
|
50
|
-
/**
|
|
51
|
-
* `server`: the server answered VERSION_CONFLICT; `rebase`: a newer version arrived and the pending
|
|
52
|
-
* operations no longer apply on it; `undo`/`redo`: the artifact changed after the edit being undone.
|
|
53
|
-
*/
|
|
54
|
-
reason: 'server' | 'rebase' | 'undo' | 'redo';
|
|
55
|
-
/** What changed in the newer version, for the conflict diff. */
|
|
56
|
-
changes: ChangeSummary[];
|
|
57
|
-
/** The local operations that would be reapplied. */
|
|
22
|
+
/** Operations applied since `baseVersion`, in order. */
|
|
58
23
|
operations: unknown[];
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export type SessionEvent =
|
|
63
|
-
| { type: 'saved'; version: number }
|
|
64
|
-
| { type: 'undone'; version: number }
|
|
65
|
-
| { type: 'redone'; version: number }
|
|
66
|
-
| { type: 'rebased'; version: number }
|
|
67
|
-
| { type: 'rejected'; error: ArtifactError }
|
|
68
|
-
| { type: 'rolled_back'; error: ArtifactError }
|
|
69
|
-
| { type: 'conflict'; reason: SessionConflict['reason'] }
|
|
70
|
-
| { type: 'conflict_resolved'; choice: ConflictChoice };
|
|
71
|
-
|
|
72
|
-
export type ConflictChoice = 'keep-theirs' | 'reapply';
|
|
73
|
-
|
|
74
|
-
export interface EditorSessionState {
|
|
75
|
-
confirmed: Artifact;
|
|
76
|
-
/** Optimistic state: confirmed version plus every unconfirmed local operation. */
|
|
77
|
-
view: EnvelopeState;
|
|
78
|
-
status: SessionStatus;
|
|
79
|
-
/** Local operations not confirmed yet (buffered, queued or in flight). */
|
|
24
|
+
/** True when the draft differs from the base. */
|
|
80
25
|
dirty: boolean;
|
|
81
26
|
canUndo: boolean;
|
|
82
27
|
canRedo: boolean;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
|
|
28
|
+
/** An edit is being applied. */
|
|
29
|
+
pending: boolean;
|
|
30
|
+
/** Last rejected batch, cleared by the next accepted one. */
|
|
31
|
+
error?: { code: string; message: string; issues: { path: string; message: string; hint?: string }[] };
|
|
32
|
+
/** Lint issues of the draft. */
|
|
33
|
+
lint: LintIssue[];
|
|
34
|
+
/** Changes of the last accepted batch. */
|
|
35
|
+
changes: ChangeSummary[];
|
|
89
36
|
}
|
|
90
37
|
|
|
91
|
-
interface
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
38
|
+
export interface DraftSessionOptions {
|
|
39
|
+
registry: ArtifactRegistry;
|
|
40
|
+
artifact: Artifact;
|
|
41
|
+
/** Locale of the model-facing messages produced by the engine (pt-BR or en). */
|
|
42
|
+
locale?: string;
|
|
43
|
+
/** Times within which consecutive typing batches share one undo step. */
|
|
44
|
+
coalesceMs?: number;
|
|
45
|
+
/** Undo steps kept in memory. */
|
|
46
|
+
maxUndo?: number;
|
|
47
|
+
now?: () => number;
|
|
96
48
|
}
|
|
97
49
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| { type: 'redo' };
|
|
104
|
-
|
|
105
|
-
export function envelopeOf(
|
|
106
|
-
artifact: Pick<Artifact, 'content' | 'title' | 'themeId' | 'locale'>,
|
|
107
|
-
): EnvelopeState {
|
|
108
|
-
return {
|
|
109
|
-
content: artifact.content,
|
|
110
|
-
title: artifact.title,
|
|
111
|
-
themeId: artifact.themeId,
|
|
112
|
-
locale: artifact.locale,
|
|
113
|
-
};
|
|
50
|
+
interface Step {
|
|
51
|
+
artifact: Artifact;
|
|
52
|
+
operations: unknown[];
|
|
53
|
+
at: number;
|
|
54
|
+
typing: boolean;
|
|
114
55
|
}
|
|
115
56
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
57
|
+
export interface EmitOptions {
|
|
58
|
+
/** Continuous gestures (typing, dragging) share one undo step; discrete changes always get their own. */
|
|
59
|
+
kind?: 'typing' | 'continuous' | 'discrete';
|
|
120
60
|
}
|
|
121
61
|
|
|
122
|
-
|
|
123
|
-
* Operations that turn `from` into `to`. Kinds do not provide inverse operations, so undo and redo send
|
|
124
|
-
* `replace_content` with the snapshot (plus envelope operations); the commit still becomes a new version
|
|
125
|
-
* (spec §8.5).
|
|
126
|
-
*/
|
|
127
|
-
export function operationsBetween(from: EnvelopeState, to: EnvelopeState): unknown[] {
|
|
128
|
-
const ops: unknown[] = [];
|
|
129
|
-
if (!deepEqual(from.content, to.content))
|
|
130
|
-
ops.push({ op: 'replace_content', content: structuredClone(to.content) });
|
|
131
|
-
if (from.title !== to.title) ops.push({ op: 'set_title', title: to.title });
|
|
132
|
-
if (from.themeId !== to.themeId) ops.push({ op: 'set_theme', themeId: to.themeId });
|
|
133
|
-
if (from.locale !== to.locale) ops.push({ op: 'set_locale', locale: to.locale });
|
|
134
|
-
return ops;
|
|
135
|
-
}
|
|
62
|
+
type Listener = () => void;
|
|
136
63
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return new ArtifactError('INTERNAL_ERROR', 'Internal error while processing the request.', {
|
|
140
|
-
cause: error,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
64
|
+
const DEFAULT_COALESCE_MS = 1000;
|
|
65
|
+
const DEFAULT_MAX_UNDO = 200;
|
|
143
66
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Framework-free editing session behind `ArtifactEditor` (spec §13.6): batching of typing operations,
|
|
152
|
-
* optimistic apply with the kind's own logic, rollback on rejection, local undo/redo over the user's own
|
|
153
|
-
* batches, rebase of pending operations on newer versions, conflict resolution and lock handling.
|
|
154
|
-
*
|
|
155
|
-
* Batches are sent one at a time, each against the version the previous one produced.
|
|
156
|
-
*/
|
|
157
|
-
export class EditorSession {
|
|
158
|
-
private readonly options: EditorSessionOptions;
|
|
159
|
-
private readonly windowMs: number;
|
|
160
|
-
private readonly maxGroupingMs: number;
|
|
67
|
+
/** Draft of one artifact with local undo and redo. Batches are applied in order, one at a time. */
|
|
68
|
+
export class DraftSession {
|
|
69
|
+
private readonly registry: ArtifactRegistry;
|
|
70
|
+
private readonly locale: string;
|
|
71
|
+
private readonly coalesceMs: number;
|
|
161
72
|
private readonly maxUndo: number;
|
|
162
|
-
private
|
|
163
|
-
private
|
|
164
|
-
private
|
|
165
|
-
private
|
|
166
|
-
private
|
|
167
|
-
private
|
|
168
|
-
private
|
|
169
|
-
private
|
|
170
|
-
private
|
|
171
|
-
private
|
|
172
|
-
private
|
|
173
|
-
private
|
|
174
|
-
private lockedUntil: string | undefined;
|
|
175
|
-
private deferredRemote: Artifact | undefined;
|
|
176
|
-
private event: (SessionEvent & { seq: number }) | undefined;
|
|
177
|
-
private seq = 0;
|
|
73
|
+
private readonly now: () => number;
|
|
74
|
+
private base: Artifact;
|
|
75
|
+
private current: Step;
|
|
76
|
+
private undoStack: Step[] = [];
|
|
77
|
+
private redoStack: Step[] = [];
|
|
78
|
+
private queue: Promise<unknown> = Promise.resolve();
|
|
79
|
+
private pending = 0;
|
|
80
|
+
private error: DraftSnapshot['error'];
|
|
81
|
+
private lint: LintIssue[];
|
|
82
|
+
private changes: ChangeSummary[] = [];
|
|
83
|
+
private snapshot: DraftSnapshot;
|
|
84
|
+
private readonly listeners = new Set<Listener>();
|
|
178
85
|
private disposed = false;
|
|
179
|
-
private readonly listeners = new Set<() => void>();
|
|
180
|
-
private idleWaiters: (() => void)[] = [];
|
|
181
|
-
private snapshot: EditorSessionState;
|
|
182
86
|
|
|
183
|
-
constructor(options:
|
|
184
|
-
this.
|
|
185
|
-
this.
|
|
186
|
-
this.
|
|
187
|
-
this.maxUndo = options.maxUndo ??
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
87
|
+
constructor(options: DraftSessionOptions) {
|
|
88
|
+
this.registry = options.registry;
|
|
89
|
+
this.locale = options.locale ?? 'en';
|
|
90
|
+
this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
|
|
91
|
+
this.maxUndo = options.maxUndo ?? DEFAULT_MAX_UNDO;
|
|
92
|
+
this.now = options.now ?? (() => Date.now());
|
|
93
|
+
this.base = options.artifact;
|
|
94
|
+
this.current = { artifact: options.artifact, operations: [], at: 0, typing: false };
|
|
95
|
+
this.lint = this.lintOf(options.artifact);
|
|
190
96
|
this.snapshot = this.buildSnapshot();
|
|
191
97
|
}
|
|
192
98
|
|
|
193
|
-
|
|
99
|
+
/** The current snapshot. Its identity changes only when the state changes. */
|
|
100
|
+
getSnapshot = (): DraftSnapshot => this.snapshot;
|
|
194
101
|
|
|
195
|
-
subscribe = (listener:
|
|
102
|
+
subscribe = (listener: Listener): (() => void) => {
|
|
196
103
|
this.listeners.add(listener);
|
|
197
104
|
return () => this.listeners.delete(listener);
|
|
198
105
|
};
|
|
199
106
|
|
|
200
|
-
getSnapshot = (): EditorSessionState => this.snapshot;
|
|
201
|
-
|
|
202
|
-
/** Resolves once nothing is buffered, queued or in flight (or a conflict is waiting for the user). */
|
|
203
|
-
whenIdle(): Promise<void> {
|
|
204
|
-
if (this.isIdle()) return Promise.resolve();
|
|
205
|
-
return new Promise((resolve) => this.idleWaiters.push(resolve));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private isIdle(): boolean {
|
|
209
|
-
return (
|
|
210
|
-
this.buffer.length === 0 && !this.inflight && (this.queue.length === 0 || this.conflict !== undefined)
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private buildSnapshot(): EditorSessionState {
|
|
215
|
-
const pendingEdits = this.queue.filter(
|
|
216
|
-
(item) => item.type === 'ops' && item.origin !== 'undo' && item.origin !== 'redo',
|
|
217
|
-
).length;
|
|
218
|
-
const queuedUndo = this.queue.filter((item) => item.type === 'undo').length;
|
|
219
|
-
const queuedRedo = this.queue.filter((item) => item.type === 'redo').length;
|
|
220
|
-
const dirty = this.buffer.length > 0 || this.queue.length > 0 || this.inflight;
|
|
221
|
-
let status: SessionStatus = 'idle';
|
|
222
|
-
if (this.conflict) status = 'conflict';
|
|
223
|
-
else if (this.inflight || this.queue.length > 0) status = 'saving';
|
|
224
|
-
else if (this.buffer.length > 0) status = 'editing';
|
|
225
|
-
const blocked = this.conflict !== undefined || this.isLocked();
|
|
226
|
-
return {
|
|
227
|
-
confirmed: this.confirmed,
|
|
228
|
-
view: this.view,
|
|
229
|
-
status,
|
|
230
|
-
dirty,
|
|
231
|
-
canUndo:
|
|
232
|
-
!blocked && this.undoStack.length + pendingEdits + (this.buffer.length > 0 ? 1 : 0) > queuedUndo,
|
|
233
|
-
canRedo:
|
|
234
|
-
!blocked && this.buffer.length === 0 && pendingEdits === 0 && this.redoStack.length > queuedRedo,
|
|
235
|
-
...(this.error ? { error: this.error } : {}),
|
|
236
|
-
...(this.conflict
|
|
237
|
-
? {
|
|
238
|
-
conflict: {
|
|
239
|
-
reason: this.conflict.reason,
|
|
240
|
-
changes: this.conflict.changes,
|
|
241
|
-
operations: this.conflict.operations,
|
|
242
|
-
latest: this.conflict.latest,
|
|
243
|
-
},
|
|
244
|
-
}
|
|
245
|
-
: {}),
|
|
246
|
-
...(this.lockedUntil ? { lockedUntil: this.lockedUntil } : {}),
|
|
247
|
-
...(this.event ? { event: this.event } : {}),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private notify(): void {
|
|
252
|
-
this.snapshot = this.buildSnapshot();
|
|
253
|
-
for (const listener of [...this.listeners]) listener();
|
|
254
|
-
if (this.isIdle() && this.idleWaiters.length > 0) {
|
|
255
|
-
const waiters = this.idleWaiters;
|
|
256
|
-
this.idleWaiters = [];
|
|
257
|
-
for (const resolve of waiters) resolve();
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private announce(event: SessionEvent): void {
|
|
262
|
-
this.seq += 1;
|
|
263
|
-
this.event = { ...event, seq: this.seq };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private isLocked(): boolean {
|
|
267
|
-
return this.lockedUntil !== undefined && Date.parse(this.lockedUntil) > Date.now();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// -- local application -----------------------------------------------------------------------------
|
|
271
|
-
|
|
272
|
-
private applyLocal(state: EnvelopeState, ops: unknown[]): EnvelopeState {
|
|
273
|
-
const result = applyOperations(
|
|
274
|
-
this.options.registry,
|
|
275
|
-
{ kind: this.confirmed.kind, ...state },
|
|
276
|
-
ops,
|
|
277
|
-
this.options.locale ? { locale: this.options.locale } : {},
|
|
278
|
-
);
|
|
279
|
-
return { content: result.content, title: result.title, themeId: result.themeId, locale: result.locale };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private pendingOperations(): unknown[] {
|
|
283
|
-
const ops: unknown[] = [];
|
|
284
|
-
for (const item of this.queue) if (item.type === 'ops') ops.push(...item.ops);
|
|
285
|
-
ops.push(...this.buffer);
|
|
286
|
-
return ops;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
107
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
108
|
+
* Applies a batch to the draft. Resolves to `true` when the batch was accepted. A rejected batch leaves the draft
|
|
109
|
+
* untouched and sets `snapshot.error`.
|
|
292
110
|
*/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
111
|
+
apply(operations: unknown[], options: EmitOptions = {}): Promise<boolean> {
|
|
112
|
+
if (operations.length === 0) return Promise.resolve(true);
|
|
113
|
+
this.pending += 1;
|
|
114
|
+
this.publish();
|
|
115
|
+
const run = this.queue.then(async () => {
|
|
116
|
+
if (this.disposed) return false;
|
|
117
|
+
const typing = options.kind === 'typing' || options.kind === 'continuous';
|
|
118
|
+
try {
|
|
119
|
+
const result = await applyOperations(this.registry, this.current.artifact, operations, {
|
|
120
|
+
locale: this.locale,
|
|
121
|
+
});
|
|
122
|
+
const next: Artifact = {
|
|
123
|
+
...this.current.artifact,
|
|
124
|
+
formatVersion: result.formatVersion,
|
|
125
|
+
title: result.title,
|
|
126
|
+
locale: result.locale,
|
|
127
|
+
themeId: result.themeId,
|
|
128
|
+
content: result.content,
|
|
129
|
+
};
|
|
130
|
+
const now = this.now();
|
|
131
|
+
const coalesce = typing && this.current.typing && now - this.current.at <= this.coalesceMs;
|
|
132
|
+
if (!coalesce) {
|
|
133
|
+
this.undoStack.push(this.current);
|
|
134
|
+
if (this.undoStack.length > this.maxUndo) this.undoStack.shift();
|
|
135
|
+
}
|
|
136
|
+
this.redoStack = [];
|
|
137
|
+
this.current = {
|
|
138
|
+
artifact: next,
|
|
139
|
+
operations: [...this.current.operations, ...operations],
|
|
140
|
+
at: now,
|
|
141
|
+
typing,
|
|
142
|
+
};
|
|
143
|
+
this.lint = result.lint;
|
|
144
|
+
this.changes = result.diff;
|
|
145
|
+
this.error = undefined;
|
|
146
|
+
return true;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.error = errorOf(error);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
this.queue = run.finally(() => {
|
|
153
|
+
this.pending -= 1;
|
|
154
|
+
this.publish();
|
|
155
|
+
});
|
|
156
|
+
return run.then((accepted) => {
|
|
157
|
+
this.publish();
|
|
158
|
+
return accepted;
|
|
159
|
+
});
|
|
315
160
|
}
|
|
316
161
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
325
|
-
if (before.themeId !== after.themeId)
|
|
326
|
-
changes.push({
|
|
327
|
-
type: 'changed',
|
|
328
|
-
target: 'envelope:themeId',
|
|
329
|
-
summary: `${before.themeId} → ${after.themeId}`,
|
|
330
|
-
});
|
|
331
|
-
if (before.locale !== after.locale)
|
|
332
|
-
changes.push({
|
|
333
|
-
type: 'changed',
|
|
334
|
-
target: 'envelope:locale',
|
|
335
|
-
summary: `${before.locale} → ${after.locale}`,
|
|
336
|
-
});
|
|
337
|
-
try {
|
|
338
|
-
changes.push(...this.options.registry.get(this.confirmed.kind).diff(before.content, after.content));
|
|
339
|
-
} catch {
|
|
340
|
-
// A diff failure must not hide the conflict itself.
|
|
341
|
-
}
|
|
342
|
-
return changes;
|
|
162
|
+
/** Steps back one edit (local). Returns false when there is nothing to undo. */
|
|
163
|
+
undo(): boolean {
|
|
164
|
+
const previous = this.undoStack.pop();
|
|
165
|
+
if (!previous) return false;
|
|
166
|
+
this.redoStack.push(this.current);
|
|
167
|
+
this.restore(previous);
|
|
168
|
+
return true;
|
|
343
169
|
}
|
|
344
170
|
|
|
345
|
-
|
|
171
|
+
/** Steps forward after an undo. Returns false when there is nothing to redo. */
|
|
172
|
+
redo(): boolean {
|
|
173
|
+
const next = this.redoStack.pop();
|
|
174
|
+
if (!next) return false;
|
|
175
|
+
this.undoStack.push(this.current);
|
|
176
|
+
this.restore(next);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
346
179
|
|
|
347
180
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
181
|
+
* Adopts an artifact loaded or saved by the application. A clean draft follows the new artifact. A draft that has
|
|
182
|
+
* changes keeps its content, and the base moves to the new version so the next save is a complete one.
|
|
350
183
|
*/
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.
|
|
362
|
-
} catch (error) {
|
|
363
|
-
this.error = toArtifactError(error);
|
|
364
|
-
this.announce({ type: 'rejected', error: this.error });
|
|
365
|
-
this.notify();
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
this.error = undefined;
|
|
369
|
-
this.redoStack = [];
|
|
370
|
-
const maxOps = this.options.registry.batch.maxOperations;
|
|
371
|
-
if (options.kind === 'typing') {
|
|
372
|
-
if (this.buffer.length + operations.length > maxOps) this.flushBuffer();
|
|
373
|
-
if (this.buffer.length === 0) this.bufferStartedAt = Date.now();
|
|
374
|
-
this.buffer.push(...operations);
|
|
375
|
-
if (Date.now() - this.bufferStartedAt >= this.maxGroupingMs) this.flushBuffer();
|
|
376
|
-
else this.schedule();
|
|
377
|
-
} else {
|
|
378
|
-
// Discrete operations and the end of continuous gestures flush right away, after pending typing.
|
|
379
|
-
this.flushBuffer();
|
|
380
|
-
this.queue.push({ type: 'ops', ops: operations, key: this.key(), origin: 'edit' });
|
|
184
|
+
load(artifact: Artifact): void {
|
|
185
|
+
if (artifact.id !== this.base.id) throw new ArtifactError('INVALID_OPERATION', 'Load the same artifact.');
|
|
186
|
+
const clean = !this.buildSnapshot().dirty;
|
|
187
|
+
const sameContent = JSON.stringify(artifact.content) === JSON.stringify(this.current.artifact.content);
|
|
188
|
+
this.base = artifact;
|
|
189
|
+
if (clean || sameContent) {
|
|
190
|
+
this.current = { artifact, operations: [], at: 0, typing: false };
|
|
191
|
+
this.undoStack = [];
|
|
192
|
+
this.redoStack = [];
|
|
193
|
+
this.lint = this.lintOf(artifact);
|
|
194
|
+
this.changes = [];
|
|
381
195
|
}
|
|
382
|
-
this.
|
|
383
|
-
this.notify();
|
|
384
|
-
return true;
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
/** Sends buffered typing operations now (e.g. on blur or before navigation). */
|
|
388
|
-
flush(): void {
|
|
389
|
-
if (this.buffer.length === 0) return;
|
|
390
|
-
this.flushBuffer();
|
|
391
|
-
this.pump();
|
|
392
|
-
this.notify();
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private key(): string {
|
|
396
|
-
return this.options.createKey?.() ?? defaultKey();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
private schedule(): void {
|
|
400
|
-
this.clearTimer();
|
|
401
|
-
this.timer = setTimeout(() => {
|
|
402
|
-
this.timer = undefined;
|
|
403
|
-
this.flush();
|
|
404
|
-
}, this.windowMs);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private clearTimer(): void {
|
|
408
|
-
if (this.timer !== undefined) clearTimeout(this.timer);
|
|
409
|
-
this.timer = undefined;
|
|
196
|
+
this.publish();
|
|
410
197
|
}
|
|
411
198
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this.
|
|
416
|
-
this.buffer = [];
|
|
199
|
+
/** Stops the session: later batches are ignored and listeners are dropped. */
|
|
200
|
+
dispose(): void {
|
|
201
|
+
this.disposed = true;
|
|
202
|
+
this.listeners.clear();
|
|
417
203
|
}
|
|
418
204
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
this.
|
|
422
|
-
this.
|
|
423
|
-
this.pump();
|
|
424
|
-
this.notify();
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
redo = (): void => {
|
|
428
|
-
if (!this.snapshot.canRedo) return;
|
|
429
|
-
this.queue.push({ type: 'redo' });
|
|
430
|
-
this.pump();
|
|
431
|
-
this.notify();
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
dismissError = (): void => {
|
|
435
|
-
if (!this.error) return;
|
|
205
|
+
private restore(step: Step): void {
|
|
206
|
+
this.current = step;
|
|
207
|
+
this.lint = this.lintOf(step.artifact);
|
|
208
|
+
this.changes = [];
|
|
436
209
|
this.error = undefined;
|
|
437
|
-
this.
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
// -- sending ---------------------------------------------------------------------------------------
|
|
441
|
-
|
|
442
|
-
private pump(): void {
|
|
443
|
-
if (this.inflight || this.conflict) return;
|
|
444
|
-
const item = this.queue[0];
|
|
445
|
-
if (!item) return;
|
|
446
|
-
if (item.type === 'undo' || item.type === 'redo') {
|
|
447
|
-
this.resolveHistoryCommand(item.type);
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
this.inflight = true;
|
|
451
|
-
const base = this.confirmed;
|
|
452
|
-
this.options
|
|
453
|
-
.send({ baseVersion: base.version, operations: item.ops, idempotencyKey: item.key })
|
|
454
|
-
.then(
|
|
455
|
-
(result) => this.onCommitted(item, base, result),
|
|
456
|
-
(error: unknown) => this.onRejected(item, base, error),
|
|
457
|
-
)
|
|
458
|
-
.finally(() => this.notify());
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
private resolveHistoryCommand(direction: 'undo' | 'redo'): void {
|
|
462
|
-
const stack = direction === 'undo' ? this.undoStack : this.redoStack;
|
|
463
|
-
const entry = stack.pop();
|
|
464
|
-
if (!entry) {
|
|
465
|
-
this.queue.shift();
|
|
466
|
-
this.pump();
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (!entry.before) {
|
|
470
|
-
// Rebased commit: the version before it is only known by the server.
|
|
471
|
-
this.inflight = true;
|
|
472
|
-
this.options
|
|
473
|
-
.load(entry.beforeVersion)
|
|
474
|
-
.then(
|
|
475
|
-
(artifact) => {
|
|
476
|
-
entry.before = envelopeOf(artifact);
|
|
477
|
-
stack.push(entry);
|
|
478
|
-
},
|
|
479
|
-
(error: unknown) => {
|
|
480
|
-
this.queue.shift();
|
|
481
|
-
this.error = toArtifactError(error);
|
|
482
|
-
this.announce({ type: 'rolled_back', error: this.error });
|
|
483
|
-
},
|
|
484
|
-
)
|
|
485
|
-
.finally(() => {
|
|
486
|
-
this.inflight = false;
|
|
487
|
-
this.applyDeferred();
|
|
488
|
-
this.pump();
|
|
489
|
-
this.notify();
|
|
490
|
-
});
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
const current = envelopeOf(this.confirmed);
|
|
494
|
-
const expected = direction === 'undo' ? entry.after : entry.before;
|
|
495
|
-
const target = direction === 'undo' ? entry.before : entry.after;
|
|
496
|
-
this.queue.shift();
|
|
497
|
-
if (!sameEnvelope(current, expected)) {
|
|
498
|
-
// Someone else changed the artifact after this edit: undoing blindly would discard their work.
|
|
499
|
-
this.conflict = {
|
|
500
|
-
reason: direction,
|
|
501
|
-
changes: this.changesBetween(expected, current),
|
|
502
|
-
operations: operationsBetween(current, target),
|
|
503
|
-
latest: this.confirmed,
|
|
504
|
-
origin: direction,
|
|
505
|
-
entry,
|
|
506
|
-
};
|
|
507
|
-
this.view = current;
|
|
508
|
-
this.announce({ type: 'conflict', reason: direction });
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const ops = operationsBetween(current, target);
|
|
512
|
-
if (ops.length === 0) {
|
|
513
|
-
(direction === 'undo' ? this.redoStack : this.undoStack).push(entry);
|
|
514
|
-
this.pump();
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
this.queue.unshift({ type: 'ops', ops, key: this.key(), origin: direction, entry });
|
|
518
|
-
this.rebaseView(this.confirmed);
|
|
519
|
-
this.pump();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
private onCommitted(item: Extract<QueueItem, { type: 'ops' }>, base: Artifact, result: CommitResult): void {
|
|
523
|
-
this.inflight = false;
|
|
524
|
-
this.queue.shift();
|
|
525
|
-
const previous = this.confirmed;
|
|
526
|
-
if (result.artifact.version > this.confirmed.version) this.confirmed = result.artifact;
|
|
527
|
-
const after = envelopeOf(result.artifact);
|
|
528
|
-
if (item.origin === 'undo' && item.entry) {
|
|
529
|
-
this.redoStack.push(item.entry);
|
|
530
|
-
this.announce({ type: 'undone', version: result.version });
|
|
531
|
-
} else if (item.origin === 'redo' && item.entry) {
|
|
532
|
-
this.pushUndo(item.entry);
|
|
533
|
-
this.announce({ type: 'redone', version: result.version });
|
|
534
|
-
} else {
|
|
535
|
-
this.pushUndo({
|
|
536
|
-
...(result.rebased ? {} : { before: envelopeOf(base) }),
|
|
537
|
-
beforeVersion: result.version - 1,
|
|
538
|
-
after,
|
|
539
|
-
});
|
|
540
|
-
this.announce({ type: 'saved', version: result.version });
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
this.options.onCommitted?.(result);
|
|
544
|
-
} catch {
|
|
545
|
-
// A failing host callback must not break the session.
|
|
546
|
-
}
|
|
547
|
-
if (!this.disposed) this.rebaseView(previous);
|
|
548
|
-
this.applyDeferred();
|
|
549
|
-
this.pump();
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/** Incorporates a version that arrived while a request was in flight. */
|
|
553
|
-
private applyDeferred(): void {
|
|
554
|
-
const remote = this.deferredRemote;
|
|
555
|
-
this.deferredRemote = undefined;
|
|
556
|
-
if (remote && remote.version > this.confirmed.version) this.incorporate(remote);
|
|
210
|
+
this.publish();
|
|
557
211
|
}
|
|
558
212
|
|
|
559
|
-
private
|
|
560
|
-
this.
|
|
561
|
-
|
|
213
|
+
private lintOf(artifact: Artifact): LintIssue[] {
|
|
214
|
+
const adapter = this.registry.get(artifact.format, this.locale);
|
|
215
|
+
return adapter.lint(artifact.content as never, {
|
|
216
|
+
locale: artifact.locale,
|
|
217
|
+
theme: this.registry.theme(artifact.themeId, this.locale),
|
|
218
|
+
});
|
|
562
219
|
}
|
|
563
220
|
|
|
564
|
-
private
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
raw: unknown,
|
|
568
|
-
): Promise<void> | void {
|
|
569
|
-
const error = toArtifactError(raw);
|
|
570
|
-
this.queue.shift();
|
|
571
|
-
if (error.code === 'VERSION_CONFLICT') {
|
|
572
|
-
const operations = [...item.ops, ...this.pendingOperations()];
|
|
573
|
-
this.clearTimer();
|
|
574
|
-
this.queue = [];
|
|
575
|
-
this.buffer = [];
|
|
576
|
-
const changes = Array.isArray(error.details.changesSinceBase)
|
|
577
|
-
? (error.details.changesSinceBase as ChangeSummary[])
|
|
578
|
-
: undefined;
|
|
579
|
-
// Keep `inflight` set while loading so nothing else is sent against a stale base.
|
|
580
|
-
return this.options
|
|
581
|
-
.load()
|
|
582
|
-
.then(
|
|
583
|
-
(latest) => {
|
|
584
|
-
if (latest.version > this.confirmed.version) this.confirmed = latest;
|
|
585
|
-
},
|
|
586
|
-
() => undefined,
|
|
587
|
-
)
|
|
588
|
-
.then(() => {
|
|
589
|
-
this.inflight = false;
|
|
590
|
-
const remote = this.deferredRemote;
|
|
591
|
-
this.deferredRemote = undefined;
|
|
592
|
-
if (remote && remote.version > this.confirmed.version) this.confirmed = remote;
|
|
593
|
-
const reason = item.origin === 'undo' || item.origin === 'redo' ? item.origin : 'server';
|
|
594
|
-
this.conflict = {
|
|
595
|
-
reason,
|
|
596
|
-
changes: changes ?? this.changesBetween(base, this.confirmed),
|
|
597
|
-
operations: item.origin === 'undo' || item.origin === 'redo' ? item.ops : operations,
|
|
598
|
-
latest: this.confirmed,
|
|
599
|
-
origin: item.origin,
|
|
600
|
-
...(item.entry ? { entry: item.entry } : {}),
|
|
601
|
-
};
|
|
602
|
-
this.view = envelopeOf(this.confirmed);
|
|
603
|
-
this.announce({ type: 'conflict', reason });
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
this.inflight = false;
|
|
607
|
-
// Any other rejection rolls back to the last confirmed version (spec §13.6).
|
|
608
|
-
this.clearTimer();
|
|
609
|
-
this.queue = [];
|
|
610
|
-
this.buffer = [];
|
|
611
|
-
if (item.entry) (item.origin === 'undo' ? this.undoStack : this.redoStack).push(item.entry);
|
|
612
|
-
if (error.code === 'LOCKED') this.setLocked(error);
|
|
613
|
-
this.error = error;
|
|
614
|
-
this.view = envelopeOf(this.confirmed);
|
|
615
|
-
this.announce({ type: 'rolled_back', error });
|
|
616
|
-
const remote = this.deferredRemote;
|
|
617
|
-
this.deferredRemote = undefined;
|
|
618
|
-
if (remote && remote.version > this.confirmed.version) {
|
|
619
|
-
this.confirmed = remote;
|
|
620
|
-
this.view = envelopeOf(remote);
|
|
621
|
-
}
|
|
221
|
+
private publish(): void {
|
|
222
|
+
this.snapshot = this.buildSnapshot();
|
|
223
|
+
for (const listener of this.listeners) listener();
|
|
622
224
|
}
|
|
623
225
|
|
|
624
|
-
private
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
this.
|
|
635
|
-
this.
|
|
636
|
-
|
|
226
|
+
private buildSnapshot(): DraftSnapshot {
|
|
227
|
+
const dirty =
|
|
228
|
+
this.current.operations.length > 0 ||
|
|
229
|
+
JSON.stringify(this.current.artifact.content) !== JSON.stringify(this.base.content) ||
|
|
230
|
+
this.current.artifact.title !== this.base.title;
|
|
231
|
+
return {
|
|
232
|
+
baseVersion: this.base.version,
|
|
233
|
+
artifact: this.current.artifact,
|
|
234
|
+
operations: this.current.operations,
|
|
235
|
+
dirty,
|
|
236
|
+
canUndo: this.undoStack.length > 0,
|
|
237
|
+
canRedo: this.redoStack.length > 0,
|
|
238
|
+
pending: this.pending > 0,
|
|
239
|
+
...(this.error ? { error: this.error } : {}),
|
|
240
|
+
lint: this.lint,
|
|
241
|
+
changes: this.changes,
|
|
242
|
+
};
|
|
637
243
|
}
|
|
244
|
+
}
|
|
638
245
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (artifact.version < this.confirmed.version) return;
|
|
648
|
-
if (artifact.version === this.confirmed.version) {
|
|
649
|
-
if (
|
|
650
|
-
artifact.status !== this.confirmed.status ||
|
|
651
|
-
artifact.meta.publishedVersion !== this.confirmed.meta.publishedVersion
|
|
652
|
-
) {
|
|
653
|
-
this.confirmed = { ...this.confirmed, status: artifact.status, meta: artifact.meta };
|
|
654
|
-
this.notify();
|
|
655
|
-
}
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
if (this.inflight) {
|
|
659
|
-
// Wait for the in-flight batch: applying it on top of a version that may already contain it
|
|
660
|
-
// would show its operations twice.
|
|
661
|
-
if (!this.deferredRemote || artifact.version > this.deferredRemote.version)
|
|
662
|
-
this.deferredRemote = artifact;
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
this.incorporate(artifact);
|
|
666
|
-
this.notify();
|
|
667
|
-
};
|
|
668
|
-
|
|
669
|
-
private incorporate(artifact: Artifact): void {
|
|
670
|
-
const previous = this.confirmed;
|
|
671
|
-
this.confirmed = artifact;
|
|
672
|
-
if (this.conflict) {
|
|
673
|
-
this.conflict.latest = artifact;
|
|
674
|
-
this.view = envelopeOf(artifact);
|
|
675
|
-
} else {
|
|
676
|
-
this.rebaseView(previous);
|
|
677
|
-
if (!this.conflict) this.announce({ type: 'rebased', version: artifact.version });
|
|
678
|
-
}
|
|
246
|
+
function errorOf(error: unknown): NonNullable<DraftSnapshot['error']> {
|
|
247
|
+
if (isArtifactError(error)) {
|
|
248
|
+
const payload = error.toPayload();
|
|
249
|
+
return {
|
|
250
|
+
code: payload.error.code,
|
|
251
|
+
message: payload.error.message,
|
|
252
|
+
issues: payload.error.issues ?? [],
|
|
253
|
+
};
|
|
679
254
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.conflict = undefined;
|
|
685
|
-
this.error = undefined;
|
|
686
|
-
this.view = envelopeOf(this.confirmed);
|
|
687
|
-
if (choice === 'reapply') {
|
|
688
|
-
if (conflict.origin === 'undo' || conflict.origin === 'redo') {
|
|
689
|
-
const entry = conflict.entry;
|
|
690
|
-
const target = entry ? (conflict.origin === 'undo' ? entry.before : entry.after) : undefined;
|
|
691
|
-
const ops = target ? operationsBetween(envelopeOf(this.confirmed), target) : [];
|
|
692
|
-
if (ops.length > 0) {
|
|
693
|
-
this.queue.unshift({
|
|
694
|
-
type: 'ops',
|
|
695
|
-
ops,
|
|
696
|
-
key: this.key(),
|
|
697
|
-
origin: conflict.origin,
|
|
698
|
-
...(entry ? { entry } : {}),
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
} else {
|
|
702
|
-
try {
|
|
703
|
-
this.applyLocal(envelopeOf(this.confirmed), conflict.operations);
|
|
704
|
-
this.queue.unshift({ type: 'ops', ops: conflict.operations, key: this.key(), origin: 'reapply' });
|
|
705
|
-
} catch (error) {
|
|
706
|
-
this.error = toArtifactError(error);
|
|
707
|
-
this.announce({ type: 'rolled_back', error: this.error });
|
|
708
|
-
this.pump();
|
|
709
|
-
this.notify();
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
this.rebaseView(this.confirmed);
|
|
714
|
-
}
|
|
715
|
-
this.announce({ type: 'conflict_resolved', choice });
|
|
716
|
-
this.pump();
|
|
717
|
-
this.notify();
|
|
255
|
+
return {
|
|
256
|
+
code: 'INTERNAL_ERROR',
|
|
257
|
+
message: error instanceof Error ? error.message : String(error),
|
|
258
|
+
issues: [],
|
|
718
259
|
};
|
|
719
|
-
|
|
720
|
-
/**
|
|
721
|
-
* Stops timers and listeners. Buffered typing is still sent unless `discard` is set, so closing the
|
|
722
|
-
* editor right after typing does not lose the last keystrokes.
|
|
723
|
-
*/
|
|
724
|
-
dispose(options: { discard?: boolean } = {}): void {
|
|
725
|
-
if (this.disposed) return;
|
|
726
|
-
if (options.discard) {
|
|
727
|
-
this.buffer = [];
|
|
728
|
-
this.queue = this.inflight ? this.queue.slice(0, 1) : [];
|
|
729
|
-
} else {
|
|
730
|
-
this.flushBuffer();
|
|
731
|
-
this.pump();
|
|
732
|
-
}
|
|
733
|
-
this.clearTimer();
|
|
734
|
-
if (this.lockTimer !== undefined) clearTimeout(this.lockTimer);
|
|
735
|
-
this.disposed = true;
|
|
736
|
-
this.listeners.clear();
|
|
737
|
-
}
|
|
738
260
|
}
|