edytor 0.1.0-next.21 → 0.1.0-next.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cloudflare/DocumentRoom.d.ts +302 -10
- package/dist/cloudflare/DocumentRoom.js +965 -88
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/routeDocumentSocket.js +30 -6
- package/dist/collaboration/awarenessSelection.d.ts +7 -1
- package/dist/collaboration/awarenessSelection.js +7 -1
- package/dist/collaboration/index.d.ts +2 -2
- package/dist/collaboration/index.js +2 -2
- package/dist/collaboration/providers.d.ts +4 -0
- package/dist/collaboration/providers.js +4 -0
- package/dist/components/Edytor.svelte +14 -2
- package/dist/components/Edytor.svelte.d.ts +8 -1
- package/dist/crdt/admission.d.ts +6 -1
- package/dist/crdt/admission.js +9 -2
- package/dist/crdt/data.d.ts +3 -1
- package/dist/crdt/data.js +42 -32
- package/dist/crdt/document.d.ts +31 -8
- package/dist/crdt/document.js +53 -18
- package/dist/crdt/edytor-doc.d.ts +31 -3
- package/dist/crdt/edytor-doc.js +131 -14
- package/dist/crdt/engine.d.ts +6 -0
- package/dist/crdt/engine.js +7 -0
- package/dist/crdt/flow.d.ts +2 -0
- package/dist/crdt/flow.js +2 -2
- package/dist/crdt/index.d.ts +20 -9
- package/dist/crdt/index.js +3 -3
- package/dist/crdt/migration/migrate.js +9 -3
- package/dist/crdt/placement/model.d.ts +9 -1
- package/dist/crdt/placement/model.js +29 -10
- package/dist/crdt/placement/rank.d.ts +20 -0
- package/dist/crdt/placement/rank.js +79 -0
- package/dist/crdt/protocols/envelope.d.ts +20 -1
- package/dist/crdt/protocols/envelope.js +14 -2
- package/dist/crdt/providers/container.d.ts +15 -0
- package/dist/crdt/providers/container.js +17 -3
- package/dist/crdt/providers/index.d.ts +21 -1
- package/dist/crdt/providers/index.js +86 -3
- package/dist/crdt/providers/indexeddb.js +60 -40
- package/dist/crdt/providers/room.d.ts +18 -2
- package/dist/crdt/providers/room.js +29 -5
- package/dist/crdt/providers/websocket.d.ts +9 -1
- package/dist/crdt/providers/websocket.js +21 -6
- package/dist/crdt/storage.d.ts +21 -0
- package/dist/crdt/storage.js +37 -0
- package/dist/crdt/structs.d.ts +17 -0
- package/dist/crdt/structs.js +41 -0
- package/dist/crdt/text/model.d.ts +9 -1
- package/dist/crdt/text/model.js +24 -14
- package/dist/crdt/text/rows.d.ts +94 -0
- package/dist/crdt/text/rows.js +193 -0
- package/dist/crdt/text/runs.d.ts +30 -10
- package/dist/crdt/text/runs.js +1235 -264
- package/dist/edytor.svelte.d.ts +3 -1
- package/dist/edytor.svelte.js +9 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/image/ImageEmpty.svelte +19 -6
- package/dist/plugins/image/ImagePlugin.svelte +9 -3
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +2 -2
- package/dist/plugins/image/image.d.ts +16 -0
- package/dist/plugins/image/image.js +19 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/plugins.d.ts +11 -0
- package/dist/session/commands.d.ts +1 -0
- package/dist/session/commands.js +4 -1
- package/package.json +1 -1
package/dist/crdt/index.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export { type BlockId, type Destination, type ContentItem, type BlockSpec, type
|
|
|
57
57
|
export { type DocBlock } from './nodes.js';
|
|
58
58
|
export { type ContentRun } from './text/runs.js';
|
|
59
59
|
export type { YDoc, YUndoManager, YTransaction } from './engine-api.js';
|
|
60
|
-
export { type EdytorSync, type EdytorSyncPayload, type EdytorSyncCleanup, type IndexeddbSyncOptions, type WebsocketSync, type WebsocketSyncOptions, type ProviderStack } from './providers/index.js';
|
|
60
|
+
export { type EdytorSync, type EdytorSyncPayload, type EdytorSyncCleanup, type IndexeddbSyncOptions, type WebsocketSync, type WebsocketSyncOptions, type ProviderStack, type PrefetchOptions, type PrefetchResult, type LastUpdatedOptions } from './providers/index.js';
|
|
61
61
|
export { type IndexeddbPersistenceApi, type IndexeddbPersistenceOptions, type ProtocolMismatch, type SchemaMismatchDetail } from './providers/indexeddb.js';
|
|
62
62
|
export { type WebsocketProviderOptions, type WebsocketProviderEvents, type WebsocketPolyfill } from './providers/websocket.js';
|
|
63
63
|
export { SyncRefusedError } from './providers/room.js';
|
|
@@ -168,7 +168,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
168
168
|
isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
|
|
169
169
|
ranksAt: (siblings: readonly {
|
|
170
170
|
rank: string;
|
|
171
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
171
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
172
172
|
writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
|
|
173
173
|
withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
174
174
|
inserts: {
|
|
@@ -277,7 +277,10 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
277
277
|
checkSchema: () => import("./edytor-doc.js").SchemaProblem | null;
|
|
278
278
|
assertSchema: () => void;
|
|
279
279
|
dispose: () => void;
|
|
280
|
-
createUndoManager: (
|
|
280
|
+
createUndoManager: (options?: ConstructorParameters<EngineApi["UndoManager"]>[1] & {
|
|
281
|
+
limit?: number;
|
|
282
|
+
}) => import("./engine-api.js").YUndoManager;
|
|
283
|
+
releaseHistory: (um: import("./engine-api.js").YUndoManager) => void;
|
|
281
284
|
project: () => import("./index.js").ProjectedDoc;
|
|
282
285
|
toJSON: () => import("./index.js").JSONDoc;
|
|
283
286
|
blockJSON: (id: string) => import("./index.js").JSONBlock;
|
|
@@ -345,6 +348,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
345
348
|
})[];
|
|
346
349
|
onChange: (cb: (change: import("./edytor-doc.js").DocChange) => void) => (() => void);
|
|
347
350
|
};
|
|
351
|
+
keepCopies: (doc: import("./engine-api.js").EngineDoc) => void;
|
|
348
352
|
model: {
|
|
349
353
|
REGISTRY_KEY: string;
|
|
350
354
|
registryOf: (doc: import("./engine-api.js").EngineDoc) => import("./engine-api.js").EngineNode;
|
|
@@ -360,7 +364,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
360
364
|
isSelfOrDescendant: (placements: Map<import("./index.js").BlockId, import("./placement/model.js").ResolvedPlacement>, own: import("./text/model.js").Ownership, id: import("./index.js").BlockId, maybeAncestor: import("./index.js").BlockId) => boolean;
|
|
361
365
|
ranksAt: (siblings: readonly {
|
|
362
366
|
rank: string;
|
|
363
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
367
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
364
368
|
writePlacement: (doc: import("./engine-api.js").EngineDoc, node: import("./engine-api.js").EngineNode, p: import("./index.js").BlockId | null, r: string) => void;
|
|
365
369
|
withdrawOnUndo: (doc: import("./engine-api.js").EngineDoc) => (item: import("./engine-api.js").EngineItemRef, stackItem: {
|
|
366
370
|
inserts: {
|
|
@@ -416,6 +420,8 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
416
420
|
};
|
|
417
421
|
};
|
|
418
422
|
providers: {
|
|
423
|
+
prefetch: (options: import("./index.js").PrefetchOptions) => Promise<import("./index.js").PrefetchResult>;
|
|
424
|
+
lastUpdated: (options: import("./index.js").LastUpdatedOptions) => Promise<number | null>;
|
|
419
425
|
IndexeddbPersistence: {
|
|
420
426
|
new (name: string, doc: YDoc, options?: import("./index.js").IndexeddbPersistenceOptions): {
|
|
421
427
|
doc: YDoc;
|
|
@@ -495,7 +501,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
495
501
|
};
|
|
496
502
|
};
|
|
497
503
|
WebsocketProvider: {
|
|
498
|
-
new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc }?: import("./index.js").WebsocketProviderOptions): {
|
|
504
|
+
new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc, maxFrameBytes }?: import("./index.js").WebsocketProviderOptions): {
|
|
499
505
|
serverUrl: string;
|
|
500
506
|
roomname: string;
|
|
501
507
|
doc: YDoc;
|
|
@@ -503,6 +509,7 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
503
509
|
params: Record<string, string>;
|
|
504
510
|
maxBackoffTime: number;
|
|
505
511
|
connectTimeout: number;
|
|
512
|
+
maxFrameBytes: number;
|
|
506
513
|
_WS: import("./index.js").WebsocketPolyfill;
|
|
507
514
|
shouldConnect: boolean;
|
|
508
515
|
ws: WebSocket | null;
|
|
@@ -616,7 +623,11 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
616
623
|
};
|
|
617
624
|
/** U8 — the document-admission boundary (`admitUpdate` staged restore + the shared gate vocabulary). */
|
|
618
625
|
admission: {
|
|
619
|
-
admitUpdate: (update: Uint8Array
|
|
626
|
+
admitUpdate: (update: Uint8Array | ReadonlyArray<Uint8Array | {
|
|
627
|
+
v2: Uint8Array;
|
|
628
|
+
}>, docName?: string, options?: {
|
|
629
|
+
prepare?: (doc: YDoc) => void;
|
|
630
|
+
}) => YDoc;
|
|
620
631
|
};
|
|
621
632
|
/** Bind the attribution service to this engine — actor dictionary + legacy `a/` reads + block attribution (documents attach it automatically). */
|
|
622
633
|
attribution: {
|
|
@@ -627,9 +638,9 @@ export declare const bindCrdt: (Y: EngineApi) => {
|
|
|
627
638
|
};
|
|
628
639
|
export type Crdt = ReturnType<typeof bindCrdt>;
|
|
629
640
|
export type { EngineApi, EngineDoc, EngineNode, EngineDeepEvent, EngineTransaction, EngineItemRef, YNode, YItem } from './engine-api.js';
|
|
630
|
-
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
|
|
631
|
-
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion, type GenerationRecord } from './protocols/envelope.js';
|
|
632
|
-
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
|
|
641
|
+
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
|
|
642
|
+
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, STORED_GENERATION_RECORD, STORAGE_FORMAT, GenerationMismatchError, readProtocolVersion, type GenerationRecord, type StorageFormat } from './protocols/envelope.js';
|
|
643
|
+
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkLimitError, CLOSE } from './providers/room.js';
|
|
633
644
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, type SyncProtocol } from './protocols/sync.js';
|
|
634
645
|
export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
635
646
|
export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries, type AwarenessEntry } from './protocols/awareness.js';
|
package/dist/crdt/index.js
CHANGED
|
@@ -155,9 +155,9 @@ export const bindCrdt = (Y) => {
|
|
|
155
155
|
// hibernation), the store-before-ack frame (`messageSaved`) and the bounded
|
|
156
156
|
// catch-up codec (`chunkFrame`/`createChunkReader`, `messageChunk`).
|
|
157
157
|
// `pnpm check:worker` keeps this graph Worker-safe.
|
|
158
|
-
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError } from './edytor-doc.js';
|
|
159
|
-
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
|
|
160
|
-
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader } from './providers/room.js';
|
|
158
|
+
export { SCHEMA_VERSION, META_KEY, EdytorDocDisposedError, DEFAULT_HISTORY_LIMIT } from './edytor-doc.js';
|
|
159
|
+
export { GENERATION, generationWord, frame, PROTOCOL_VERSION, GENERATION_RECORD, STORED_GENERATION_RECORD, STORAGE_FORMAT, GenerationMismatchError, readProtocolVersion } from './protocols/envelope.js';
|
|
160
|
+
export { messageSync, messageAwareness, messageAuth, messageQueryAwareness, messageSaved, messageChunk, MAX_FRAME_BYTES, chunkFrame, createChunkReader, ChunkLimitError, CLOSE } from './providers/room.js';
|
|
161
161
|
export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate } from './protocols/sync.js';
|
|
162
162
|
export { messagePermissionDenied, messageReadOnly, writePermissionDenied, writeReadOnly } from './protocols/auth.js';
|
|
163
163
|
export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, readAwarenessEntries, writeAwarenessEntries } from './protocols/awareness.js';
|
|
@@ -33,7 +33,7 @@ import * as f from 'lib0-v14/function';
|
|
|
33
33
|
import { bindEdytorDoc } from '../edytor-doc.js';
|
|
34
34
|
import { bindLegacyReader } from './legacy-schema.js';
|
|
35
35
|
import { generationDbName } from '../protocols/envelope.js';
|
|
36
|
-
import { CUSTOM, decodeRow, encodeRow, openContainer, openIfExists, UPDATES, verifyOrStamp } from '../providers/container.js';
|
|
36
|
+
import { CUSTOM, decodeRow, encodeRow, readRow, openContainer, openIfExists, UPDATES, verifyOrStamp } from '../providers/container.js';
|
|
37
37
|
import { jsonBlockToSpec, sanitizeWireJson } from '../../utils/json.js';
|
|
38
38
|
const MIGRATION_KEY = 'migration';
|
|
39
39
|
/** The name of the migration lock of logical document `name`. */
|
|
@@ -170,8 +170,14 @@ export const bindMigration = (Y) => {
|
|
|
170
170
|
if (force) {
|
|
171
171
|
const [updates, custom] = idb.transact(db, [UPDATES, CUSTOM]);
|
|
172
172
|
await verifyOrStamp(name, updates, custom);
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
// Rows first (one transaction), then inflate: a snapshot row is v2, maybe gzip (P5).
|
|
174
|
+
for (const row of await idb.getAll(updates)) {
|
|
175
|
+
const update = await readRow(row);
|
|
176
|
+
if (update instanceof Uint8Array)
|
|
177
|
+
Y.applyUpdate(doc, update);
|
|
178
|
+
else
|
|
179
|
+
Y.applyUpdateV2(doc, update.v2);
|
|
180
|
+
}
|
|
175
181
|
}
|
|
176
182
|
const base = Y.encodeStateVector(doc);
|
|
177
183
|
edytorDoc.restore(doc, json.children.map((b) => jsonBlockToSpec(b)));
|
|
@@ -342,6 +342,14 @@ export declare const displayParentOf: (own: DisplayOwnership, pl: ResolvedPlacem
|
|
|
342
342
|
* promoted one ({@link displaySlotOf}).
|
|
343
343
|
*/
|
|
344
344
|
export declare const childrenIndex: (placements: Map<BlockId, ResolvedPlacement>, own: DisplayOwnership) => Map<BlockId | null, ChildSlot[]>;
|
|
345
|
+
/** The order of a children list: by rank, ties by id. */
|
|
346
|
+
export declare const bySlot: (a: {
|
|
347
|
+
id: BlockId;
|
|
348
|
+
rank: string;
|
|
349
|
+
}, b: {
|
|
350
|
+
id: BlockId;
|
|
351
|
+
rank: string;
|
|
352
|
+
}) => number;
|
|
345
353
|
/**
|
|
346
354
|
* Document order (O7): ONE pre-order over the visible blocks of a children
|
|
347
355
|
* index — `ids` in reading order, `at` the position of each id. Every
|
|
@@ -376,7 +384,7 @@ export declare const bindModel: (Y: EngineApi) => {
|
|
|
376
384
|
isSelfOrDescendant: (placements: Map<BlockId, ResolvedPlacement>, own: Ownership, id: BlockId, maybeAncestor: BlockId) => boolean;
|
|
377
385
|
ranksAt: (siblings: readonly {
|
|
378
386
|
rank: string;
|
|
379
|
-
}[], index: number, count: number, clientId: number, rand?: () => number) => string[];
|
|
387
|
+
}[], index: number, count: number, clientId: number, rand?: () => number, run?: boolean) => string[];
|
|
380
388
|
writePlacement: (doc: EngineDoc, node: EngineNode, p: BlockId | null, r: string) => void;
|
|
381
389
|
withdrawOnUndo: (doc: EngineDoc) => (item: EngineItemRef, stackItem: {
|
|
382
390
|
inserts: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeRank, encodeRank, rankBetween, RANK_VMIN } from './rank.js';
|
|
1
|
+
import { decodeRank, encodeRank, rankAfter, rankBetween, RANK_VMIN } from './rank.js';
|
|
2
2
|
import { AT, AT_NODE, BLOCK_NODE, CONTENT, CONTENT_NODE, DEL_PREFIX, hasDeleteMark, ID, CLAIMS, CLAIMS_NODE, INLINE_NODE, isNodeLike, NONCE, REGISTRY_KEY, TYPE, WITHDRAW_PREFIX } from '../schema.js';
|
|
3
3
|
import { nonceOf, randOf } from '../rand.js';
|
|
4
4
|
import { bindRuns } from '../text/runs.js';
|
|
@@ -409,11 +409,12 @@ export const childrenIndex = (placements, own) => {
|
|
|
409
409
|
else
|
|
410
410
|
index.set(parent, [slot]);
|
|
411
411
|
}
|
|
412
|
-
for (const bucket of index.values())
|
|
413
|
-
bucket.sort(
|
|
414
|
-
}
|
|
412
|
+
for (const bucket of index.values())
|
|
413
|
+
bucket.sort(bySlot);
|
|
415
414
|
return index;
|
|
416
415
|
};
|
|
416
|
+
/** The order of a children list: by rank, ties by id. */
|
|
417
|
+
export const bySlot = (a, b) => (a.rank === b.rank ? a.id.localeCompare(b.id) : a.rank < b.rank ? -1 : 1);
|
|
417
418
|
export const documentOrder = (kids) => {
|
|
418
419
|
const ids = [];
|
|
419
420
|
const at = new Map();
|
|
@@ -559,9 +560,10 @@ export const bindModel = (Y) => {
|
|
|
559
560
|
* so each new member takes `left` verbatim: it JOINS the tie and the
|
|
560
561
|
* `(rank, id)` display sort orders the whole group deterministically.
|
|
561
562
|
* On a valid seam the emitted chain is `rankBetween(left, right)` then
|
|
562
|
-
* `rankBetween(prev, right)
|
|
563
|
+
* `rankBetween(prev, right)` — with `run`, `rankAfter` (an insert: after a
|
|
564
|
+
* block this client ranked, in its run there, H1).
|
|
563
565
|
*/
|
|
564
|
-
const ranksAt = (siblings, index, count, clientId, rand) => {
|
|
566
|
+
const ranksAt = (siblings, index, count, clientId, rand, run = false) => {
|
|
565
567
|
let left = siblings[index - 1]?.rank;
|
|
566
568
|
const right = siblings[index]?.rank;
|
|
567
569
|
if (left !== undefined && right !== undefined && left >= right) {
|
|
@@ -569,7 +571,10 @@ export const bindModel = (Y) => {
|
|
|
569
571
|
}
|
|
570
572
|
const out = [];
|
|
571
573
|
for (let i = 0; i < count; i++) {
|
|
572
|
-
|
|
574
|
+
// An insert extends this client's run after a block it ranked (H1).
|
|
575
|
+
const r = run
|
|
576
|
+
? rankAfter(left, right, clientId, rand)
|
|
577
|
+
: rankBetween(left, right, clientId, rand);
|
|
573
578
|
out.push(r);
|
|
574
579
|
left = r;
|
|
575
580
|
}
|
|
@@ -749,12 +754,26 @@ export const bindModel = (Y) => {
|
|
|
749
754
|
* slot ({@link displaySlotOf}). Content is each block's display (R2).
|
|
750
755
|
*/
|
|
751
756
|
const project = (doc) => ({ children: R.attach(doc).project() });
|
|
752
|
-
/**
|
|
757
|
+
/**
|
|
758
|
+
* `positionOf` in a view: the display parent and the index among its
|
|
759
|
+
* visible children — a binary search by the block's display rank (the
|
|
760
|
+
* list's own order), not a scan of its siblings.
|
|
761
|
+
*/
|
|
753
762
|
const positionInView = (v, id) => {
|
|
754
763
|
if (!isLiveIn(v, id))
|
|
755
764
|
return null;
|
|
756
|
-
const
|
|
757
|
-
const
|
|
765
|
+
const slot = displaySlotOf(v.own, v.placements, v.placements.get(id), id);
|
|
766
|
+
const dp = slot.parent;
|
|
767
|
+
const list = v.kids.get(dp) ?? [];
|
|
768
|
+
let [lo, hi] = [0, list.length];
|
|
769
|
+
while (lo < hi) {
|
|
770
|
+
const mid = (lo + hi) >> 1;
|
|
771
|
+
if (bySlot(list[mid], { id, rank: slot.rank }) < 0)
|
|
772
|
+
lo = mid + 1;
|
|
773
|
+
else
|
|
774
|
+
hi = mid;
|
|
775
|
+
}
|
|
776
|
+
const index = list[lo]?.id === id ? lo : list.findIndex((s) => s.id === id);
|
|
758
777
|
return index < 0 ? null : { parent: dp, index };
|
|
759
778
|
};
|
|
760
779
|
/**
|
|
@@ -55,3 +55,23 @@ export declare const decodeRank: (rank: string) => RankSeg[];
|
|
|
55
55
|
/** Encode segments back to the persisted string form. */
|
|
56
56
|
export declare const encodeRank: (segs: readonly RankSeg[]) => string;
|
|
57
57
|
export declare const rankBetween: (left: string | undefined, right: string | undefined, clientId: number, rand?: () => number) => string;
|
|
58
|
+
/**
|
|
59
|
+
* The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
|
|
60
|
+
* of that form is the marker segment, then ranks of the client's own): still
|
|
61
|
+
* read as a run, so what a client inserts there stays in it.
|
|
62
|
+
*/
|
|
63
|
+
export declare const RANK_RUN: number;
|
|
64
|
+
/**
|
|
65
|
+
* A rank in `(left, right)` for an insert right after `left` — one rule for
|
|
66
|
+
* array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
|
|
67
|
+
* When `client` made `left`, the rank is in the client's run after it: `left`
|
|
68
|
+
* itself as the prefix, then one segment of the run band — or, when `left`
|
|
69
|
+
* is already a member of the client's run, that run's prefix and a member
|
|
70
|
+
* between `left` and the next one. So what one client inserts after its own
|
|
71
|
+
* items stays together whatever a peer inserts in that gap meanwhile: a
|
|
72
|
+
* peer's rank there is either no extension of the run's prefix (it sorts
|
|
73
|
+
* after the whole run) or one whose next digit is above the band (YATA's
|
|
74
|
+
* origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
|
|
75
|
+
* when the run rank would not sort below `right`, a plain {@link rankBetween}.
|
|
76
|
+
*/
|
|
77
|
+
export declare const rankAfter: (left: string | undefined, right: string | undefined, client: number, rand?: () => number) => string;
|
|
@@ -207,3 +207,82 @@ export const rankBetween = (left, right, clientId, rand = Math.random) => {
|
|
|
207
207
|
path.push(lSeg);
|
|
208
208
|
}
|
|
209
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* The digit of the run marker data arrays wrote up to 0.1.0-next.21 (a run
|
|
212
|
+
* of that form is the marker segment, then ranks of the client's own): still
|
|
213
|
+
* read as a run, so what a client inserts there stays in it.
|
|
214
|
+
*/
|
|
215
|
+
export const RANK_RUN = -(2 ** 39);
|
|
216
|
+
/**
|
|
217
|
+
* A run member's digit lies in `(RANK_RUN, RANK_RUN + RUN_BAND)`: below every
|
|
218
|
+
* digit a plain rank extends a prefix with (`0`, an append's), far above the
|
|
219
|
+
* digit minimum. A run opens at {@link RUN_START}, the middle of the band, so
|
|
220
|
+
* its members can be inserted before and after one another.
|
|
221
|
+
*/
|
|
222
|
+
const RUN_BAND = 2 ** 38;
|
|
223
|
+
const RUN_START = RANK_RUN + 2 ** 37;
|
|
224
|
+
const inRun = (v) => v > RANK_RUN && v < RANK_RUN + RUN_BAND;
|
|
225
|
+
/**
|
|
226
|
+
* A rank in `(left, right)` for an insert right after `left` — one rule for
|
|
227
|
+
* array items (`crdt/data.ts`) and inserted blocks (H1, `order.insert.run`).
|
|
228
|
+
* When `client` made `left`, the rank is in the client's run after it: `left`
|
|
229
|
+
* itself as the prefix, then one segment of the run band — or, when `left`
|
|
230
|
+
* is already a member of the client's run, that run's prefix and a member
|
|
231
|
+
* between `left` and the next one. So what one client inserts after its own
|
|
232
|
+
* items stays together whatever a peer inserts in that gap meanwhile: a
|
|
233
|
+
* peer's rank there is either no extension of the run's prefix (it sorts
|
|
234
|
+
* after the whole run) or one whose next digit is above the band (YATA's
|
|
235
|
+
* origin rule, as `Y.Array` keeps an insert after its origin). Elsewhere, or
|
|
236
|
+
* when the run rank would not sort below `right`, a plain {@link rankBetween}.
|
|
237
|
+
*/
|
|
238
|
+
export const rankAfter = (left, right, client, rand = Math.random) => {
|
|
239
|
+
const plain = () => rankBetween(left, right, client, rand);
|
|
240
|
+
if (left === undefined)
|
|
241
|
+
return plain();
|
|
242
|
+
const segs = decodeRank(left);
|
|
243
|
+
const last = segs.at(-1);
|
|
244
|
+
const bottom = encodeRank([{ v: RANK_RUN, t: 0 }]);
|
|
245
|
+
const top = encodeRank([{ v: RANK_RUN + RUN_BAND, t: 0 }]);
|
|
246
|
+
/**
|
|
247
|
+
* `prefix` and a rank after `after` (`undefined`: the run opens) in the
|
|
248
|
+
* run: inside the band when `band`. Opening, it takes the band's middle,
|
|
249
|
+
* or a digit below the run's first member; at the run's end it appends as
|
|
250
|
+
* an open end does (a window, not half the band); else between the two.
|
|
251
|
+
*/
|
|
252
|
+
const own = (prefix, after, band) => {
|
|
253
|
+
const tail = (r) => r && r.length > prefix.length && r.startsWith(prefix) ? r.slice(prefix.length) : undefined;
|
|
254
|
+
let next = tail(right);
|
|
255
|
+
if (band && next !== undefined && next > top)
|
|
256
|
+
next = undefined;
|
|
257
|
+
let mine;
|
|
258
|
+
if (after === undefined) {
|
|
259
|
+
if (next === undefined)
|
|
260
|
+
mine = encodeRank([{ v: RUN_START, t: client }]);
|
|
261
|
+
else if (band && next > bottom)
|
|
262
|
+
mine = rankBetween(bottom, next, client, rand);
|
|
263
|
+
else
|
|
264
|
+
return plain();
|
|
265
|
+
}
|
|
266
|
+
else if (next === undefined && band) {
|
|
267
|
+
const append = rankBetween(after, undefined, client, rand);
|
|
268
|
+
mine = append < top ? append : rankBetween(after, top, client, rand);
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
mine = rankBetween(after, next, client, rand);
|
|
272
|
+
const rank = prefix + mine;
|
|
273
|
+
return right === undefined || rank < right ? rank : plain();
|
|
274
|
+
};
|
|
275
|
+
// A member of the client's run: the next member, inside the band.
|
|
276
|
+
if (last.t === client && inRun(last.v))
|
|
277
|
+
return own(encodeRank(segs.slice(0, -1)), encodeRank([last]), true);
|
|
278
|
+
// A run of the earlier form (the marker, then the client's ranks): stays in it.
|
|
279
|
+
const k = segs.findLastIndex((g) => g.v === RANK_RUN);
|
|
280
|
+
if (k >= 0 && segs[k].t === client) {
|
|
281
|
+
const prefix = encodeRank(segs.slice(0, k + 1));
|
|
282
|
+
return own(prefix, left.length > prefix.length ? left.slice(prefix.length) : undefined, false);
|
|
283
|
+
}
|
|
284
|
+
// A rank the client made: its run opens right after it.
|
|
285
|
+
if (last.t === client)
|
|
286
|
+
return own(left, undefined, true);
|
|
287
|
+
return plain();
|
|
288
|
+
};
|
|
@@ -37,20 +37,39 @@ export declare const GENERATION: number;
|
|
|
37
37
|
export declare const GENERATION_PREFIX = "edytor-v14:";
|
|
38
38
|
/** The IndexedDB name a v14 provider uses for logical document `name`. */
|
|
39
39
|
export declare const generationDbName: (name: string) => string;
|
|
40
|
+
/**
|
|
41
|
+
* How a container stores its snapshots (P5, 0.1.0-next.23): `'v1'` (the
|
|
42
|
+
* update encoding, every container before it) or `'v2'` (the columnar
|
|
43
|
+
* encoding, gzip-compressed where the platform has `CompressionStream`;
|
|
44
|
+
* a compressed snapshot starts with gzip's `1f 8b`, a v2 update with
|
|
45
|
+
* `00`). Update rows stay v1 in both: a keystroke's update is smaller in
|
|
46
|
+
* v1 (24 bytes against 28, a one-character delete 13 against 24).
|
|
47
|
+
*/
|
|
48
|
+
export type StorageFormat = 'v1' | 'v2';
|
|
40
49
|
/** Record written to the `custom` store marking a DB as this generation's. */
|
|
41
50
|
export type GenerationRecord = {
|
|
42
51
|
engine: 'yjs-v14';
|
|
43
52
|
protocol: number;
|
|
44
53
|
schema: number;
|
|
54
|
+
/** Absent: `'v1'` (written before 0.1.0-next.23). */
|
|
55
|
+
storage?: StorageFormat;
|
|
45
56
|
};
|
|
46
57
|
export declare const GENERATION_KEY = "generation";
|
|
58
|
+
/** This generation's wire and schema (the storage format aside: {@link STORED_GENERATION_RECORD}). */
|
|
47
59
|
export declare const GENERATION_RECORD: GenerationRecord;
|
|
60
|
+
/** The storage format this build writes its snapshots in. */
|
|
61
|
+
export declare const STORAGE_FORMAT: StorageFormat;
|
|
62
|
+
/** The record this build stamps a container with (its generation and its storage format). */
|
|
63
|
+
export declare const STORED_GENERATION_RECORD: GenerationRecord;
|
|
48
64
|
/**
|
|
49
65
|
* Is `v` this generation's container record? A record without `schema` was
|
|
50
66
|
* written before the schema joined the generation, by builds that spoke
|
|
51
|
-
* schema 1 only — it is a schema-1 record.
|
|
67
|
+
* schema 1 only — it is a schema-1 record. A storage format this build
|
|
68
|
+
* does not read (a later one) is another generation's.
|
|
52
69
|
*/
|
|
53
70
|
export declare const isGenerationRecord: (v: unknown) => v is GenerationRecord;
|
|
71
|
+
/** The storage format of a container record ({@link isGenerationRecord} first). */
|
|
72
|
+
export declare const storageOf: (record: GenerationRecord) => StorageFormat;
|
|
54
73
|
export declare class GenerationMismatchError extends Error {
|
|
55
74
|
readonly dbName: string;
|
|
56
75
|
readonly found: unknown;
|
|
@@ -39,21 +39,33 @@ export const GENERATION_PREFIX = 'edytor-v14:';
|
|
|
39
39
|
/** The IndexedDB name a v14 provider uses for logical document `name`. */
|
|
40
40
|
export const generationDbName = (name) => GENERATION_PREFIX + name;
|
|
41
41
|
export const GENERATION_KEY = 'generation';
|
|
42
|
+
/** This generation's wire and schema (the storage format aside: {@link STORED_GENERATION_RECORD}). */
|
|
42
43
|
export const GENERATION_RECORD = {
|
|
43
44
|
engine: 'yjs-v14',
|
|
44
45
|
protocol: PROTOCOL_VERSION,
|
|
45
46
|
schema: SCHEMA_VERSION
|
|
46
47
|
};
|
|
48
|
+
/** The storage format this build writes its snapshots in. */
|
|
49
|
+
export const STORAGE_FORMAT = 'v2';
|
|
50
|
+
/** The record this build stamps a container with (its generation and its storage format). */
|
|
51
|
+
export const STORED_GENERATION_RECORD = {
|
|
52
|
+
...GENERATION_RECORD,
|
|
53
|
+
storage: STORAGE_FORMAT
|
|
54
|
+
};
|
|
47
55
|
/**
|
|
48
56
|
* Is `v` this generation's container record? A record without `schema` was
|
|
49
57
|
* written before the schema joined the generation, by builds that spoke
|
|
50
|
-
* schema 1 only — it is a schema-1 record.
|
|
58
|
+
* schema 1 only — it is a schema-1 record. A storage format this build
|
|
59
|
+
* does not read (a later one) is another generation's.
|
|
51
60
|
*/
|
|
52
61
|
export const isGenerationRecord = (v) => typeof v === 'object' &&
|
|
53
62
|
v !== null &&
|
|
54
63
|
v.engine === GENERATION_RECORD.engine &&
|
|
55
64
|
v.protocol === GENERATION_RECORD.protocol &&
|
|
56
|
-
(v.schema ?? 1) === SCHEMA_VERSION
|
|
65
|
+
(v.schema ?? 1) === SCHEMA_VERSION &&
|
|
66
|
+
[undefined, 'v1', 'v2'].includes(v.storage);
|
|
67
|
+
/** The storage format of a container record ({@link isGenerationRecord} first). */
|
|
68
|
+
export const storageOf = (record) => record.storage ?? 'v1';
|
|
57
69
|
export class GenerationMismatchError extends Error {
|
|
58
70
|
dbName;
|
|
59
71
|
found;
|
|
@@ -5,6 +5,21 @@ export declare const openContainer: (dbName: string) => Promise<IDBDatabase>;
|
|
|
5
5
|
/** A row is the update's bytes in a buffer of their own. */
|
|
6
6
|
export declare const encodeRow: (update: Uint8Array) => ArrayBuffer;
|
|
7
7
|
export declare const decodeRow: (row: unknown) => Uint8Array;
|
|
8
|
+
/**
|
|
9
|
+
* A snapshot row (P5, storage `'v2'`): the document in the v2 encoding,
|
|
10
|
+
* gzip-compressed where the platform has `CompressionStream` (and that
|
|
11
|
+
* makes it smaller). Update rows stay v1 `ArrayBuffer`s. A build before
|
|
12
|
+
* 0.1.0-next.23 reads such a row as no update at all (`decodeRow` throws),
|
|
13
|
+
* so it fails to load the store instead of misreading it.
|
|
14
|
+
*/
|
|
15
|
+
export type SnapshotRow = {
|
|
16
|
+
v2: ArrayBuffer;
|
|
17
|
+
};
|
|
18
|
+
export declare const snapshotRow: (updateV2: Uint8Array) => Promise<SnapshotRow>;
|
|
19
|
+
/** A stored row as the update to apply: `{ v2 }` for a snapshot row (inflated). */
|
|
20
|
+
export declare const readRow: (row: unknown) => Promise<Uint8Array | {
|
|
21
|
+
v2: Uint8Array;
|
|
22
|
+
}>;
|
|
8
23
|
/**
|
|
9
24
|
* The one verify-or-stamp rule: a container carrying this generation's
|
|
10
25
|
* record passes; an EMPTY container without a record is stamped; anything
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The v14 IndexedDB container (L58) — one owner for the store names, the row
|
|
3
|
-
* codec, the open, the non-creating probe and the ONE verify-or-stamp rule,
|
|
3
|
+
* codec (v1 update rows, v2 snapshot rows: P5), the open, the non-creating probe and the ONE verify-or-stamp rule,
|
|
4
4
|
* shared by the provider and the migrator. Rows are append-only for every
|
|
5
5
|
* writer (a provider's compaction appends its snapshot before deleting the
|
|
6
6
|
* rows it subsumes).
|
|
7
7
|
*/
|
|
8
8
|
import * as idb from 'lib0-v14/indexeddb';
|
|
9
|
-
import { GENERATION_KEY,
|
|
9
|
+
import { GENERATION_KEY, GenerationMismatchError, isGenerationRecord, STORED_GENERATION_RECORD } from '../protocols/envelope.js';
|
|
10
|
+
import { gunzip, packed } from '../storage.js';
|
|
10
11
|
export const UPDATES = 'updates';
|
|
11
12
|
export const CUSTOM = 'custom';
|
|
12
13
|
/** Open (creating when absent) a container with its two stores. */
|
|
@@ -20,6 +21,19 @@ export const decodeRow = (row) => {
|
|
|
20
21
|
return row;
|
|
21
22
|
throw new TypeError('Stored Yjs update is not binary data');
|
|
22
23
|
};
|
|
24
|
+
export const snapshotRow = async (updateV2) => ({
|
|
25
|
+
v2: (await packed(updateV2)).slice().buffer
|
|
26
|
+
});
|
|
27
|
+
/** A stored row as the update to apply: `{ v2 }` for a snapshot row (inflated). */
|
|
28
|
+
export const readRow = async (row) => {
|
|
29
|
+
if (typeof row === 'object' && row !== null && 'v2' in row) {
|
|
30
|
+
const bytes = row.v2;
|
|
31
|
+
if (!(bytes instanceof ArrayBuffer))
|
|
32
|
+
throw new TypeError('Stored snapshot is not binary data');
|
|
33
|
+
return { v2: await gunzip(new Uint8Array(bytes)) };
|
|
34
|
+
}
|
|
35
|
+
return decodeRow(row);
|
|
36
|
+
};
|
|
23
37
|
/**
|
|
24
38
|
* The one verify-or-stamp rule: a container carrying this generation's
|
|
25
39
|
* record passes; an EMPTY container without a record is stamped; anything
|
|
@@ -29,7 +43,7 @@ export const decodeRow = (row) => {
|
|
|
29
43
|
export const verifyOrStamp = async (name, updates, custom) => {
|
|
30
44
|
const found = await idb.get(custom, GENERATION_KEY);
|
|
31
45
|
if (found === undefined && (await idb.count(updates)) === 0) {
|
|
32
|
-
await idb.rtop(custom.put({ ...
|
|
46
|
+
await idb.rtop(custom.put({ ...STORED_GENERATION_RECORD }, GENERATION_KEY));
|
|
33
47
|
}
|
|
34
48
|
else if (!isGenerationRecord(found)) {
|
|
35
49
|
throw new GenerationMismatchError(name, found);
|
|
@@ -99,6 +99,8 @@ export type WebsocketSyncOptions = WebsocketTarget & {
|
|
|
99
99
|
onExpired?: (state: Parameters<WebsocketProviderEvents['expired']>[0]) => void;
|
|
100
100
|
/** Opt out of cross-tab sync over the BroadcastChannel (on by default). */
|
|
101
101
|
disableBc?: boolean;
|
|
102
|
+
/** Largest frame sent whole (default 32 MiB); larger ones are chunked (H6). */
|
|
103
|
+
maxFrameBytes?: number;
|
|
102
104
|
/**
|
|
103
105
|
* Keep a local copy in IndexedDB (default `true`; skipped where there is
|
|
104
106
|
* no `indexedDB`). Edits survive offline reloads and reach the server on
|
|
@@ -112,8 +114,25 @@ export type WebsocketSyncOptions = WebsocketTarget & {
|
|
|
112
114
|
export type WebsocketSync = EdytorSync & {
|
|
113
115
|
persistName?: string;
|
|
114
116
|
};
|
|
117
|
+
/** What {@link bindProviders}' `prefetch` takes: a websocket sync's target and options. */
|
|
118
|
+
export type PrefetchOptions = (WebsocketSyncOptions extends infer O ? O extends unknown ? Omit<O, 'onExpired' | 'persist' | 'disableBc'> : never : never) & {
|
|
119
|
+
/** ms before it gives up (default 30 000). */
|
|
120
|
+
timeout?: number;
|
|
121
|
+
};
|
|
122
|
+
/** What `prefetch` found: whether the room sent the store anything it lacked. */
|
|
123
|
+
export type PrefetchResult = {
|
|
124
|
+
updated: boolean;
|
|
125
|
+
};
|
|
126
|
+
/** What `lastUpdated` takes: the room it asks, and the `params` (a token) its dial would carry. */
|
|
127
|
+
export type LastUpdatedOptions = WebsocketTarget & {
|
|
128
|
+
params?: Record<string, string>;
|
|
129
|
+
/** The fetch to use (default the global one). */
|
|
130
|
+
fetch?: typeof fetch;
|
|
131
|
+
};
|
|
115
132
|
export type ProviderStack = ReturnType<typeof bindProviders>;
|
|
116
133
|
export declare const bindProviders: (Y: EngineApi) => {
|
|
134
|
+
prefetch: (options: PrefetchOptions) => Promise<PrefetchResult>;
|
|
135
|
+
lastUpdated: (options: LastUpdatedOptions) => Promise<number | null>;
|
|
117
136
|
IndexeddbPersistence: {
|
|
118
137
|
new (name: string, doc: YDoc, options?: import("./indexeddb.js").IndexeddbPersistenceOptions): {
|
|
119
138
|
doc: YDoc;
|
|
@@ -193,7 +212,7 @@ export declare const bindProviders: (Y: EngineApi) => {
|
|
|
193
212
|
};
|
|
194
213
|
};
|
|
195
214
|
WebsocketProvider: {
|
|
196
|
-
new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc }?: import("./websocket.js").WebsocketProviderOptions): {
|
|
215
|
+
new (serverUrl: string, roomname: string, doc: YDoc, { connect, awareness, params, WebSocketPolyfill, resyncInterval, maxBackoffTime, connectTimeout, disableBc, maxFrameBytes }?: import("./websocket.js").WebsocketProviderOptions): {
|
|
197
216
|
serverUrl: string;
|
|
198
217
|
roomname: string;
|
|
199
218
|
doc: YDoc;
|
|
@@ -201,6 +220,7 @@ export declare const bindProviders: (Y: EngineApi) => {
|
|
|
201
220
|
params: Record<string, string>;
|
|
202
221
|
maxBackoffTime: number;
|
|
203
222
|
connectTimeout: number;
|
|
223
|
+
maxFrameBytes: number;
|
|
204
224
|
_WS: import("./websocket.js").WebsocketPolyfill;
|
|
205
225
|
shouldConnect: boolean;
|
|
206
226
|
ws: WebSocket | null;
|