@loro-dev/streams-crdt 0.6.0 → 0.7.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.
@@ -109,6 +109,8 @@ interface ReconnectConfig {
109
109
  readonly maxAttempts: number;
110
110
  readonly connectTimeoutMs: number;
111
111
  readonly pollTimeoutMs: number;
112
+ /** SSE inactivity timeout. Aborts and reconnects if no event arrives within this period. */
113
+ readonly liveInactivityTimeoutMs?: number;
112
114
  }
113
115
  //#endregion
114
116
  //#region src/types.d.ts
@@ -211,7 +213,21 @@ type TransportError = {
211
213
  readonly code: "unknown";
212
214
  readonly retryable: false;
213
215
  readonly message: string;
216
+ } | {
217
+ readonly code: "payload_protection_error";
218
+ readonly retryable: false;
219
+ readonly reason: "plaintext_forbidden" | "missing_read_key" | "decrypt_failed" | "invalid_envelope" | "wrong_payload_kind" | "encrypt_failed";
220
+ readonly keyId?: string;
221
+ readonly message: string;
214
222
  };
223
+ interface WriteOnlyAppendResult<TVersion extends JsonObject> {
224
+ /** Whether this call found and appended local CRDT updates. */
225
+ readonly appended: boolean;
226
+ /** Server tail offset returned by the append request. Omitted when `appended` is false. */
227
+ readonly nextOffset?: string;
228
+ /** Local CRDT version exported by this runtime's append-only path. */
229
+ readonly localVersion: TVersion;
230
+ }
215
231
  /**
216
232
  * Result payload returned by `createStream()`.
217
233
  */
@@ -230,6 +246,13 @@ interface TransportDeleteStreamSuccess {
230
246
  interface TransportSyncSuccess<TVersion extends JsonObject = JsonObject> {
231
247
  readonly cursor: RemoteCursor<TVersion>;
232
248
  }
249
+ /**
250
+ * Result payload returned by the internal snapshot upload test hook.
251
+ */
252
+ interface TransportSnapshotUploadSuccess {
253
+ readonly snapshotOffset: string;
254
+ readonly snapshotByteLength: number;
255
+ }
233
256
  /**
234
257
  * Optional parameters for a `join()` call.
235
258
  *
@@ -280,6 +303,54 @@ interface SnapshotCodec {
280
303
  readonly compress: SnapshotTransformHook;
281
304
  readonly decompress: SnapshotTransformHook;
282
305
  }
306
+ type PayloadProtectionReadPolicy = "encrypted-only" | "allow-plaintext";
307
+ type PayloadProtectionWritePolicy = "encrypt" | "plaintext";
308
+ type PayloadProtectionScope = string | {
309
+ readonly bucketId: string;
310
+ readonly streamId: string;
311
+ };
312
+ interface PayloadProtectionKey {
313
+ readonly id: string;
314
+ readonly key: CryptoKey | Uint8Array;
315
+ }
316
+ type PayloadProtectionKeyProvider = () => MaybePromise<readonly PayloadProtectionKey[]>;
317
+ interface PayloadProtectionEncryptionOptions {
318
+ /**
319
+ * Stable encryption context authenticated with every encrypted payload.
320
+ *
321
+ * Prefer bucket/stream identity or an application-stable string. Do not use
322
+ * a full gateway URL unless old encrypted bytes should become unreadable
323
+ * after host/proxy migration.
324
+ */
325
+ readonly scope: PayloadProtectionScope;
326
+ /** Key used for new encrypted writes. Required when writePolicy is "encrypt". */
327
+ readonly writeKey?: PayloadProtectionKey;
328
+ /**
329
+ * Keys accepted for remote encrypted reads.
330
+ *
331
+ * When omitted, `writeKey` is also used as the only read key.
332
+ */
333
+ readonly readKeys?: readonly PayloadProtectionKey[] | PayloadProtectionKeyProvider;
334
+ }
335
+ interface PayloadProtectionOptions {
336
+ /**
337
+ * Remote plaintext policy. Defaults to "encrypted-only" when
338
+ * E2EE is provided.
339
+ */
340
+ readonly readPolicy?: PayloadProtectionReadPolicy;
341
+ /**
342
+ * Local write policy. Defaults to "encrypt" when E2EE is provided.
343
+ */
344
+ readonly writePolicy?: PayloadProtectionWritePolicy;
345
+ readonly encryption?: PayloadProtectionEncryptionOptions;
346
+ }
347
+ type E2eeReadPolicy = PayloadProtectionReadPolicy;
348
+ type E2eeWritePolicy = PayloadProtectionWritePolicy;
349
+ type E2eeScope = PayloadProtectionScope;
350
+ type E2eeKey = PayloadProtectionKey;
351
+ type E2eeKeyProvider = PayloadProtectionKeyProvider;
352
+ type E2eeEncryptionOptions = PayloadProtectionEncryptionOptions;
353
+ type E2eeOptions = PayloadProtectionOptions;
283
354
  /**
284
355
  * Active live subscription returned by `join()`.
285
356
  *
@@ -293,6 +364,14 @@ interface TransportSubscription {
293
364
  readonly connected: boolean;
294
365
  /** Current room status. */
295
366
  readonly status: TransportRoomStatus;
367
+ /**
368
+ * The last transport error observed, if any.
369
+ *
370
+ * Updated when the room transitions to `"error"` or `"disconnected"`.
371
+ * Cleared on successful recovery. Callers can inspect this to decide
372
+ * whether to retry, surface auth failure, or mark the room as terminal.
373
+ */
374
+ readonly lastError?: TransportError;
296
375
  /** Registers a status listener. Returns an unsubscribe function. */
297
376
  readonly onStatusChange: (listener: (status: "connecting" | TransportRoomStatus) => void) => () => void;
298
377
  /**
@@ -307,6 +386,9 @@ interface TransportSubscription {
307
386
  *
308
387
  * The promise rejects if the flush encounters a non-recoverable error or
309
388
  * all subscriptions are closed before all pending changes are confirmed.
389
+ *
390
+ * If the initial join sync has failed and not yet recovered, the promise
391
+ * rejects immediately with a structured error indicating the failure.
310
392
  */
311
393
  readonly waitUntilSynced: () => Promise<void>;
312
394
  }
@@ -435,6 +517,16 @@ interface StreamsCrdtOptions<TVersion extends JsonObject> {
435
517
  * the server.
436
518
  */
437
519
  readonly snapshotCodec?: SnapshotCodec;
520
+ /**
521
+ * E2EE and mixed encrypted/plaintext payload behavior.
522
+ */
523
+ readonly e2ee?: E2eeOptions;
524
+ /**
525
+ * Deprecated alias for `e2ee`.
526
+ *
527
+ * Prefer `e2ee`. Passing both `e2ee` and `payloadProtection` is an error.
528
+ */
529
+ readonly payloadProtection?: PayloadProtectionOptions;
438
530
  readonly snapshotUpload?: SnapshotUploadOptions;
439
531
  readonly reconnectConfig?: Partial<ReconnectConfig>;
440
532
  readonly debug?: boolean;
@@ -443,9 +535,25 @@ interface StreamsCrdtLike<TVersion extends JsonObject> {
443
535
  createStream(): Promise<Result<TransportCreateStreamSuccess, TransportError>>;
444
536
  deleteStream(): Promise<Result<TransportDeleteStreamSuccess, TransportError>>;
445
537
  sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
538
+ /**
539
+ * Appends only locally observed batches without reading remote state first.
540
+ *
541
+ * This is the write-only path for actors that may POST but may not bootstrap,
542
+ * catch up, or join live reads. It must not be mixed with `sync()`/`join()`
543
+ * on the same instance, and `close()` permanently disables it for that
544
+ * instance.
545
+ */
546
+ appendWriteOnly(): Promise<Result<WriteOnlyAppendResult<TVersion>, TransportError>>;
446
547
  join(params?: TransportJoinParams): Promise<Result<TransportSubscription, TransportError>>;
447
548
  /** Resets retry state and immediately attempts to reconnect all rooms. */
448
549
  rejoin(): void;
550
+ /**
551
+ * Closes the active join and releases transport-owned resources.
552
+ *
553
+ * If this instance was using `appendWriteOnly()`, closing also tears down the
554
+ * write-only subscription. Create a new instance to use the write-only path
555
+ * again later.
556
+ */
449
557
  close(): Promise<void>;
450
558
  getSnapshotOffset(): Promise<Result<string | null, TransportError>>;
451
559
  readonly streamUrl: string;
@@ -466,6 +574,10 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
466
574
  private readonly cursorManager;
467
575
  private readonly volatileCursorManager;
468
576
  private readonly decodeSnapshot?;
577
+ private readonly encodeSnapshot?;
578
+ private readonly decodeUpdateItems?;
579
+ private readonly encodeUpdateItems?;
580
+ private readonly measureUpdateItemsByteLength?;
469
581
  private readonly reconnectConfig;
470
582
  private readonly debug;
471
583
  private readonly beforeRemoteCursorSave?;
@@ -473,6 +585,10 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
473
585
  private readonly producerId;
474
586
  private producerEpoch;
475
587
  private nextProducerSeq;
588
+ private readonly pendingWriteOnly;
589
+ private writeOnlyCursor;
590
+ private unsubscribeWriteOnlyLocal?;
591
+ private writeOnlyDisabledReason?;
476
592
  private joinState?;
477
593
  private opQueue;
478
594
  /**
@@ -484,6 +600,8 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
484
600
  * Creates a transport for one stream URL and one local adapter instance.
485
601
  */
486
602
  constructor(options: StreamsCrdtOptions<TVersion>);
603
+ private composeSnapshotDecoder;
604
+ private composeSnapshotEncoder;
487
605
  /**
488
606
  * Stops any active live join and releases transport-owned state.
489
607
  */
@@ -492,16 +610,24 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
492
610
  * Resets the retry counter and immediately attempts to reconnect.
493
611
  *
494
612
  * Use this after device wake or network recovery to skip any pending
495
- * backoff delay. No-op when the room is already joined or no join is active.
613
+ * backoff delay. No-op when the room is already fully healthy (both
614
+ * read and write sub-statuses are "ok") or no join is active.
496
615
  *
497
- * If the room reached `"disconnected"` status (only possible with a finite
498
- * `maxAttempts` config), this restarts the read loop.
616
+ * If the room reached `"disconnected"` or `"error"` status, this
617
+ * restarts the affected loop(s).
499
618
  */
500
619
  rejoin(): void;
501
620
  /**
502
621
  * Reads the latest remote snapshot offset, if the server exposes one.
503
622
  */
504
623
  getSnapshotOffset(): Promise<Result<string | null, TransportError>>;
624
+ /**
625
+ * Uploads a full adapter snapshot at the current remote tail.
626
+ *
627
+ * @internal Intended for end-to-end tests that must deterministically create
628
+ * a remote snapshot without waiting for the best-effort join() debounce.
629
+ */
630
+ uploadSnapshotForTesting(): Promise<Result<TransportSnapshotUploadSuccess, TransportError>>;
505
631
  /**
506
632
  * Explicitly creates the target stream.
507
633
  */
@@ -514,6 +640,14 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
514
640
  * Runs one bootstrap/catch-up cycle without entering live mode.
515
641
  */
516
642
  sync(): Promise<Result<TransportSyncSuccess<TVersion>, TransportError>>;
643
+ /**
644
+ * Appends local CRDT updates without reading the stream first.
645
+ *
646
+ * Use this for write-only actors whose auth token may POST to the stream but
647
+ * may not bootstrap, catch up, or join live reads. The method does not save a
648
+ * RemoteCursorStore cursor and does not apply remote updates.
649
+ */
650
+ appendWriteOnly(): Promise<Result<WriteOnlyAppendResult<TVersion>, TransportError>>;
517
651
  /**
518
652
  * Replays remote state, flushes queued local updates, then enters live read mode.
519
653
  */
@@ -529,6 +663,11 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
529
663
  private catchup;
530
664
  private catchupOrGone;
531
665
  private appendLocalBatch;
666
+ /**
667
+ * Ensures the target stream exists, creating it if necessary.
668
+ * Tolerates 409 Conflict (stream already created concurrently).
669
+ */
670
+ private ensureStreamExists;
532
671
  private exportUpdates;
533
672
  private applyBootstrapItems;
534
673
  private applyRemoteItems;
@@ -550,7 +689,9 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
550
689
  *
551
690
  * Returns `undefined` when the queue is empty.
552
691
  */
692
+ private drainQueuedBatch;
553
693
  private drainPendingBatch;
694
+ private drainPendingWriteOnlyBatch;
554
695
  private flushPendingLocal;
555
696
  /**
556
697
  * Decrements all sync waiters by the number of flushed batches and
@@ -560,12 +701,28 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
560
701
  /** Rejects and removes all sync waiters. */
561
702
  private rejectSyncWaiters;
562
703
  private saveRemoteCursor;
563
- private setJoinStatus;
704
+ private setReadSubStatus;
705
+ private setWriteSubStatus;
706
+ private recomputeJoinStatus;
707
+ private computeOverallStatus;
564
708
  private enqueueExclusive;
709
+ private measureAppendBodyByteLength;
710
+ private disableWriteOnlyMode;
565
711
  private logDebug;
566
712
  private logError;
567
713
  }
568
714
  //#endregion
715
+ //#region src/payload-protection.d.ts
716
+ type PayloadProtectionFailureReason = "plaintext_forbidden" | "missing_read_key" | "decrypt_failed" | "invalid_envelope" | "wrong_payload_kind" | "encrypt_failed";
717
+ declare class PayloadProtectionError extends Error {
718
+ readonly reason: PayloadProtectionFailureReason;
719
+ readonly keyId?: string;
720
+ constructor(reason: PayloadProtectionFailureReason, message: string, options?: {
721
+ readonly keyId?: string;
722
+ readonly cause?: unknown;
723
+ });
724
+ }
725
+ //#endregion
569
726
  //#region src/stream-id.d.ts
570
727
  /**
571
728
  * Validates a stream id accepted by `createStreamUrl()`.
@@ -584,5 +741,5 @@ declare function createStreamUrl(input: {
584
741
  baseUrl?: string;
585
742
  }): string;
586
743
  //#endregion
587
- export { RemoteCursorSaveSource as A, TransportSyncSuccess as C, IndexedDbRemoteCursorStore as D, InMemoryRemoteCursorStore as E, createInitialRemoteCursor as M, IndexedDbRemoteCursorStoreOptions as O, TransportSubscription as S, BeforeRemoteCursorSaveHook as T, TransportCreateStreamSuccess as _, CrdtAdapter as a, TransportJoinParams as b, JsonObject as c, SnapshotCodec as d, SnapshotTransformHook as f, StreamsCrdtOptions as g, StreamsAuthProvider as h, StreamsCrdt as i, RemoteCursorStore as j, RemoteCursor as k, JsonValue as l, StreamsAuthContext as m, isValidBucketId as n, CrdtUpdateBatch as o, SnapshotUploadOptions as p, isValidRillId as r, IsolatedCrdtAdapter as s, createStreamUrl as t, Result as u, TransportDeleteStreamSuccess as v, BeforeRemoteCursorSaveContext as w, TransportRoomStatus as x, TransportError as y };
588
- //# sourceMappingURL=stream-id-Do4h2lOD.d.ts.map
744
+ export { StreamsAuthProvider as A, WriteOnlyAppendResult as B, PayloadProtectionScope as C, SnapshotTransformHook as D, SnapshotCodec as E, TransportJoinParams as F, IndexedDbRemoteCursorStoreOptions as G, BeforeRemoteCursorSaveHook as H, TransportRoomStatus as I, RemoteCursorStore as J, RemoteCursor as K, TransportSnapshotUploadSuccess as L, TransportCreateStreamSuccess as M, TransportDeleteStreamSuccess as N, SnapshotUploadOptions as O, TransportError as P, TransportSubscription as R, PayloadProtectionReadPolicy as S, Result as T, InMemoryRemoteCursorStore as U, BeforeRemoteCursorSaveContext as V, IndexedDbRemoteCursorStore as W, createInitialRemoteCursor as Y, JsonValue as _, StreamsCrdt as a, PayloadProtectionKeyProvider as b, E2eeEncryptionOptions as c, E2eeOptions as d, E2eeReadPolicy as f, JsonObject as g, IsolatedCrdtAdapter as h, PayloadProtectionError as i, StreamsCrdtOptions as j, StreamsAuthContext as k, E2eeKey as l, E2eeWritePolicy as m, isValidBucketId as n, CrdtAdapter as o, E2eeScope as p, RemoteCursorSaveSource as q, isValidRillId as r, CrdtUpdateBatch as s, createStreamUrl as t, E2eeKeyProvider as u, PayloadProtectionEncryptionOptions as v, PayloadProtectionWritePolicy as w, PayloadProtectionOptions as x, PayloadProtectionKey as y, TransportSyncSuccess as z };
745
+ //# sourceMappingURL=stream-id-CS7SzuLW.d.ts.map