@loro-dev/streams-crdt 0.6.1 → 0.7.1

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.
@@ -213,7 +213,21 @@ type TransportError = {
213
213
  readonly code: "unknown";
214
214
  readonly retryable: false;
215
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;
216
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
+ }
217
231
  /**
218
232
  * Result payload returned by `createStream()`.
219
233
  */
@@ -232,6 +246,13 @@ interface TransportDeleteStreamSuccess {
232
246
  interface TransportSyncSuccess<TVersion extends JsonObject = JsonObject> {
233
247
  readonly cursor: RemoteCursor<TVersion>;
234
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
+ }
235
256
  /**
236
257
  * Optional parameters for a `join()` call.
237
258
  *
@@ -282,6 +303,54 @@ interface SnapshotCodec {
282
303
  readonly compress: SnapshotTransformHook;
283
304
  readonly decompress: SnapshotTransformHook;
284
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;
285
354
  /**
286
355
  * Active live subscription returned by `join()`.
287
356
  *
@@ -295,6 +364,14 @@ interface TransportSubscription {
295
364
  readonly connected: boolean;
296
365
  /** Current room status. */
297
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;
298
375
  /** Registers a status listener. Returns an unsubscribe function. */
299
376
  readonly onStatusChange: (listener: (status: "connecting" | TransportRoomStatus) => void) => () => void;
300
377
  /**
@@ -309,6 +386,9 @@ interface TransportSubscription {
309
386
  *
310
387
  * The promise rejects if the flush encounters a non-recoverable error or
311
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.
312
392
  */
313
393
  readonly waitUntilSynced: () => Promise<void>;
314
394
  }
@@ -437,6 +517,16 @@ interface StreamsCrdtOptions<TVersion extends JsonObject> {
437
517
  * the server.
438
518
  */
439
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;
440
530
  readonly snapshotUpload?: SnapshotUploadOptions;
441
531
  readonly reconnectConfig?: Partial<ReconnectConfig>;
442
532
  readonly debug?: boolean;
@@ -445,9 +535,25 @@ interface StreamsCrdtLike<TVersion extends JsonObject> {
445
535
  createStream(): Promise<Result<TransportCreateStreamSuccess, TransportError>>;
446
536
  deleteStream(): Promise<Result<TransportDeleteStreamSuccess, TransportError>>;
447
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>>;
448
547
  join(params?: TransportJoinParams): Promise<Result<TransportSubscription, TransportError>>;
449
548
  /** Resets retry state and immediately attempts to reconnect all rooms. */
450
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
+ */
451
557
  close(): Promise<void>;
452
558
  getSnapshotOffset(): Promise<Result<string | null, TransportError>>;
453
559
  readonly streamUrl: string;
@@ -468,6 +574,10 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
468
574
  private readonly cursorManager;
469
575
  private readonly volatileCursorManager;
470
576
  private readonly decodeSnapshot?;
577
+ private readonly encodeSnapshot?;
578
+ private readonly decodeUpdateItems?;
579
+ private readonly encodeUpdateItems?;
580
+ private readonly measureUpdateItemsByteLength?;
471
581
  private readonly reconnectConfig;
472
582
  private readonly debug;
473
583
  private readonly beforeRemoteCursorSave?;
@@ -475,6 +585,10 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
475
585
  private readonly producerId;
476
586
  private producerEpoch;
477
587
  private nextProducerSeq;
588
+ private readonly pendingWriteOnly;
589
+ private writeOnlyCursor;
590
+ private unsubscribeWriteOnlyLocal?;
591
+ private writeOnlyDisabledReason?;
478
592
  private joinState?;
479
593
  private opQueue;
480
594
  /**
@@ -486,6 +600,8 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
486
600
  * Creates a transport for one stream URL and one local adapter instance.
487
601
  */
488
602
  constructor(options: StreamsCrdtOptions<TVersion>);
603
+ private composeSnapshotDecoder;
604
+ private composeSnapshotEncoder;
489
605
  /**
490
606
  * Stops any active live join and releases transport-owned state.
491
607
  */
@@ -505,6 +621,13 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
505
621
  * Reads the latest remote snapshot offset, if the server exposes one.
506
622
  */
507
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>>;
508
631
  /**
509
632
  * Explicitly creates the target stream.
510
633
  */
@@ -517,6 +640,14 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
517
640
  * Runs one bootstrap/catch-up cycle without entering live mode.
518
641
  */
519
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>>;
520
651
  /**
521
652
  * Replays remote state, flushes queued local updates, then enters live read mode.
522
653
  */
@@ -558,7 +689,9 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
558
689
  *
559
690
  * Returns `undefined` when the queue is empty.
560
691
  */
692
+ private drainQueuedBatch;
561
693
  private drainPendingBatch;
694
+ private drainPendingWriteOnlyBatch;
562
695
  private flushPendingLocal;
563
696
  /**
564
697
  * Decrements all sync waiters by the number of flushed batches and
@@ -573,10 +706,23 @@ declare class StreamsCrdt<TVersion extends JsonObject> implements StreamsCrdtLik
573
706
  private recomputeJoinStatus;
574
707
  private computeOverallStatus;
575
708
  private enqueueExclusive;
709
+ private measureAppendBodyByteLength;
710
+ private disableWriteOnlyMode;
576
711
  private logDebug;
577
712
  private logError;
578
713
  }
579
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
580
726
  //#region src/stream-id.d.ts
581
727
  /**
582
728
  * Validates a stream id accepted by `createStreamUrl()`.
@@ -595,5 +741,5 @@ declare function createStreamUrl(input: {
595
741
  baseUrl?: string;
596
742
  }): string;
597
743
  //#endregion
598
- 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 };
599
- //# sourceMappingURL=stream-id-Dr8ZBpUQ.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