@pixerate/editor 0.278.0 → 0.279.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/canvas/editorDocument.d.cts +53 -0
- package/dist/canvas/editorDocument.d.ts +53 -0
- package/dist/canvas/index.cjs +169 -22
- package/dist/canvas/index.js +148 -1
- package/dist/chart/index.cjs +1 -1
- package/dist/chart/index.js +1 -1
- package/dist/{chunk-T2HUQBRC.cjs → chunk-DK33HLA2.cjs} +331 -10
- package/dist/{chunk-JV7DKLMX.js → chunk-KBH6CM3U.js} +326 -5
- package/dist/persistence/autosave.d.cts +15 -1
- package/dist/persistence/autosave.d.ts +15 -1
- package/dist/persistence/editorAutosave.d.cts +12 -1
- package/dist/persistence/editorAutosave.d.ts +12 -1
- package/dist/persistence/index.cjs +6 -2
- package/dist/persistence/index.d.cts +1 -0
- package/dist/persistence/index.d.ts +1 -0
- package/dist/persistence/index.js +5 -1
- package/dist/persistence/remoteSync.d.cts +152 -0
- package/dist/persistence/remoteSync.d.ts +152 -0
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { EditorDocument } from "./editorDocument.cjs";
|
|
2
|
+
import type { EditorAutosave } from "./editorAutosave.cjs";
|
|
3
|
+
export type EditorRemoteSyncState = "idle" | "pending" | "saving" | "saved" | "error";
|
|
4
|
+
export interface EditorRemoteSyncStatus {
|
|
5
|
+
status: EditorRemoteSyncState;
|
|
6
|
+
version: number;
|
|
7
|
+
savedVersion: number;
|
|
8
|
+
dirty: boolean;
|
|
9
|
+
lastSavedAt: number | null;
|
|
10
|
+
error?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface EditorRemoteSaveContext<TDocument = unknown, TDiff = unknown> {
|
|
13
|
+
document: TDocument;
|
|
14
|
+
/** The version to write. */
|
|
15
|
+
version: number;
|
|
16
|
+
/**
|
|
17
|
+
* The server version this save builds on (the last one confirmed). For a compare-and-set
|
|
18
|
+
* write, update only where the stored version still equals it, and return `{ conflict: true }`
|
|
19
|
+
* when nothing was updated.
|
|
20
|
+
*/
|
|
21
|
+
baseVersion: number;
|
|
22
|
+
/** Changes since the last saved snapshot (with the `diff` option). */
|
|
23
|
+
diff?: TDiff;
|
|
24
|
+
}
|
|
25
|
+
export interface EditorRemoteSaveResult {
|
|
26
|
+
version?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The server holds a newer version than `baseVersion`, so nothing was written. The controller
|
|
29
|
+
* calls `onConflict` (or reports an `EditorRemoteConflictError` without it) and keeps the edits.
|
|
30
|
+
*/
|
|
31
|
+
conflict?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface EditorRemoteConflictContext<TDocument = unknown> {
|
|
34
|
+
/** What this session tried to save. */
|
|
35
|
+
document: TDocument;
|
|
36
|
+
/** The last snapshot the server confirmed: the common base for a three-way merge. */
|
|
37
|
+
base: TDocument;
|
|
38
|
+
/** The server version reported with the conflict, when `save` returned one. */
|
|
39
|
+
serverVersion?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Runs `fn` (e.g. loading the merged document into the editor) without it counting as a local
|
|
42
|
+
* change, so it is not broadcast as one; the retry save picks the result up.
|
|
43
|
+
*/
|
|
44
|
+
apply<TResult>(fn: () => TResult): TResult;
|
|
45
|
+
}
|
|
46
|
+
export interface EditorRemoteConflictResolution<TDocument = unknown> {
|
|
47
|
+
/** The server's version the merge was made on; the retry save builds on it. */
|
|
48
|
+
serverVersion: number;
|
|
49
|
+
/** The server's document, used as the base for the retry's `diff` (default: the old base). */
|
|
50
|
+
serverDocument?: TDocument;
|
|
51
|
+
}
|
|
52
|
+
/** Reported through `onError` / `status().error` when a save conflicts and is not resolved. */
|
|
53
|
+
export declare class EditorRemoteConflictError extends Error {
|
|
54
|
+
readonly serverVersion?: number | undefined;
|
|
55
|
+
constructor(serverVersion?: number | undefined);
|
|
56
|
+
}
|
|
57
|
+
export interface EditorRemoteLocalChangeEvent<TDocument = unknown, TDiff = unknown> {
|
|
58
|
+
document: TDocument;
|
|
59
|
+
baseline: TDocument;
|
|
60
|
+
diff?: TDiff;
|
|
61
|
+
version: number;
|
|
62
|
+
}
|
|
63
|
+
export interface EditorRemoteSyncOptions<TDocument = unknown, TDiff = unknown> {
|
|
64
|
+
/**
|
|
65
|
+
* Persists the latest document snapshot and version to the remote backend.
|
|
66
|
+
* May return `{ version }` or a numeric `version` if the server advanced the version higher.
|
|
67
|
+
*/
|
|
68
|
+
save: (context: EditorRemoteSaveContext<TDocument, TDiff>) => Promise<EditorRemoteSaveResult | number | void>;
|
|
69
|
+
/** Initial known server version of the document (default `0`). */
|
|
70
|
+
initialVersion?: number;
|
|
71
|
+
/** Debounce delay in milliseconds after a local edit before calling `save` (default `500`). */
|
|
72
|
+
debounceMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Optional diff calculator. `onLocalChange` receives the diff from the previous local snapshot
|
|
75
|
+
* (the one edit); `save` receives the diff from the last saved snapshot (every edit it covers).
|
|
76
|
+
*/
|
|
77
|
+
diff?: (current: TDocument, baseline: TDocument) => TDiff;
|
|
78
|
+
/**
|
|
79
|
+
* Optional predicate returning `true` when `diff` represents no meaningful change.
|
|
80
|
+
* When omitted, compares `JSON.stringify(current) === JSON.stringify(baseline)`.
|
|
81
|
+
*/
|
|
82
|
+
isEmptyDiff?: (diff: TDiff, current: TDocument, baseline: TDocument) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Invoked immediately whenever a local document change is detected (before `save` debounce),
|
|
85
|
+
* e.g. for broadcasting incremental diffs to realtime collaborators.
|
|
86
|
+
*/
|
|
87
|
+
onLocalChange?: (event: EditorRemoteLocalChangeEvent<TDocument, TDiff>) => void;
|
|
88
|
+
/** Invoked when a remote `save` completes. */
|
|
89
|
+
onSaved?: (event: {
|
|
90
|
+
document: TDocument;
|
|
91
|
+
version: number;
|
|
92
|
+
}) => void;
|
|
93
|
+
/** Invoked when a remote `save` throws. */
|
|
94
|
+
onError?: (error: unknown) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Resolves a conflicting save (`save` returned `{ conflict: true }`): fetch the server's
|
|
97
|
+
* document, merge this session's edits into it (`base` → `document`), load the result with
|
|
98
|
+
* `apply`, and return the server's version. The controller then saves again on top of it.
|
|
99
|
+
*/
|
|
100
|
+
onConflict?: (context: EditorRemoteConflictContext<TDocument>) => Promise<EditorRemoteConflictResolution<TDocument>> | EditorRemoteConflictResolution<TDocument>;
|
|
101
|
+
/** Conflicts resolved in a row before the controller gives up with an error (default `3`). */
|
|
102
|
+
maxConflictRetries?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Optional crash-recovery autosave instance. When a remote save succeeds and no newer
|
|
105
|
+
* local edits occurred while it was in flight, `autosave.markClean()` is called automatically.
|
|
106
|
+
*/
|
|
107
|
+
autosave?: Pick<EditorAutosave, "markClean">;
|
|
108
|
+
/** Wall clock (default `Date.now`). */
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}
|
|
111
|
+
export interface EditorRemoteSync<TDocument = unknown, TDiff = unknown> {
|
|
112
|
+
/** Current local version number. */
|
|
113
|
+
version(): number;
|
|
114
|
+
/** Last version confirmed saved by the remote server. */
|
|
115
|
+
savedVersion(): number;
|
|
116
|
+
/** Whether local changes are waiting to be saved or currently saving. */
|
|
117
|
+
dirty(): boolean;
|
|
118
|
+
/** Current sync status snapshot. */
|
|
119
|
+
status(): EditorRemoteSyncStatus;
|
|
120
|
+
/** Last committed local baseline snapshot. */
|
|
121
|
+
baseline(): TDocument;
|
|
122
|
+
/** Checks the document for changes against `baseline()` (called automatically via `doc.subscribe`). */
|
|
123
|
+
notify(): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Runs `fn` while suppressing local change detection (so incoming collaborator diffs or
|
|
126
|
+
* server loads do not echo back a save), then updates `baseline` and advances `version` /
|
|
127
|
+
* `savedVersion` to `Math.max(version, remoteVersion)`.
|
|
128
|
+
*/
|
|
129
|
+
applyRemote<TResult>(fn: () => TResult, remoteVersion?: number): TResult;
|
|
130
|
+
/** Resets the baseline to `doc.getDocument()` and updates `version` / `savedVersion`. */
|
|
131
|
+
resetBaseline(remoteVersion?: number): void;
|
|
132
|
+
/** Cancels any debounce timer and immediately saves pending changes. */
|
|
133
|
+
flush(): Promise<boolean>;
|
|
134
|
+
/** Resolves when any pending or in-flight save has settled. */
|
|
135
|
+
whenIdle(): Promise<void>;
|
|
136
|
+
/** Subscribes to status changes. */
|
|
137
|
+
subscribe(listener: (status: EditorRemoteSyncStatus) => void): () => void;
|
|
138
|
+
/** Cancels timers and unsubscribes from `doc`, without discarding in-flight work. */
|
|
139
|
+
destroy(): void;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reactive, version-safe remote persistence controller for an `EditorDocument`.
|
|
143
|
+
*
|
|
144
|
+
* - Subscribes to `doc` so every edit (history steps, undo/redo, node drags/resizes, inline
|
|
145
|
+
* node data updates, and companion `extras` changes) automatically diffs against the last
|
|
146
|
+
* baseline, emits `onLocalChange` for realtime collaboration, and schedules a debounced save.
|
|
147
|
+
* - Serializes remote `save` calls so two saves never overlap on the wire; edits made while a
|
|
148
|
+
* save is in flight are coalesced into a single follow-up save.
|
|
149
|
+
* - Advances versions monotonically (`Math.max(localVersion, remoteVersion) + 1`), preventing
|
|
150
|
+
* version-mismatch wedging when collaborators or server triggers advance the remote version.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createEditorRemoteSync<TDocument = unknown, TDiff = unknown>(doc: EditorDocument<TDocument>, options: EditorRemoteSyncOptions<TDocument, TDiff>): EditorRemoteSync<TDocument, TDiff>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { EditorDocument } from "./editorDocument.js";
|
|
2
|
+
import type { EditorAutosave } from "./editorAutosave.js";
|
|
3
|
+
export type EditorRemoteSyncState = "idle" | "pending" | "saving" | "saved" | "error";
|
|
4
|
+
export interface EditorRemoteSyncStatus {
|
|
5
|
+
status: EditorRemoteSyncState;
|
|
6
|
+
version: number;
|
|
7
|
+
savedVersion: number;
|
|
8
|
+
dirty: boolean;
|
|
9
|
+
lastSavedAt: number | null;
|
|
10
|
+
error?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface EditorRemoteSaveContext<TDocument = unknown, TDiff = unknown> {
|
|
13
|
+
document: TDocument;
|
|
14
|
+
/** The version to write. */
|
|
15
|
+
version: number;
|
|
16
|
+
/**
|
|
17
|
+
* The server version this save builds on (the last one confirmed). For a compare-and-set
|
|
18
|
+
* write, update only where the stored version still equals it, and return `{ conflict: true }`
|
|
19
|
+
* when nothing was updated.
|
|
20
|
+
*/
|
|
21
|
+
baseVersion: number;
|
|
22
|
+
/** Changes since the last saved snapshot (with the `diff` option). */
|
|
23
|
+
diff?: TDiff;
|
|
24
|
+
}
|
|
25
|
+
export interface EditorRemoteSaveResult {
|
|
26
|
+
version?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The server holds a newer version than `baseVersion`, so nothing was written. The controller
|
|
29
|
+
* calls `onConflict` (or reports an `EditorRemoteConflictError` without it) and keeps the edits.
|
|
30
|
+
*/
|
|
31
|
+
conflict?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface EditorRemoteConflictContext<TDocument = unknown> {
|
|
34
|
+
/** What this session tried to save. */
|
|
35
|
+
document: TDocument;
|
|
36
|
+
/** The last snapshot the server confirmed: the common base for a three-way merge. */
|
|
37
|
+
base: TDocument;
|
|
38
|
+
/** The server version reported with the conflict, when `save` returned one. */
|
|
39
|
+
serverVersion?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Runs `fn` (e.g. loading the merged document into the editor) without it counting as a local
|
|
42
|
+
* change, so it is not broadcast as one; the retry save picks the result up.
|
|
43
|
+
*/
|
|
44
|
+
apply<TResult>(fn: () => TResult): TResult;
|
|
45
|
+
}
|
|
46
|
+
export interface EditorRemoteConflictResolution<TDocument = unknown> {
|
|
47
|
+
/** The server's version the merge was made on; the retry save builds on it. */
|
|
48
|
+
serverVersion: number;
|
|
49
|
+
/** The server's document, used as the base for the retry's `diff` (default: the old base). */
|
|
50
|
+
serverDocument?: TDocument;
|
|
51
|
+
}
|
|
52
|
+
/** Reported through `onError` / `status().error` when a save conflicts and is not resolved. */
|
|
53
|
+
export declare class EditorRemoteConflictError extends Error {
|
|
54
|
+
readonly serverVersion?: number | undefined;
|
|
55
|
+
constructor(serverVersion?: number | undefined);
|
|
56
|
+
}
|
|
57
|
+
export interface EditorRemoteLocalChangeEvent<TDocument = unknown, TDiff = unknown> {
|
|
58
|
+
document: TDocument;
|
|
59
|
+
baseline: TDocument;
|
|
60
|
+
diff?: TDiff;
|
|
61
|
+
version: number;
|
|
62
|
+
}
|
|
63
|
+
export interface EditorRemoteSyncOptions<TDocument = unknown, TDiff = unknown> {
|
|
64
|
+
/**
|
|
65
|
+
* Persists the latest document snapshot and version to the remote backend.
|
|
66
|
+
* May return `{ version }` or a numeric `version` if the server advanced the version higher.
|
|
67
|
+
*/
|
|
68
|
+
save: (context: EditorRemoteSaveContext<TDocument, TDiff>) => Promise<EditorRemoteSaveResult | number | void>;
|
|
69
|
+
/** Initial known server version of the document (default `0`). */
|
|
70
|
+
initialVersion?: number;
|
|
71
|
+
/** Debounce delay in milliseconds after a local edit before calling `save` (default `500`). */
|
|
72
|
+
debounceMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Optional diff calculator. `onLocalChange` receives the diff from the previous local snapshot
|
|
75
|
+
* (the one edit); `save` receives the diff from the last saved snapshot (every edit it covers).
|
|
76
|
+
*/
|
|
77
|
+
diff?: (current: TDocument, baseline: TDocument) => TDiff;
|
|
78
|
+
/**
|
|
79
|
+
* Optional predicate returning `true` when `diff` represents no meaningful change.
|
|
80
|
+
* When omitted, compares `JSON.stringify(current) === JSON.stringify(baseline)`.
|
|
81
|
+
*/
|
|
82
|
+
isEmptyDiff?: (diff: TDiff, current: TDocument, baseline: TDocument) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Invoked immediately whenever a local document change is detected (before `save` debounce),
|
|
85
|
+
* e.g. for broadcasting incremental diffs to realtime collaborators.
|
|
86
|
+
*/
|
|
87
|
+
onLocalChange?: (event: EditorRemoteLocalChangeEvent<TDocument, TDiff>) => void;
|
|
88
|
+
/** Invoked when a remote `save` completes. */
|
|
89
|
+
onSaved?: (event: {
|
|
90
|
+
document: TDocument;
|
|
91
|
+
version: number;
|
|
92
|
+
}) => void;
|
|
93
|
+
/** Invoked when a remote `save` throws. */
|
|
94
|
+
onError?: (error: unknown) => void;
|
|
95
|
+
/**
|
|
96
|
+
* Resolves a conflicting save (`save` returned `{ conflict: true }`): fetch the server's
|
|
97
|
+
* document, merge this session's edits into it (`base` → `document`), load the result with
|
|
98
|
+
* `apply`, and return the server's version. The controller then saves again on top of it.
|
|
99
|
+
*/
|
|
100
|
+
onConflict?: (context: EditorRemoteConflictContext<TDocument>) => Promise<EditorRemoteConflictResolution<TDocument>> | EditorRemoteConflictResolution<TDocument>;
|
|
101
|
+
/** Conflicts resolved in a row before the controller gives up with an error (default `3`). */
|
|
102
|
+
maxConflictRetries?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Optional crash-recovery autosave instance. When a remote save succeeds and no newer
|
|
105
|
+
* local edits occurred while it was in flight, `autosave.markClean()` is called automatically.
|
|
106
|
+
*/
|
|
107
|
+
autosave?: Pick<EditorAutosave, "markClean">;
|
|
108
|
+
/** Wall clock (default `Date.now`). */
|
|
109
|
+
now?: () => number;
|
|
110
|
+
}
|
|
111
|
+
export interface EditorRemoteSync<TDocument = unknown, TDiff = unknown> {
|
|
112
|
+
/** Current local version number. */
|
|
113
|
+
version(): number;
|
|
114
|
+
/** Last version confirmed saved by the remote server. */
|
|
115
|
+
savedVersion(): number;
|
|
116
|
+
/** Whether local changes are waiting to be saved or currently saving. */
|
|
117
|
+
dirty(): boolean;
|
|
118
|
+
/** Current sync status snapshot. */
|
|
119
|
+
status(): EditorRemoteSyncStatus;
|
|
120
|
+
/** Last committed local baseline snapshot. */
|
|
121
|
+
baseline(): TDocument;
|
|
122
|
+
/** Checks the document for changes against `baseline()` (called automatically via `doc.subscribe`). */
|
|
123
|
+
notify(): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Runs `fn` while suppressing local change detection (so incoming collaborator diffs or
|
|
126
|
+
* server loads do not echo back a save), then updates `baseline` and advances `version` /
|
|
127
|
+
* `savedVersion` to `Math.max(version, remoteVersion)`.
|
|
128
|
+
*/
|
|
129
|
+
applyRemote<TResult>(fn: () => TResult, remoteVersion?: number): TResult;
|
|
130
|
+
/** Resets the baseline to `doc.getDocument()` and updates `version` / `savedVersion`. */
|
|
131
|
+
resetBaseline(remoteVersion?: number): void;
|
|
132
|
+
/** Cancels any debounce timer and immediately saves pending changes. */
|
|
133
|
+
flush(): Promise<boolean>;
|
|
134
|
+
/** Resolves when any pending or in-flight save has settled. */
|
|
135
|
+
whenIdle(): Promise<void>;
|
|
136
|
+
/** Subscribes to status changes. */
|
|
137
|
+
subscribe(listener: (status: EditorRemoteSyncStatus) => void): () => void;
|
|
138
|
+
/** Cancels timers and unsubscribes from `doc`, without discarding in-flight work. */
|
|
139
|
+
destroy(): void;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Reactive, version-safe remote persistence controller for an `EditorDocument`.
|
|
143
|
+
*
|
|
144
|
+
* - Subscribes to `doc` so every edit (history steps, undo/redo, node drags/resizes, inline
|
|
145
|
+
* node data updates, and companion `extras` changes) automatically diffs against the last
|
|
146
|
+
* baseline, emits `onLocalChange` for realtime collaboration, and schedules a debounced save.
|
|
147
|
+
* - Serializes remote `save` calls so two saves never overlap on the wire; edits made while a
|
|
148
|
+
* save is in flight are coalesced into a single follow-up save.
|
|
149
|
+
* - Advances versions monotonically (`Math.max(localVersion, remoteVersion) + 1`), preventing
|
|
150
|
+
* version-mismatch wedging when collaborators or server triggers advance the remote version.
|
|
151
|
+
*/
|
|
152
|
+
export declare function createEditorRemoteSync<TDocument = unknown, TDiff = unknown>(doc: EditorDocument<TDocument>, options: EditorRemoteSyncOptions<TDocument, TDiff>): EditorRemoteSync<TDocument, TDiff>;
|