@loro-dev/streams-crdt 0.15.0 → 0.15.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.
package/dist/flock.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CO65-58E.js";
1
+ import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CVCOLgSp.js";
2
2
  import { ExportBundle, Flock, VersionVector, VersionVector as VersionVector$1 } from "@loro-dev/flock-wasm";
3
3
 
4
4
  //#region src/flock.d.ts
package/dist/flock.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-CggbAC6b.js";
1
+ import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-BygPTNvQ.js";
2
2
  import { Flock } from "@loro-dev/flock-wasm";
3
3
  //#region src/flock.ts
4
4
  function isAfter(entry, current) {
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CO65-58E.js";
1
+ import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CVCOLgSp.js";
2
2
  export { BeforeRemoteCursorSaveContext, BeforeRemoteCursorSaveHook, CrdtAdapter, CrdtApplyOutcome, CrdtApplyReturn, CrdtUnresolvedSpan, CrdtUpdateBatch, PayloadProtectionError as E2eeError, PayloadProtectionError, E2eeOptions, E2eeProvider, E2eeProviderConfig, E2eeReadPolicy, E2eeWritePolicy, EphemeralStreamAdaptor, EphemeralStreamCrdt, EphemeralStreamCrdtOptions, EphemeralStreamJoinParams, EphemeralStreamSubscription, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, IndexedDbRemoteCursorStoreOptions, IsolatedCrdtAdapter, JsonObject, JsonValue, LocalAppendFailedError, LocalSyncStats, PayloadProtectionContext, PayloadProtectionFailureReason, PayloadProtectionKind, PayloadProtectionOpenInput, PayloadProtectionOptions, PayloadProtectionProvider, PayloadProtectionProviderConfig, PayloadProtectionReadPolicy, PayloadProtectionSealInput, PayloadProtectionSealResult, PayloadProtectionWritePolicy, RemoteCursor, RemoteCursorSaveSource, RemoteCursorStore, Result, SnapshotCodec, SnapshotTransformHook, SnapshotUploadOptions, StreamsAuthContext, StreamsAuthProvider, StreamsCrdt, StreamsCrdtOptions, StreamsCrdtShardUrlsOptions, TransportCatchupParams, TransportCatchupSuccess, TransportCreateStreamSuccess, TransportDeleteStreamSuccess, TransportError, TransportJoinParams, TransportRoomStatus, TransportSnapshotUploadSuccess, TransportSubscription, TransportSyncSuccess, WriteOnlyAppendResult, createInitialRemoteCursor, isValidRillId };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-CggbAC6b.js";
1
+ import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-BygPTNvQ.js";
2
2
  export { PayloadProtectionError as E2eeError, PayloadProtectionError, EphemeralStreamCrdt, InMemoryRemoteCursorStore, IndexedDbRemoteCursorStore, LocalAppendFailedError, StreamsCrdt, createInitialRemoteCursor, isValidRillId };
package/dist/loro.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CO65-58E.js";
1
+ import { $ as WriteOnlyAppendResult, A as PayloadProtectionProviderConfig, B as StreamsAuthProvider, C as LocalSyncStats, D as PayloadProtectionOpenInput, E as PayloadProtectionKind, F as Result, G as TransportCreateStreamSuccess, H as StreamsCrdtShardUrlsOptions, I as SnapshotCodec, J as TransportJoinParams, K as TransportDeleteStreamSuccess, L as SnapshotTransformHook, M as PayloadProtectionSealInput, N as PayloadProtectionSealResult, O as PayloadProtectionOptions, P as PayloadProtectionWritePolicy, Q as TransportSyncSuccess, R as SnapshotUploadOptions, S as JsonValue, T as PayloadProtectionFailureReason, U as TransportCatchupParams, V as StreamsCrdtOptions, W as TransportCatchupSuccess, X as TransportSnapshotUploadSuccess, Y as TransportRoomStatus, Z as TransportSubscription, _ as EphemeralStreamCrdtOptions, a as StreamsCrdt, at as RemoteCursor, b as IsolatedCrdtAdapter, c as CrdtApplyReturn, ct as createInitialRemoteCursor, d as E2eeOptions, et as BeforeRemoteCursorSaveContext, f as E2eeProvider, g as EphemeralStreamAdaptor, h as E2eeWritePolicy, i as EphemeralStreamCrdt, it as IndexedDbRemoteCursorStoreOptions, j as PayloadProtectionReadPolicy, k as PayloadProtectionProvider, l as CrdtUnresolvedSpan, m as E2eeReadPolicy, n as PayloadProtectionError, nt as InMemoryRemoteCursorStore, o as CrdtAdapter, ot as RemoteCursorSaveSource, p as E2eeProviderConfig, q as TransportError, r as LocalAppendFailedError, rt as IndexedDbRemoteCursorStore, s as CrdtApplyOutcome, st as RemoteCursorStore, t as isValidRillId, tt as BeforeRemoteCursorSaveHook, u as CrdtUpdateBatch, v as EphemeralStreamJoinParams, w as PayloadProtectionContext, x as JsonObject, y as EphemeralStreamSubscription, z as StreamsAuthContext } from "./stream-id-CVCOLgSp.js";
2
2
  import { LoroDoc } from "loro-crdt";
3
3
 
4
4
  //#region src/loro.d.ts
package/dist/loro.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-CggbAC6b.js";
1
+ import { a as createInitialRemoteCursor, c as PayloadProtectionError, i as IndexedDbRemoteCursorStore, n as StreamsCrdt, o as EphemeralStreamCrdt, r as InMemoryRemoteCursorStore, s as LocalAppendFailedError, t as isValidRillId } from "./stream-id-BygPTNvQ.js";
2
2
  import { LoroDoc, VersionVector, decodeImportBlobMeta } from "loro-crdt";
3
3
  //#region src/loro.ts
4
4
  function batchImportUpdates(doc, updates) {
@@ -2066,11 +2066,32 @@ function createInitialRemoteCursor(input) {
2066
2066
  }
2067
2067
  //#endregion
2068
2068
  //#region src/transport-cursor.ts
2069
+ const MAX_SPANS_IN_MESSAGE = 8;
2070
+ /**
2071
+ * Renders a bounded summary of unresolved spans for the error message, so a
2072
+ * caller's error log names the author(s) whose ops are missing from the
2073
+ * stream without the caller having to unwrap `error.unresolved` itself.
2074
+ */
2075
+ function formatUnresolvedSpans(unresolved) {
2076
+ if (unresolved.length === 0) return "";
2077
+ const shown = unresolved.slice(0, MAX_SPANS_IN_MESSAGE).map((span) => `${span.peer}@[${span.start}..${span.end})`).join(", ");
2078
+ const suffix = unresolved.length > MAX_SPANS_IN_MESSAGE ? `, +${unresolved.length - MAX_SPANS_IN_MESSAGE} more` : "";
2079
+ return ` (missing deps for ${unresolved.length} span(s): ${shown}${suffix})`;
2080
+ }
2069
2081
  var CrdtApplyIncompleteError = class extends Error {
2070
2082
  unresolved;
2071
- constructor(unresolved) {
2072
- super("CRDT apply incomplete: unresolved remote updates remain");
2083
+ /**
2084
+ * The volatile server version at the moment apply failed: everything the
2085
+ * server delivered that the local doc actually applied (success spans
2086
+ * only — pending spans never advance it). `exportUpdates(appliedVersion)`
2087
+ * is therefore exactly "the ops this replica holds that the server does
2088
+ * not", which is what the join-time salvage append publishes.
2089
+ */
2090
+ appliedVersion;
2091
+ constructor(unresolved, appliedVersion) {
2092
+ super(`CRDT apply incomplete: unresolved remote updates remain${formatUnresolvedSpans(unresolved)}`);
2073
2093
  this.unresolved = unresolved;
2094
+ this.appliedVersion = appliedVersion;
2074
2095
  }
2075
2096
  };
2076
2097
  function isApplyOutcome(value) {
@@ -2098,6 +2119,22 @@ var TransportCursorManager = class {
2098
2119
  isCursorComplete(cursor) {
2099
2120
  return !this.incompleteCursors.has(cursor);
2100
2121
  }
2122
+ /**
2123
+ * Marks `cursor` incomplete without an apply.
2124
+ *
2125
+ * The `CrdtApplyIncompleteError` throw path discards its candidate cursor
2126
+ * BEFORE the usual WeakMap marking, so a caller's retained cursor keeps
2127
+ * whatever completeness it had. For a caught-up live room the first
2128
+ * poisoned event typically arrives with `up_to_date=true`, so the retained
2129
+ * cursor still reads "complete" for the whole wedge — leaving the degraded
2130
+ * local flush unengaged and the snapshot-upload clean gate open, exactly
2131
+ * the two consumers keyed on this marking. The read loop calls this when
2132
+ * it catches the error so the room's incomplete state is visible on the
2133
+ * cursor it actually holds.
2134
+ */
2135
+ markCursorIncomplete(cursor, unresolved) {
2136
+ this.incompleteCursors.set(cursor, unresolved);
2137
+ }
2101
2138
  async applyBootstrapPayload(cursor, snapshot, updates, nextOffset, upToDate = false) {
2102
2139
  let nextVersion = cursor.serverLowerBoundVersion;
2103
2140
  let complete = true;
@@ -2171,7 +2208,7 @@ var TransportCursorManager = class {
2171
2208
  }
2172
2209
  async finalizeCursor(cursor, source, complete, unresolved, upToDate) {
2173
2210
  if (!complete) {
2174
- if (upToDate) throw new CrdtApplyIncompleteError(unresolved);
2211
+ if (upToDate) throw new CrdtApplyIncompleteError(unresolved, cursor.serverLowerBoundVersion);
2175
2212
  this.incompleteCursors.set(cursor, unresolved);
2176
2213
  return cursor;
2177
2214
  }
@@ -2326,6 +2363,7 @@ function createJoinState(cursor, liveMode = "sse") {
2326
2363
  pendingLocal: new Deque(),
2327
2364
  flushingLocal: false,
2328
2365
  retryAttempt: 0,
2366
+ incompleteRecoveryAttempt: 0,
2329
2367
  retrySleepController: new AbortController(),
2330
2368
  readLoopRunning: false,
2331
2369
  readErrorRetryAttempt: 0,
@@ -2909,7 +2947,13 @@ var StreamsCrdt = class StreamsCrdt {
2909
2947
  async startJoin(state) {
2910
2948
  try {
2911
2949
  state.requestController ??= new AbortController();
2912
- const joined = await this.performInitialJoinSync(state.liveMode, state.requestController?.signal);
2950
+ let joined;
2951
+ try {
2952
+ joined = await this.performInitialJoinSync(state.liveMode, state.requestController?.signal);
2953
+ } catch (error) {
2954
+ if (!(error instanceof CrdtApplyIncompleteError) || state.closed || !await this.salvageLocalBacklogOnIncompleteInitialSync(state, error)) throw error;
2955
+ joined = await this.performInitialJoinSync(state.liveMode, state.requestController?.signal);
2956
+ }
2913
2957
  state.cursor = joined.cursor;
2914
2958
  state.streamCursor = joined.streamCursor;
2915
2959
  state.liveMode = joined.liveMode;
@@ -2941,6 +2985,65 @@ var StreamsCrdt = class StreamsCrdt {
2941
2985
  throw error;
2942
2986
  }
2943
2987
  }
2988
+ /**
2989
+ * Join-time salvage for a stream whose head cannot be fully applied
2990
+ * (apply_incomplete at `up_to_date`, even after bootstrap): before the
2991
+ * join surfaces the error, durably append everything this replica holds
2992
+ * that the server does not.
2993
+ *
2994
+ * Why this exists: the dual-author wedge is usually two-sided — the stream
2995
+ * is missing some author's ops, and a replica that HOLDS those ops (e.g.
2996
+ * received out-of-band over a local data plane) may itself be unable to
2997
+ * complete the join because of a second gap. The regular join-time export
2998
+ * runs only AFTER a successful initial sync, so without this salvage such
2999
+ * a replica can never publish the healing ops, and its own backlog from a
3000
+ * previous process run stays local for as long as the stream stays
3001
+ * poisoned.
3002
+ *
3003
+ * `error.appliedVersion` advances only from success spans, so
3004
+ * `exportUpdates(appliedVersion)` is exactly "ops this replica holds that
3005
+ * the server does not" — the failed initial sync already imported
3006
+ * everything the server DOES have, so the delta cannot re-append
3007
+ * server-known data. Cursor safety is untouched: the append is write-only
3008
+ * style (producer-acked, no read catch-up, no durable-cursor save), and
3009
+ * the retried initial sync re-derives its cursor from the server.
3010
+ *
3011
+ * Returns `true` when anything was durably appended; the caller then
3012
+ * retries the initial sync once. Best-effort: a salvage failure logs and
3013
+ * returns `false` so the original apply-incomplete error is what callers
3014
+ * observe.
3015
+ */
3016
+ async salvageLocalBacklogOnIncompleteInitialSync(state, error) {
3017
+ const appliedVersion = error.appliedVersion;
3018
+ if (appliedVersion == null) return false;
3019
+ try {
3020
+ const appended = await this.enqueueExclusive(async () => {
3021
+ let didAppend = false;
3022
+ const frozen = this.frozenLocalAppend;
3023
+ if (frozen != null) {
3024
+ const ack = await this.appendFrozenLocalBatchRemote(frozen);
3025
+ this.commitProducerAck(ack.producerAck);
3026
+ didAppend = true;
3027
+ }
3028
+ await this.adapter.flushLocalExports?.();
3029
+ const localBatch = this.exportUpdates(appliedVersion);
3030
+ if (localBatch.batch != null) {
3031
+ const appendedBatch = await this.appendLocalBatchRemote({
3032
+ ...state.cursor,
3033
+ serverLowerBoundVersion: appliedVersion
3034
+ }, localBatch.batch, void 0, "direct");
3035
+ this.commitProducerAck(appendedBatch.producerAck);
3036
+ didAppend = true;
3037
+ }
3038
+ return didAppend;
3039
+ });
3040
+ if (appended) this.logDebug("join salvage appended local backlog", { streamUrl: this.streamUrl });
3041
+ return appended;
3042
+ } catch (salvageError) {
3043
+ this.logError("join salvage append failed", salvageError, { streamUrl: this.streamUrl });
3044
+ return false;
3045
+ }
3046
+ }
2944
3047
  async syncOnce(operation, parentSignal) {
2945
3048
  let cursor = (await this.finalizeDirectLocalAppendIfNeeded(await this.resolveInitialRemoteState(operation, parentSignal))).cursor;
2946
3049
  const localBatch = this.exportUpdates(cursor.serverLowerBoundVersion);
@@ -2955,8 +3058,11 @@ var StreamsCrdt = class StreamsCrdt {
2955
3058
  let liveMode = preferredLiveMode;
2956
3059
  const exportedLocal = this.exportUpdates(localExportRefVersion);
2957
3060
  localExportRefVersion = exportedLocal.nextRefVersion;
2958
- if (exportedLocal.batch != null) {
2959
- const committed = await this.appendLocalBatchDurably(cursor, exportedLocal.batch, void 0, "direct", streamCursor);
3061
+ const exportedBatch = exportedLocal.batch;
3062
+ if (exportedBatch != null) {
3063
+ const appendCursor = cursor;
3064
+ const appendStreamCursor = streamCursor;
3065
+ const committed = await this.enqueueExclusive(() => this.appendLocalBatchDurably(appendCursor, exportedBatch, void 0, "direct", appendStreamCursor));
2960
3066
  cursor = committed.cursor;
2961
3067
  streamCursor = committed.streamCursor;
2962
3068
  }
@@ -3116,7 +3222,8 @@ var StreamsCrdt = class StreamsCrdt {
3116
3222
  };
3117
3223
  }
3118
3224
  }
3119
- async recoverLiveIncompleteRemoteApply(state) {
3225
+ async recoverLiveIncompleteRemoteApply(state, error) {
3226
+ this.cursorManager.markCursorIncomplete(state.cursor, error.unresolved);
3120
3227
  try {
3121
3228
  const bootstrapped = await this.bootstrapState({
3122
3229
  ...state.cursor,
@@ -3126,6 +3233,7 @@ var StreamsCrdt = class StreamsCrdt {
3126
3233
  state.cursor = bootstrapped.cursor;
3127
3234
  state.streamCursor = bootstrapped.streamCursor;
3128
3235
  state.retryAttempt = 0;
3236
+ state.incompleteRecoveryAttempt = 0;
3129
3237
  state.lastError = void 0;
3130
3238
  this.setReadSubStatus(state, "ok");
3131
3239
  if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
@@ -3137,7 +3245,10 @@ var StreamsCrdt = class StreamsCrdt {
3137
3245
  code: classified.code
3138
3246
  });
3139
3247
  state.lastError = toTransportError(error);
3248
+ if (state.pendingLocal.length > 0) this.flushPendingLocal(state);
3140
3249
  if (classified.retriable && !isAuthOrProtocolError(error)) {
3250
+ state.incompleteRecoveryAttempt += 1;
3251
+ state.retryAttempt = Math.max(state.retryAttempt, state.incompleteRecoveryAttempt);
3141
3252
  this.setReadSubStatus(state, "reconnecting");
3142
3253
  return "retry";
3143
3254
  }
@@ -3613,7 +3724,7 @@ var StreamsCrdt = class StreamsCrdt {
3613
3724
  if (state.closed) return;
3614
3725
  if (error instanceof CrdtApplyIncompleteError) {
3615
3726
  if (capturedController?.signal.aborted && state.requestController === capturedController) state.requestController = new AbortController();
3616
- const recovery = await this.recoverLiveIncompleteRemoteApply(state);
3727
+ const recovery = await this.recoverLiveIncompleteRemoteApply(state, error);
3617
3728
  if (recovery === "recovered") continue;
3618
3729
  if (recovery === "terminal") return;
3619
3730
  if (await this.awaitReadRetryBackoff(state, error)) continue;
@@ -3647,7 +3758,7 @@ var StreamsCrdt = class StreamsCrdt {
3647
3758
  const flushError = abortReason instanceof Error ? abortReason : error;
3648
3759
  if (flushError instanceof CrdtApplyIncompleteError) {
3649
3760
  if (state.requestController === capturedController) state.requestController = new AbortController();
3650
- const recovery = await this.recoverLiveIncompleteRemoteApply(state);
3761
+ const recovery = await this.recoverLiveIncompleteRemoteApply(state, flushError);
3651
3762
  if (recovery === "recovered") continue;
3652
3763
  if (recovery === "terminal") return;
3653
3764
  if (await this.awaitReadRetryBackoff(state, flushError)) continue;
@@ -3742,10 +3853,15 @@ var StreamsCrdt = class StreamsCrdt {
3742
3853
  * `"error"` write status (with `lastWriteError` set) and then retries via
3743
3854
  * this flush's own backoff timer (`maybeScheduleWriteRetry`) — it does not
3744
3855
  * depend on a future local op to re-trigger the flush.
3856
+ *
3857
+ * While the cursor is incomplete (unresolved remote spans), the flush
3858
+ * degrades to an append-only mode instead of pausing: batches still reach
3859
+ * the server, but read progress, durable-cursor saves, and snapshot
3860
+ * scheduling stay untouched. See the inline comment in the incomplete
3861
+ * branch and specs/loro-pending.md "Local Append Rules".
3745
3862
  */
3746
3863
  async flushPendingLocal(state) {
3747
3864
  if (state.flushingLocal || state.closed) return;
3748
- if (!this.cursorManager.isCursorComplete(state.cursor)) return;
3749
3865
  this.clearWriteRetryTimer(state);
3750
3866
  state.flushingLocal = true;
3751
3867
  try {
@@ -3754,16 +3870,23 @@ var StreamsCrdt = class StreamsCrdt {
3754
3870
  if (drained == null) return;
3755
3871
  let attempts = 0;
3756
3872
  while (!state.closed) try {
3757
- state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
3758
- const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
3759
- if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
3760
- const committedCount = this.consumeCommittedPendingCount() ?? drained.count;
3873
+ let committedCount;
3874
+ if (this.cursorManager.isCursorComplete(state.cursor)) {
3875
+ state.cursor = await this.enqueueExclusive(async () => this.appendLocalBatch(state.cursor, drained.batch, drained.count, state.streamCursor));
3876
+ const committedStreamCursor = this.consumeCommittedLocalStreamCursor();
3877
+ if (committedStreamCursor != null) state.streamCursor = committedStreamCursor;
3878
+ committedCount = this.consumeCommittedPendingCount() ?? drained.count;
3879
+ this.snapshotManager.schedule(state);
3880
+ } else {
3881
+ const appended = await this.enqueueExclusive(async () => this.appendLocalBatchRemote(state.cursor, drained.batch, drained.count, "pending"));
3882
+ this.commitProducerAck(appended.producerAck);
3883
+ committedCount = appended.pendingCount ?? drained.count;
3884
+ }
3761
3885
  for (let i = 0; i < committedCount; i += 1) state.pendingLocal.popFront();
3762
3886
  this.notifySyncWaiters(state, committedCount);
3763
3887
  state.flushHeadBatchOnly = false;
3764
3888
  state.lastWriteError = void 0;
3765
3889
  this.setWriteSubStatus(state, "ok");
3766
- this.snapshotManager.schedule(state);
3767
3890
  break;
3768
3891
  } catch (error) {
3769
3892
  if (isPayloadTooLargeError(error) && drained.count > 1 && this.abandonPendingFrozenLocalAppend()) {
@@ -3857,12 +3980,15 @@ var StreamsCrdt = class StreamsCrdt {
3857
3980
  for (const waiter of state.syncWaiters.splice(0)) waiter.reject(error);
3858
3981
  }
3859
3982
  async saveRemoteCursor(cursor, source) {
3860
- return await persistRemoteCursor({
3983
+ const saved = await persistRemoteCursor({
3861
3984
  remoteCursorStore: this.remoteCursorStore,
3862
3985
  cursor,
3863
3986
  source,
3864
3987
  beforeRemoteCursorSave: this.beforeRemoteCursorSave
3865
3988
  });
3989
+ const state = this.joinState;
3990
+ if (state != null && !state.closed) state.incompleteRecoveryAttempt = 0;
3991
+ return saved;
3866
3992
  }
3867
3993
  setReadSubStatus(state, sub) {
3868
3994
  if (sub === "ok") state.readErrorRetryAttempt = 0;
@@ -3945,4 +4071,4 @@ function isValidRillId(id) {
3945
4071
  //#endregion
3946
4072
  export { createInitialRemoteCursor as a, PayloadProtectionError as c, IndexedDbRemoteCursorStore as i, StreamsCrdt as n, EphemeralStreamCrdt as o, InMemoryRemoteCursorStore as r, LocalAppendFailedError as s, isValidRillId as t };
3947
4073
 
3948
- //# sourceMappingURL=stream-id-CggbAC6b.js.map
4074
+ //# sourceMappingURL=stream-id-BygPTNvQ.js.map