edytor 5.0.0 → 7.0.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/README.md +3 -3
- package/dist/cloudflare/DocumentRoom.d.ts +77 -2
- package/dist/cloudflare/DocumentRoom.js +37 -1
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/room/access.d.ts +6 -1
- package/dist/cloudflare/room/access.js +30 -4
- package/dist/cloudflare/room/admission.d.ts +11 -0
- package/dist/cloudflare/room/admission.js +43 -10
- package/dist/cloudflare/room/context.d.ts +73 -76
- package/dist/cloudflare/room/context.js +17 -6
- package/dist/cloudflare/room/forged.d.ts +1 -1
- package/dist/cloudflare/room/forged.js +11 -9
- package/dist/cloudflare/room/frames.d.ts +2 -0
- package/dist/cloudflare/room/frames.js +2 -0
- package/dist/cloudflare/room/presence.js +1 -4
- package/dist/cloudflare/room/reseed.d.ts +93 -0
- package/dist/cloudflare/room/reseed.js +500 -0
- package/dist/cloudflare/room/scheduler.d.ts +1 -1
- package/dist/cloudflare/room/scheduler.js +9 -1
- package/dist/cloudflare/room/shared.d.ts +10 -0
- package/dist/cloudflare/room/shared.js +10 -0
- package/dist/cloudflare/room/storage.d.ts +7 -0
- package/dist/cloudflare/room/storage.js +33 -1
- package/dist/components/Edytor.svelte +18 -3
- package/dist/components/Edytor.svelte.d.ts +10 -1
- package/dist/components/EdytorRoot.svelte +17 -0
- package/dist/components/EdytorRoot.svelte.d.ts +4 -0
- package/dist/components/Suggestion.svelte +1 -1
- package/dist/crdt/attribution/block.d.ts +3 -0
- package/dist/crdt/attribution/block.js +3 -2
- package/dist/crdt/doc/history.js +2 -3
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +157 -157
- package/dist/crdt/edytor-doc.js +15 -43
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +64 -75
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/protocol.d.ts +2 -2
- package/dist/crdt/protocol.js +2 -2
- package/dist/crdt/protocols/auth.d.ts +8 -1
- package/dist/crdt/protocols/auth.js +14 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +11 -22
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +26 -0
- package/dist/crdt/providers/room.js +41 -12
- package/dist/crdt/providers/websocket.d.ts +10 -14
- package/dist/crdt/providers/websocket.js +43 -23
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/reseed.d.ts +127 -0
- package/dist/crdt/reseed.js +533 -0
- package/dist/crdt/restore.d.ts +13 -1
- package/dist/crdt/restore.js +8 -3
- package/dist/crdt/schema.d.ts +6 -0
- package/dist/crdt/schema.js +6 -0
- package/dist/crdt/structs.d.ts +8 -11
- package/dist/crdt/structs.js +25 -44
- package/dist/crdt/text/index/build.d.ts +119 -0
- package/dist/crdt/text/index/build.js +2801 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +2 -5
- package/dist/crdt/text/model.js +2 -3
- package/dist/crdt/text/runs.d.ts +9 -12
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
- package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/commands.js +49 -51
- package/dist/session/handles.js +3 -7
- package/dist/surface/attributes.d.ts +3 -3
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/observer.svelte.d.ts +7 -7
- package/dist/surface/observer.svelte.js +225 -336
- package/dist/surface/projector.svelte.js +2 -2
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -207
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -198
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -203
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -22
- package/dist/crdt/text/index/layout.js +0 -258
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -738
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable editor with an API to build any kind of collaborative rich text editor.
|
|
19
19
|
|
|
20
|
-
> **
|
|
20
|
+
> **7.0.** Edytor `7.0.0` is on npm as `edytor`: its API follows semantic versioning, and every change is listed in the changelog. It has no production track record yet: read the [limitations](https://edytor.dev/docs/reference/limitations) before you build on it. Issues and PRs are welcome: when you report a bug, include the document's JSON value.
|
|
21
21
|
|
|
22
22
|
## Features
|
|
23
23
|
|
|
@@ -34,13 +34,13 @@ Edytor aims to be for Svelte what Slate.js is for React: a heavily customizable
|
|
|
34
34
|
- **Plugins that can veto anything.** Every command is prepared before it writes; plugins see the command and each planned step and can refuse or replace it.
|
|
35
35
|
- **Real-time collaboration.** One `EdytorDocument` shared by any number of views, or none (headless). Presence cursors, identity-preserving moves, splits and merges, and undo that only takes back your own edits.
|
|
36
36
|
- **Offline first.** A local IndexedDB copy and cross-tab sync; offline edits survive reloads and reach the server on reconnect.
|
|
37
|
-
- **A Cloudflare Durable Object room.** `edytor/cloudflare` stores in SQLite, acknowledges after the write, binds identity to the socket and hibernates.
|
|
37
|
+
- **A Cloudflare Durable Object room.** `edytor/cloudflare` stores in SQLite, acknowledges after the write, binds identity to the socket and hibernates. A busy document is [re-seeded](https://edytor.dev/docs/server/room#re-seeding) from its content when it grows, offline edits merged when their device comes back.
|
|
38
38
|
- **Import and export.** `edytor/export` turns a document into HTML or Markdown and back (`toHTML`, `toMarkdown`, `fromHTML`, `textToBlocks`), by each kind's own forms, in the browser, Node or a Worker; HTML import is sanitized ([Import and export](https://edytor.dev/docs/editor/import-export)).
|
|
39
39
|
- **Headless and Worker-safe.** `edytor/crdt/edytor` runs in Node and Workers without Svelte.
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
|
-
Edytor `
|
|
43
|
+
Edytor `7.0.0` is on npm under `latest`:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
pnpm add edytor
|
|
@@ -111,7 +111,7 @@ import type { CommitResult, ExportedBlocks, ImportReceipt, ImportRequest, LateEd
|
|
|
111
111
|
import type { AwarenessEntry } from '../crdt/protocol.js';
|
|
112
112
|
import type { DocumentSemanticsConfig, DocumentOperations, JSONBlock, JSONDoc, YDoc } from '../crdt/index.js';
|
|
113
113
|
import type { CommentActor, CommentChange, CommentOutcome, CommentRequest, CommentSnapshot } from '../crdt/protocols/comments.js';
|
|
114
|
-
export { closedSocket, COMMENTS_HEADER, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, MAX_REFUSALS, MAX_WAITING_DELETES, noTimers, PROBE_HEADER, PURGE_ORIGIN, RESTORE_ORIGIN, ROOM_ORIGIN, SOCKET_TAG } from './room/shared.js';
|
|
114
|
+
export { closedSocket, COMMENTS_HEADER, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, DEFAULT_RESEED_FORCE, DEFAULT_RESEED_GAIN, DEFAULT_RESEED_GRACE_DAYS, DEFAULT_RESEED_IDLE, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, MAX_REFUSALS, MAX_WAITING_DELETES, noTimers, PROBE_HEADER, PURGE_ORIGIN, RESTORE_ORIGIN, ROOM_ORIGIN, SOCKET_TAG } from './room/shared.js';
|
|
115
115
|
/** Under SQLite-backed Durable Objects' 2 MB row cap, with room for the other columns. */
|
|
116
116
|
export declare const DEFAULT_MAX_ROW_BYTES: number;
|
|
117
117
|
/** Update records before the rows are merged into one snapshot. */
|
|
@@ -224,6 +224,8 @@ export type DocumentRoomEnv = {
|
|
|
224
224
|
EDYTOR_MAX_PRESENCE_FANOUT?: string | number;
|
|
225
225
|
/** `off`: the room logs nothing (default: one JSON line per compaction, quota hit, denial, fault). */
|
|
226
226
|
EDYTOR_LOG?: string;
|
|
227
|
+
/** `off`: the room never re-seeds its document ({@link AttachRoomOptions.reseed}). */
|
|
228
|
+
EDYTOR_RESEED?: string;
|
|
227
229
|
/**
|
|
228
230
|
* Days after which deleted content is purged (default: the history
|
|
229
231
|
* retention, else 30); `off` never purges.
|
|
@@ -279,9 +281,11 @@ export type SocketIdentity = {
|
|
|
279
281
|
export type Attachment = SocketIdentity & {
|
|
280
282
|
clock: number | null;
|
|
281
283
|
comments?: boolean;
|
|
284
|
+
/** The older epoch the socket dialed: it may only hand over its old document (`room.reseed.protocol`). */
|
|
285
|
+
epoch?: number;
|
|
282
286
|
};
|
|
283
287
|
export type Refusal = {
|
|
284
|
-
reason: 'generation' | 'schema' | 'malformed' | 'container' | 'identity' | 'replica' | 'read-only' | 'waiting' | 'storage' | 'internal' | 'quota' | 'access' | 'presence' | 'comments' | 'mark' | 'suggest-only' | 'forged' | 'denied' | 'orphan' | 'relayed' | 'history';
|
|
288
|
+
reason: 'generation' | 'schema' | 'malformed' | 'container' | 'identity' | 'replica' | 'read-only' | 'waiting' | 'storage' | 'internal' | 'quota' | 'access' | 'presence' | 'comments' | 'mark' | 'suggest-only' | 'forged' | 'denied' | 'orphan' | 'relayed' | 'history' | 'epoch';
|
|
285
289
|
detail: unknown;
|
|
286
290
|
};
|
|
287
291
|
/** A duration series: how many, their total and longest (ms; see {@link RoomMetrics}). */
|
|
@@ -346,6 +350,8 @@ export type RoomMetrics = {
|
|
|
346
350
|
runs: number;
|
|
347
351
|
horizon: number | null;
|
|
348
352
|
};
|
|
353
|
+
/** The room's epoch (`room.reseed.protocol`): how many times its document was re-seeded. */
|
|
354
|
+
epoch: number;
|
|
349
355
|
/** When this instance started (ms since the epoch). */
|
|
350
356
|
since: number;
|
|
351
357
|
};
|
|
@@ -402,6 +408,31 @@ export type RoomLogEntry = {
|
|
|
402
408
|
role: 'out' | 'in';
|
|
403
409
|
peer: string;
|
|
404
410
|
blocks: number;
|
|
411
|
+
} | {
|
|
412
|
+
edytor: 'reseed';
|
|
413
|
+
epoch: number;
|
|
414
|
+
before: number;
|
|
415
|
+
after: number;
|
|
416
|
+
suggestions: number;
|
|
417
|
+
ms: number;
|
|
418
|
+
} | {
|
|
419
|
+
edytor: 'reseed';
|
|
420
|
+
skipped: 'suggestions';
|
|
421
|
+
bytes: number;
|
|
422
|
+
pending: number;
|
|
423
|
+
} | {
|
|
424
|
+
edytor: 'reseed';
|
|
425
|
+
skipped: 'gain';
|
|
426
|
+
bytes: number;
|
|
427
|
+
estimate: number;
|
|
428
|
+
} | {
|
|
429
|
+
edytor: 'rebase';
|
|
430
|
+
user: string;
|
|
431
|
+
from: number;
|
|
432
|
+
epoch: number;
|
|
433
|
+
status: 'noop' | 'applied' | 'refused' | 'failed';
|
|
434
|
+
bytes: number;
|
|
435
|
+
ms: number;
|
|
405
436
|
} | {
|
|
406
437
|
edytor: 'late';
|
|
407
438
|
moveId: string;
|
|
@@ -476,6 +507,22 @@ export type CommentOptions = {
|
|
|
476
507
|
*/
|
|
477
508
|
maxRequestsPerSecond?: number;
|
|
478
509
|
};
|
|
510
|
+
/** When the room re-seeds its document (`AttachRoomOptions.reseed`, `room.reseed.timing`). */
|
|
511
|
+
export type ReseedOptions = {
|
|
512
|
+
/** Stored bytes past which a re-seed is due (default half of `maxDocumentBytes`). */
|
|
513
|
+
at?: number;
|
|
514
|
+
/** ms nobody may write before it runs (default {@link DEFAULT_RESEED_IDLE}, ten minutes). */
|
|
515
|
+
idleMs?: number;
|
|
516
|
+
/** The share of the stored bytes it must give back (default {@link DEFAULT_RESEED_GAIN}, 0.4). */
|
|
517
|
+
gain?: number;
|
|
518
|
+
/**
|
|
519
|
+
* The share of `maxDocumentBytes` past which it waits for no idle period
|
|
520
|
+
* and drops the pending suggestions (default {@link DEFAULT_RESEED_FORCE}, 0.8).
|
|
521
|
+
*/
|
|
522
|
+
force?: number;
|
|
523
|
+
/** Days a cut is kept, so an offline client's edits still merge (default {@link DEFAULT_RESEED_GRACE_DAYS}, 30). */
|
|
524
|
+
graceDays?: number;
|
|
525
|
+
};
|
|
479
526
|
/** What {@link attachRoom} (and `DocumentRoom`) takes. */
|
|
480
527
|
export type AttachRoomOptions = {
|
|
481
528
|
/**
|
|
@@ -583,6 +630,16 @@ export type AttachRoomOptions = {
|
|
|
583
630
|
purgeAfterDays?: number | false;
|
|
584
631
|
/** The room's clock, ms since the epoch (default `Date.now`): its slots, epochs and alarm read it. */
|
|
585
632
|
now?: () => number;
|
|
633
|
+
/**
|
|
634
|
+
* Re-seed (`room.reseed.*`): a document that grew past `at` of its stored
|
|
635
|
+
* bytes (default half of `maxDocumentBytes`) is rebuilt from its content,
|
|
636
|
+
* with fresh identities, once nobody wrote for `idleMs` (default ten
|
|
637
|
+
* minutes) and when that gives back at least `gain` (0.4) of it; past
|
|
638
|
+
* `force` (0.8) of the quota at once. Every client reloads it; an offline
|
|
639
|
+
* client's unsaved edits are rebased onto it for `graceDays` (30). On by
|
|
640
|
+
* default; `false` never re-seeds.
|
|
641
|
+
*/
|
|
642
|
+
reseed?: ReseedOptions | false;
|
|
586
643
|
/**
|
|
587
644
|
* Comment threads (`room.comments`): kept in the room's `threads` and
|
|
588
645
|
* `comments` tables, read and written through `routeDocumentComments`
|
|
@@ -821,6 +878,15 @@ export declare class AttachedDocument {
|
|
|
821
878
|
}) | null;
|
|
822
879
|
/** The room's counters, also over RPC — see {@link RoomMetrics}. */
|
|
823
880
|
metrics(): RoomMetrics;
|
|
881
|
+
/** The room's epoch (`room.reseed.protocol`): how many times its document was re-seeded. */
|
|
882
|
+
get epoch(): number;
|
|
883
|
+
/**
|
|
884
|
+
* Re-seed the document now (`room.reseed.cut`), whatever it gives back
|
|
885
|
+
* and whether a suggestion is pending (they are dropped): every socket
|
|
886
|
+
* is closed `4410` and reloads it. Answers the new epoch, or `null`
|
|
887
|
+
* when re-seeding is off or the room is inside a transaction.
|
|
888
|
+
*/
|
|
889
|
+
reseed(): number | null;
|
|
824
890
|
/**
|
|
825
891
|
* Drop every waiting delete, stored or in memory (callable over RPC):
|
|
826
892
|
* the way out when a writer filled `MAX_WAITING_DELETES` with
|
|
@@ -925,6 +991,11 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
|
|
|
925
991
|
* Read once, at the first frame.
|
|
926
992
|
*/
|
|
927
993
|
protected locks(): LockOptions | undefined;
|
|
994
|
+
/**
|
|
995
|
+
* When the room re-seeds — see {@link AttachRoomOptions.reseed}. Default:
|
|
996
|
+
* on, with its default thresholds; `false` when `EDYTOR_RESEED` is `off`.
|
|
997
|
+
*/
|
|
998
|
+
protected reseeding(): ReseedOptions | false | undefined;
|
|
928
999
|
/**
|
|
929
1000
|
* The rooms blocks move to — see {@link AttachRoomOptions.rooms}.
|
|
930
1001
|
* Default: the `EDYTOR_ROOMS` binding; none without it.
|
|
@@ -946,6 +1017,10 @@ export declare class DocumentRoom<Env = DocumentRoomEnv> extends DurableObject<E
|
|
|
946
1017
|
restoreHistory(key: string, options?: {
|
|
947
1018
|
user?: string;
|
|
948
1019
|
}): Promise<RestoreResult>;
|
|
1020
|
+
/** Re-seed the document now, also over RPC — see {@link AttachedDocument.reseed}. */
|
|
1021
|
+
reseed(): number | null;
|
|
1022
|
+
/** The room's epoch — see {@link AttachedDocument.epoch}. */
|
|
1023
|
+
get epoch(): number;
|
|
949
1024
|
/** Undo the last restore, also over RPC — see {@link AttachedDocument.undoRestore}. */
|
|
950
1025
|
undoRestore(options?: {
|
|
951
1026
|
user?: string;
|
|
@@ -114,12 +114,13 @@ import { RoomHistory } from './room/history.js';
|
|
|
114
114
|
import { RoomMoves } from './room/moves.js';
|
|
115
115
|
import { RoomPresence } from './room/presence.js';
|
|
116
116
|
import { RoomPurge } from './room/purge.js';
|
|
117
|
+
import { RoomReseed } from './room/reseed.js';
|
|
117
118
|
import { ReplicaRegistry } from './room/replicas.js';
|
|
118
119
|
import { Scheduler } from './room/scheduler.js';
|
|
119
120
|
import { RoomStorage } from './room/storage.js';
|
|
120
121
|
import { RoomValidation } from './room/validation.js';
|
|
121
122
|
import { noTimers, SOCKET_TAG } from './room/shared.js';
|
|
122
|
-
export { closedSocket, COMMENTS_HEADER, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, MAX_REFUSALS, MAX_WAITING_DELETES, noTimers, PROBE_HEADER, PURGE_ORIGIN, RESTORE_ORIGIN, ROOM_ORIGIN, SOCKET_TAG } from './room/shared.js';
|
|
123
|
+
export { closedSocket, COMMENTS_HEADER, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, DEFAULT_RESEED_FORCE, DEFAULT_RESEED_GAIN, DEFAULT_RESEED_GRACE_DAYS, DEFAULT_RESEED_IDLE, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, MAX_REFUSALS, MAX_WAITING_DELETES, noTimers, PROBE_HEADER, PURGE_ORIGIN, RESTORE_ORIGIN, ROOM_ORIGIN, SOCKET_TAG } from './room/shared.js';
|
|
123
124
|
/** Under SQLite-backed Durable Objects' 2 MB row cap, with room for the other columns. */
|
|
124
125
|
export const DEFAULT_MAX_ROW_BYTES = 2_000_000 - 4096;
|
|
125
126
|
/** Update records before the rows are merged into one snapshot. */
|
|
@@ -245,6 +246,7 @@ export class AttachedDocument {
|
|
|
245
246
|
purge: () => {
|
|
246
247
|
room.purge.tick();
|
|
247
248
|
},
|
|
249
|
+
reseed: () => room.reseed.task(),
|
|
248
250
|
retention: () => room.history.expireVersions(),
|
|
249
251
|
forward: () => room.moves.forwardMoves(),
|
|
250
252
|
expiry: () => noTimers(() => room.access.expireSockets())
|
|
@@ -258,6 +260,7 @@ export class AttachedDocument {
|
|
|
258
260
|
room.presence = new RoomPresence(room);
|
|
259
261
|
room.access = new RoomAccess(room);
|
|
260
262
|
room.comments = new RoomComments(room);
|
|
263
|
+
room.reseed = new RoomReseed(room);
|
|
261
264
|
// The provider pings a silent socket: answer without waking the object.
|
|
262
265
|
if (ctx.setWebSocketAutoResponse && !ctx.getWebSocketAutoResponse?.()) {
|
|
263
266
|
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(PING, PONG));
|
|
@@ -515,10 +518,24 @@ export class AttachedDocument {
|
|
|
515
518
|
refusals: { ...this.refusalCounts },
|
|
516
519
|
history: { ...counters.history },
|
|
517
520
|
purge: { ...counters.purge },
|
|
521
|
+
epoch: room.reseed.epoch,
|
|
518
522
|
since: counters.since
|
|
519
523
|
};
|
|
520
524
|
});
|
|
521
525
|
}
|
|
526
|
+
/** The room's epoch (`room.reseed.protocol`): how many times its document was re-seeded. */
|
|
527
|
+
get epoch() {
|
|
528
|
+
return this.room.reseed.epoch;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Re-seed the document now (`room.reseed.cut`), whatever it gives back
|
|
532
|
+
* and whether a suggestion is pending (they are dropped): every socket
|
|
533
|
+
* is closed `4410` and reloads it. Answers the new epoch, or `null`
|
|
534
|
+
* when re-seeding is off or the room is inside a transaction.
|
|
535
|
+
*/
|
|
536
|
+
reseed() {
|
|
537
|
+
return this.room.reseed.run('manual');
|
|
538
|
+
}
|
|
522
539
|
/**
|
|
523
540
|
* Drop every waiting delete, stored or in memory (callable over RPC):
|
|
524
541
|
* the way out when a writer filled `MAX_WAITING_DELETES` with
|
|
@@ -644,6 +661,7 @@ export class DocumentRoom extends DurableObject {
|
|
|
644
661
|
const semantics = () => this.semantics();
|
|
645
662
|
const history = () => this.history();
|
|
646
663
|
const locks = () => this.locks();
|
|
664
|
+
const reseeding = () => this.reseeding();
|
|
647
665
|
const comments = () => String(knobs.EDYTOR_COMMENTS ?? '') === 'off'
|
|
648
666
|
? false
|
|
649
667
|
: {
|
|
@@ -677,6 +695,9 @@ export class DocumentRoom extends DurableObject {
|
|
|
677
695
|
get locks() {
|
|
678
696
|
return locks();
|
|
679
697
|
},
|
|
698
|
+
get reseed() {
|
|
699
|
+
return reseeding();
|
|
700
|
+
},
|
|
680
701
|
get comments() {
|
|
681
702
|
return comments();
|
|
682
703
|
},
|
|
@@ -755,6 +776,13 @@ export class DocumentRoom extends DurableObject {
|
|
|
755
776
|
const key = this.env.EDYTOR_LOCKS;
|
|
756
777
|
return typeof key === 'string' && key !== '' ? { key } : undefined;
|
|
757
778
|
}
|
|
779
|
+
/**
|
|
780
|
+
* When the room re-seeds — see {@link AttachRoomOptions.reseed}. Default:
|
|
781
|
+
* on, with its default thresholds; `false` when `EDYTOR_RESEED` is `off`.
|
|
782
|
+
*/
|
|
783
|
+
reseeding() {
|
|
784
|
+
return String(this.env.EDYTOR_RESEED ?? '') === 'off' ? false : undefined;
|
|
785
|
+
}
|
|
758
786
|
/**
|
|
759
787
|
* The rooms blocks move to — see {@link AttachRoomOptions.rooms}.
|
|
760
788
|
* Default: the `EDYTOR_ROOMS` binding; none without it.
|
|
@@ -794,6 +822,14 @@ export class DocumentRoom extends DurableObject {
|
|
|
794
822
|
restoreHistory(key, options) {
|
|
795
823
|
return this.room.restoreHistory(key, options);
|
|
796
824
|
}
|
|
825
|
+
/** Re-seed the document now, also over RPC — see {@link AttachedDocument.reseed}. */
|
|
826
|
+
reseed() {
|
|
827
|
+
return this.room.reseed();
|
|
828
|
+
}
|
|
829
|
+
/** The room's epoch — see {@link AttachedDocument.epoch}. */
|
|
830
|
+
get epoch() {
|
|
831
|
+
return this.room.epoch;
|
|
832
|
+
}
|
|
797
833
|
/** Undo the last restore, also over RPC — see {@link AttachedDocument.undoRestore}. */
|
|
798
834
|
undoRestore(options) {
|
|
799
835
|
return this.room.undoRestore(options);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `cloudflare:workers`); built on the Worker-safe CRDT entry. Documented in
|
|
5
5
|
* the site's `server/` section (room, authorization, extending, protocol).
|
|
6
6
|
*/
|
|
7
|
-
export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, type AttachRoomOptions, type CommentOptions, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers, type Attachment, type DocumentRoomEnv, type LoadedDocument, type Refusal, type ReplicaOwner, type SavedDocument, type SocketIdentity, type StoredRecord, type FrameValidation, type ValidatedBlock, type RoomMetrics, type RoomLogEntry, type RestoreResult, type Timing } from './DocumentRoom.js';
|
|
7
|
+
export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, type AttachRoomOptions, type CommentOptions, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, DEFAULT_RESEED_FORCE, DEFAULT_RESEED_GAIN, DEFAULT_RESEED_GRACE_DAYS, DEFAULT_RESEED_IDLE, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers, type Attachment, type DocumentRoomEnv, type LoadedDocument, type Refusal, type ReplicaOwner, type SavedDocument, type SocketIdentity, type StoredRecord, type FrameValidation, type ValidatedBlock, type RoomMetrics, type RoomLogEntry, type ReseedOptions, type RestoreResult, type Timing } from './DocumentRoom.js';
|
|
8
8
|
export { DEFAULT_RETENTION_DAYS, HISTORY_MAX_VALUE_BYTES, R2_HISTORY_MAX_VALUE_BYTES, ROOM_HISTORY_MAX_BYTES, ROOM_HISTORY_MAX_VALUE_BYTES, kvHistory, r2History, roomHistory, type HistoryEntry, type HistoryMetadata, type HistoryOptions, type HistoryRoomStorage, type HistoryStore, type HistoryStoreFactory, type HistoryStoreRecord, type KVLike, type R2BucketLike } from './history.js';
|
|
9
9
|
export { type PurgeReport } from '../crdt/purge.js';
|
|
10
10
|
export { COMMENT_ROW_BYTES, COMMENT_STATUS, MAX_COMMENT_LENGTH, MAX_COMMENTS_PER_THREAD, MAX_THREADS, commentAnchors, commentBytes, decideComment, type ThreadComment, type CommentActor, type CommentChange, type CommentOutcome, type CommentRefusal, type CommentRequest, type CommentRun, type CommentSnapshot, type CommentThread } from '../crdt/protocols/comments.js';
|
package/dist/cloudflare/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `cloudflare:workers`); built on the Worker-safe CRDT entry. Documented in
|
|
5
5
|
* the site's `server/` section (room, authorization, extending, protocol).
|
|
6
6
|
*/
|
|
7
|
-
export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers } from './DocumentRoom.js';
|
|
7
|
+
export { DocumentRoom, AttachedDocument, attachRoom, SOCKET_TAG, closedSocket, DEFAULT_COMPACT_AFTER, DEFAULT_MAX_ROW_BYTES, DEFAULT_SAVE_AFTER, DEFAULT_MAX_DOCUMENT_BYTES, DEFAULT_DOCUMENT_WARNING, DEFAULT_MAX_INBOUND_FRAME_BYTES, DEFAULT_MAX_BUFFERED_BYTES, DEFAULT_MAX_UPDATES_PER_SECOND, DEFAULT_MAX_PRESENCE_BYTES, DEFAULT_MAX_PRESENCE_PER_SECOND, DEFAULT_MAX_PRESENCE_FANOUT, DEFAULT_MAX_COMMENT_BYTES, DEFAULT_MAX_COMMENT_REQUESTS_PER_SECOND, DEFAULT_RESEED_FORCE, DEFAULT_RESEED_GAIN, DEFAULT_RESEED_GRACE_DAYS, DEFAULT_RESEED_IDLE, MAX_REFUSALS, MAX_WAITING_DELETES, ROOM_ORIGIN, RESTORE_ORIGIN, PURGE_ORIGIN, IDENTITY_HEADERS, PROBE_HEADER, noTimers } from './DocumentRoom.js';
|
|
8
8
|
export { DEFAULT_RETENTION_DAYS, HISTORY_MAX_VALUE_BYTES, R2_HISTORY_MAX_VALUE_BYTES, ROOM_HISTORY_MAX_BYTES, ROOM_HISTORY_MAX_VALUE_BYTES, kvHistory, r2History, roomHistory } from './history.js';
|
|
9
9
|
export {} from '../crdt/purge.js';
|
|
10
10
|
export { COMMENT_ROW_BYTES, COMMENT_STATUS, MAX_COMMENT_LENGTH, MAX_COMMENTS_PER_THREAD, MAX_THREADS, commentAnchors, commentBytes, decideComment } from '../crdt/protocols/comments.js';
|
|
@@ -7,7 +7,12 @@ export declare class RoomAccess {
|
|
|
7
7
|
constructor(room: RoomContext);
|
|
8
8
|
/** A request forwarded by the host Worker (`AttachedDocument.fetch`). */
|
|
9
9
|
fetch(request: Request): Promise<Response>;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Accept a verified socket: its replica bound to its user, then the join
|
|
12
|
+
* (Step1, presence). A dial of another epoch than the room's is the
|
|
13
|
+
* re-seed's (`room.reseed.protocol`): closed, or joined as stale (no
|
|
14
|
+
* replica bound, no Step1: the room asks for its old document).
|
|
15
|
+
*/
|
|
11
16
|
private accept;
|
|
12
17
|
/** The open sockets of `user`. */
|
|
13
18
|
private socketsOf;
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* revocation, a changed access, an expired credential.
|
|
7
7
|
*/
|
|
8
8
|
import { CLOSE } from '../../crdt/providers/room.js';
|
|
9
|
-
import { COMMENTS_HEADER, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, PROBE_HEADER, SOCKET_TAG, closedSocket, noTimers } from './shared.js';
|
|
9
|
+
import { COMMENTS_HEADER, HISTORY_HEADER, HISTORY_KEY_HEADER, IDENTITY_HEADERS, PROBE_HEADER, SOCKET_TAG, STALE_TAG, closedSocket, noTimers } from './shared.js';
|
|
10
10
|
import { STORAGE_FAILURE } from './context.js';
|
|
11
11
|
import { accessFrame, readOnlyFrame, step1Frame } from './frames.js';
|
|
12
12
|
import { StorageFault } from './replicas.js';
|
|
13
|
+
import { parseEpoch } from './reseed.js';
|
|
13
14
|
import { stateVector } from './updates.js';
|
|
14
15
|
/** The longest close reason the WebSocket API accepts, in UTF-8 bytes. */
|
|
15
16
|
const MAX_CLOSE_REASON_BYTES = 123;
|
|
@@ -107,13 +108,38 @@ export class RoomAccess {
|
|
|
107
108
|
if (identity === null)
|
|
108
109
|
return new Response('verified identity required', { status: 401 });
|
|
109
110
|
await room.storage.retryStart();
|
|
110
|
-
|
|
111
|
+
const epoch = parseEpoch(new URL(request.url).searchParams.get('epoch'));
|
|
112
|
+
return noTimers(() => this.accept(identity, epoch));
|
|
111
113
|
}
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Accept a verified socket: its replica bound to its user, then the join
|
|
116
|
+
* (Step1, presence). A dial of another epoch than the room's is the
|
|
117
|
+
* re-seed's (`room.reseed.protocol`): closed, or joined as stale (no
|
|
118
|
+
* replica bound, no Step1: the room asks for its old document).
|
|
119
|
+
*/
|
|
120
|
+
accept(identity, epoch) {
|
|
114
121
|
const room = this.room;
|
|
115
122
|
room.storage.heal();
|
|
116
123
|
const doc = room.live;
|
|
124
|
+
const door = doc === null ? 'current' : room.reseed.door(epoch);
|
|
125
|
+
if (typeof door === 'object') {
|
|
126
|
+
room.note({
|
|
127
|
+
reason: 'epoch',
|
|
128
|
+
detail: { user: identity.user, epoch, room: room.reseed.epoch }
|
|
129
|
+
});
|
|
130
|
+
return closedSocket(door.code, door.reason);
|
|
131
|
+
}
|
|
132
|
+
if (door === 'stale') {
|
|
133
|
+
const pair = new WebSocketPair();
|
|
134
|
+
room.ctx.acceptWebSocket(pair[1], [SOCKET_TAG, STALE_TAG]);
|
|
135
|
+
pair[1].serializeAttachment({
|
|
136
|
+
...identity,
|
|
137
|
+
clock: null,
|
|
138
|
+
epoch: epoch
|
|
139
|
+
});
|
|
140
|
+
room.reseed.join(pair[1], identity);
|
|
141
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
142
|
+
}
|
|
117
143
|
if (doc !== null && identity.replica !== null) {
|
|
118
144
|
let bound;
|
|
119
145
|
try {
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import type { YDoc } from '../../crdt/index.js';
|
|
2
|
+
import type { Attachment } from '../DocumentRoom.js';
|
|
1
3
|
import { type RoomContext } from './context.js';
|
|
2
4
|
/** The provider's keepalive text frame, and the room's answer. */
|
|
3
5
|
export declare const PING = "ping";
|
|
4
6
|
export declare const PONG = "pong";
|
|
7
|
+
/** Seconds of the update rate a socket may spend at once. */
|
|
8
|
+
export declare const BURST_SECONDS = 10;
|
|
5
9
|
/** A token bucket: its tokens, refilled since `at` (ms). */
|
|
6
10
|
export type Bucket = {
|
|
7
11
|
tokens: number;
|
|
@@ -40,6 +44,13 @@ export declare class Admission {
|
|
|
40
44
|
sequences: number;
|
|
41
45
|
bytes: number;
|
|
42
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* A stale socket's rebase (`room.reseed.rebase`): `update`, the writes
|
|
49
|
+
* of a copy of the document under a client id of their own, admitted as
|
|
50
|
+
* its user's update, from that id. `true` once applied; refused (a
|
|
51
|
+
* quota, a read-only socket, a fault), `false`.
|
|
52
|
+
*/
|
|
53
|
+
admitRebase(ws: WebSocket, attachment: Attachment, doc: YDoc, update: Uint8Array, writer: number): boolean;
|
|
43
54
|
/**
|
|
44
55
|
* The update rate quota: a token bucket per socket, refilled at
|
|
45
56
|
* `maxUpdatesPerSecond` up to ten seconds' worth. `false`: over it.
|
|
@@ -31,7 +31,7 @@ const writes = (doc, decoded) => newWriters(decoded, stateVector(doc)).size > 0
|
|
|
31
31
|
export const PING = 'ping';
|
|
32
32
|
export const PONG = 'pong';
|
|
33
33
|
/** Seconds of the update rate a socket may spend at once. */
|
|
34
|
-
const BURST_SECONDS = 10;
|
|
34
|
+
export const BURST_SECONDS = 10;
|
|
35
35
|
/**
|
|
36
36
|
* A token bucket per key (a socket, a user), refilled at `perSecond` up to
|
|
37
37
|
* ten seconds' worth: takes one token. `false`: over the rate.
|
|
@@ -106,10 +106,10 @@ export class Admission {
|
|
|
106
106
|
/** One frame: whole as it came, or reassembled from its chunks. */
|
|
107
107
|
onFrame(ws, attachment, doc, bytes) {
|
|
108
108
|
const room = this.room;
|
|
109
|
-
const limit =
|
|
109
|
+
const limit = this.#frameLimit(attachment);
|
|
110
110
|
// The frame quota: never decoded past it.
|
|
111
111
|
if (bytes.length > limit) {
|
|
112
|
-
return this.#overQuota(ws, attachment
|
|
112
|
+
return this.#overQuota(ws, attachment, 'frame', { bytes: bytes.length, limit });
|
|
113
113
|
}
|
|
114
114
|
const decoder = E.createDecoder(bytes);
|
|
115
115
|
// 1 · Admission: the generation word, before anything is decoded.
|
|
@@ -120,6 +120,9 @@ export class Admission {
|
|
|
120
120
|
const type = decode(() => E.readVarUint(decoder));
|
|
121
121
|
if (type === E.messageChunk)
|
|
122
122
|
return this.#onChunk(ws, attachment, doc, decoder);
|
|
123
|
+
// A socket of an older epoch hands over its old document, nothing else (`room.reseed.protocol`).
|
|
124
|
+
if (attachment.epoch != null)
|
|
125
|
+
return room.reseed.onStale(ws, attachment, type, decoder);
|
|
123
126
|
if (type === E.messageSync)
|
|
124
127
|
return this.onSync(ws, attachment, doc, decoder);
|
|
125
128
|
if (type === E.messageAwareness) {
|
|
@@ -160,7 +163,7 @@ export class Admission {
|
|
|
160
163
|
const room = this.room;
|
|
161
164
|
let read = this.#chunkReaders.get(ws);
|
|
162
165
|
if (read === undefined) {
|
|
163
|
-
read = E.createChunkReader(
|
|
166
|
+
read = E.createChunkReader(this.#frameLimit(attachment), (total) => this.admitChunks(ws, total));
|
|
164
167
|
this.#chunkReaders.set(ws, read);
|
|
165
168
|
}
|
|
166
169
|
let whole;
|
|
@@ -170,14 +173,14 @@ export class Admission {
|
|
|
170
173
|
catch (error) {
|
|
171
174
|
this.#chunkReaders.delete(ws);
|
|
172
175
|
if (error instanceof ChunkError && error.limit !== undefined) {
|
|
173
|
-
return this.#overQuota(ws, attachment
|
|
176
|
+
return this.#overQuota(ws, attachment, 'frame', {
|
|
174
177
|
bytes: error.total,
|
|
175
178
|
limit: error.limit
|
|
176
179
|
});
|
|
177
180
|
}
|
|
178
181
|
if (error instanceof ChunksRefused) {
|
|
179
182
|
if (error.quota === 'rate') {
|
|
180
|
-
return this.#overQuota(ws, attachment
|
|
183
|
+
return this.#overQuota(ws, attachment, 'rate', error.detail);
|
|
181
184
|
}
|
|
182
185
|
// The room's buffer is full, not the sender at fault: it redials.
|
|
183
186
|
room.note({
|
|
@@ -243,16 +246,39 @@ export class Admission {
|
|
|
243
246
|
* reports (`onSyncRefused`) and does not redial. Never a dropped frame
|
|
244
247
|
* on a live socket: the sender's later frames would build on it and
|
|
245
248
|
* wait in the room's memory for good, unstored and unacknowledged; and
|
|
246
|
-
* a redial would resend it and meet the same quota.
|
|
249
|
+
* a redial would resend it and meet the same quota. A socket of an older
|
|
250
|
+
* epoch whose hand-over is past the frame quota is closed `4410`: the
|
|
251
|
+
* room cannot read it, its client loads the room's document.
|
|
247
252
|
*/
|
|
248
|
-
#overQuota(ws, user, quota, detail) {
|
|
253
|
+
#overQuota(ws, { user, epoch }, quota, detail) {
|
|
249
254
|
const room = this.room;
|
|
250
255
|
room.note({ reason: 'quota', detail: { user, quota, ...detail } });
|
|
251
256
|
room.log({ edytor: 'quota', user, quota });
|
|
252
257
|
this.#chunkReaders.delete(ws);
|
|
253
258
|
room.presence.depart(ws);
|
|
259
|
+
if (epoch != null && quota === 'frame')
|
|
260
|
+
return room.close(ws, CLOSE.reseeded, String(room.reseed.epoch));
|
|
254
261
|
room.close(ws, CLOSE.quota, `quota: ${quota}`);
|
|
255
262
|
}
|
|
263
|
+
/** The frame quota of `attachment`'s socket: a stale one's hand-over is its whole old document. */
|
|
264
|
+
#frameLimit(attachment) {
|
|
265
|
+
const room = this.room;
|
|
266
|
+
return attachment.epoch != null
|
|
267
|
+
? room.reseed.staleFrameBytes
|
|
268
|
+
: room.limits.maxInboundFrameBytes;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A stale socket's rebase (`room.reseed.rebase`): `update`, the writes
|
|
272
|
+
* of a copy of the document under a client id of their own, admitted as
|
|
273
|
+
* its user's update, from that id. `true` once applied; refused (a
|
|
274
|
+
* quota, a read-only socket, a fault), `false`.
|
|
275
|
+
*/
|
|
276
|
+
admitRebase(ws, attachment, doc, update, writer) {
|
|
277
|
+
if (attachment.readOnly)
|
|
278
|
+
return false;
|
|
279
|
+
this.onUpdate(ws, { ...attachment, replica: writer }, doc, update);
|
|
280
|
+
return ws.readyState === WebSocket.OPEN;
|
|
281
|
+
}
|
|
256
282
|
/**
|
|
257
283
|
* The update rate quota: a token bucket per socket, refilled at
|
|
258
284
|
* `maxUpdatesPerSecond` up to ten seconds' worth. `false`: over it.
|
|
@@ -296,7 +322,7 @@ export class Admission {
|
|
|
296
322
|
const syncType = decode(() => E.readVarUint(decoder));
|
|
297
323
|
// The rate quota counts every sync message, a Step1 too (it costs a Step2).
|
|
298
324
|
if (!this.allow(ws)) {
|
|
299
|
-
return this.#overQuota(ws, attachment
|
|
325
|
+
return this.#overQuota(ws, attachment, 'rate', {
|
|
300
326
|
perSecond: room.limits.maxUpdatesPerSecond
|
|
301
327
|
});
|
|
302
328
|
}
|
|
@@ -335,6 +361,13 @@ export class Admission {
|
|
|
335
361
|
// 3 · Attribution: new structs only under client ids this user may
|
|
336
362
|
// write under; another user's are stripped, the rest applied.
|
|
337
363
|
const decoded = decode(() => Y.decodeUpdate(update));
|
|
364
|
+
// An older epoch's content (`room.reseed.protocol`): the socket's
|
|
365
|
+
// document took it from another tab; it loads the room's again.
|
|
366
|
+
if (room.reseed.holdsOld(decoded, doc)) {
|
|
367
|
+
room.note({ reason: 'epoch', detail: { user: attachment.user, epoch: room.reseed.epoch } });
|
|
368
|
+
room.presence.depart(ws);
|
|
369
|
+
return room.close(ws, CLOSE.reseeded, String(room.reseed.epoch));
|
|
370
|
+
}
|
|
338
371
|
// A storage outage (`room.storage.outage`): a frame that would write
|
|
339
372
|
// waits for storage to answer, unapplied, so the outage rebuilds once.
|
|
340
373
|
if (storage.outage !== null && writes(doc, decoded) && !storage.answers()) {
|
|
@@ -343,7 +376,7 @@ export class Admission {
|
|
|
343
376
|
// The document quota, net of what the frame deletes.
|
|
344
377
|
if (decoded.structs.some((struct) => !(struct instanceof Y.Skip)) &&
|
|
345
378
|
this.#overDocument(update.length, freedBy(doc, decoded.ds))) {
|
|
346
|
-
return this.#overQuota(ws, attachment
|
|
379
|
+
return this.#overQuota(ws, attachment, 'document', {
|
|
347
380
|
bytes: storage.documentBytes,
|
|
348
381
|
incoming: update.length,
|
|
349
382
|
limit: room.limits.maxDocumentBytes
|