@tinycloud/replica 0.4.0-beta.1 → 0.4.0-beta.3

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.
@@ -3616,6 +3616,8 @@ var DEFAULT_PAGE_LIMIT = 500;
3616
3616
  var DEFAULT_LEASE_TTL_MS = 12e4;
3617
3617
  var FETCH_CHUNK = 100;
3618
3618
  var REPAIR_BATCH = 100;
3619
+ var REPAIR_MAX_TRIES = 5;
3620
+ var REPAIR_BACKOFF_BASE_MS = 6e4;
3619
3621
  var RETENTION_REVOKED = /* @__PURE__ */ new Set(["retention-grant-revoked", "retention-grant-ancestor-revoked"]);
3620
3622
  var PENDING_REFUSALS = /* @__PURE__ */ new Set([
3621
3623
  ReplicaErrorCode.GRANT_UNAUTHORIZED,
@@ -3625,6 +3627,9 @@ var PENDING_REFUSALS = /* @__PURE__ */ new Set([
3625
3627
  function contentHash(bytes) {
3626
3628
  return bytesToHex(blake3(bytes));
3627
3629
  }
3630
+ function repairId(entry) {
3631
+ return `${entry.hash}\0${entry.key}`;
3632
+ }
3628
3633
  function sameEtag(a, b) {
3629
3634
  return a.replace(/^"|"$/g, "") === b.replace(/^"|"$/g, "");
3630
3635
  }
@@ -3641,6 +3646,9 @@ var Replica = class {
3641
3646
  #now;
3642
3647
  #leaseTtlMs;
3643
3648
  #onCommit;
3649
+ #contentFetch;
3650
+ /** Failed repair fetches of this session, by key and ETag hash. Never persisted: a new `Replica` retries afresh. */
3651
+ #repairTries = /* @__PURE__ */ new Map();
3644
3652
  constructor(options) {
3645
3653
  this.#store = options.store;
3646
3654
  const transport = options.transport;
@@ -3648,6 +3656,7 @@ var Replica = class {
3648
3656
  this.#now = options.now ?? Date.now;
3649
3657
  this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
3650
3658
  this.#onCommit = options.onCommit;
3659
+ this.#contentFetch = options.contentFetch ?? "eager";
3651
3660
  }
3652
3661
  async #state() {
3653
3662
  const state = await this.#store.open();
@@ -3976,6 +3985,7 @@ var Replica = class {
3976
3985
  changes[candidate.index].content = true;
3977
3986
  return false;
3978
3987
  });
3988
+ if (this.#contentFetch === "lazy") return { changes, blobs, fetched: 0 };
3979
3989
  let fetched2 = 0;
3980
3990
  for (let start = 0; start < needed.length; start += FETCH_CHUNK) {
3981
3991
  const chunk = needed.slice(start, start + FETCH_CHUNK);
@@ -3995,7 +4005,15 @@ var Replica = class {
3995
4005
  return { changes, blobs, fetched: fetched2 };
3996
4006
  }
3997
4007
  async #repair(attempt, cursor, nodeDid, coverage, retentionGrantCid) {
3998
- const pending = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
4008
+ if (this.#contentFetch === "lazy") return 0;
4009
+ const all = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
4010
+ const now = this.#now();
4011
+ const pendingIds = new Set(all.map((entry) => repairId(entry)));
4012
+ for (const id of this.#repairTries.keys()) if (!pendingIds.has(id)) this.#repairTries.delete(id);
4013
+ const pending = all.filter((entry) => {
4014
+ const state = this.#repairTries.get(repairId(entry));
4015
+ return state === void 0 || state.tries < REPAIR_MAX_TRIES && now >= state.nextAttemptAt;
4016
+ });
3999
4017
  let repaired = 0;
4000
4018
  for (let start = 0; start < pending.length; start += REPAIR_BATCH) {
4001
4019
  const chunk = pending.slice(start, start + REPAIR_BATCH);
@@ -4007,7 +4025,14 @@ var Replica = class {
4007
4025
  const changes = [];
4008
4026
  for (const entry of chunk) {
4009
4027
  const bytes = acceptContent(entry, contents.get(entry.key));
4010
- if (bytes === void 0) continue;
4028
+ const id = repairId(entry);
4029
+ if (bytes === void 0) {
4030
+ const tries = (this.#repairTries.get(id)?.tries ?? 0) + 1;
4031
+ const wait = tries === 1 ? 0 : REPAIR_BACKOFF_BASE_MS * 2 ** (tries - 2);
4032
+ this.#repairTries.set(id, { tries, nextAttemptAt: this.#now() + wait });
4033
+ continue;
4034
+ }
4035
+ this.#repairTries.delete(id);
4011
4036
  blobs.set(entry.hash, bytes);
4012
4037
  changes.push({ ...entry, content: true });
4013
4038
  }
@@ -6943,6 +6968,7 @@ ${spec.prefix}`);
6943
6968
  store: handle.store,
6944
6969
  now,
6945
6970
  transportFor: (record) => deps.transportFor(record, handle.spec, handle.device),
6971
+ ...handle.spec.contentFetch === void 0 ? {} : { contentFetch: handle.spec.contentFetch },
6946
6972
  onCommit: () => pages += 1
6947
6973
  });
6948
6974
  let outcome;
@@ -3616,6 +3616,8 @@ var DEFAULT_PAGE_LIMIT = 500;
3616
3616
  var DEFAULT_LEASE_TTL_MS = 12e4;
3617
3617
  var FETCH_CHUNK = 100;
3618
3618
  var REPAIR_BATCH = 100;
3619
+ var REPAIR_MAX_TRIES = 5;
3620
+ var REPAIR_BACKOFF_BASE_MS = 6e4;
3619
3621
  var RETENTION_REVOKED = /* @__PURE__ */ new Set(["retention-grant-revoked", "retention-grant-ancestor-revoked"]);
3620
3622
  var PENDING_REFUSALS = /* @__PURE__ */ new Set([
3621
3623
  ReplicaErrorCode.GRANT_UNAUTHORIZED,
@@ -3625,6 +3627,9 @@ var PENDING_REFUSALS = /* @__PURE__ */ new Set([
3625
3627
  function contentHash(bytes) {
3626
3628
  return bytesToHex(blake3(bytes));
3627
3629
  }
3630
+ function repairId(entry) {
3631
+ return `${entry.hash}\0${entry.key}`;
3632
+ }
3628
3633
  function sameEtag(a, b) {
3629
3634
  return a.replace(/^"|"$/g, "") === b.replace(/^"|"$/g, "");
3630
3635
  }
@@ -3641,6 +3646,9 @@ var Replica = class {
3641
3646
  #now;
3642
3647
  #leaseTtlMs;
3643
3648
  #onCommit;
3649
+ #contentFetch;
3650
+ /** Failed repair fetches of this session, by key and ETag hash. Never persisted: a new `Replica` retries afresh. */
3651
+ #repairTries = /* @__PURE__ */ new Map();
3644
3652
  constructor(options) {
3645
3653
  this.#store = options.store;
3646
3654
  const transport = options.transport;
@@ -3648,6 +3656,7 @@ var Replica = class {
3648
3656
  this.#now = options.now ?? Date.now;
3649
3657
  this.#leaseTtlMs = options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
3650
3658
  this.#onCommit = options.onCommit;
3659
+ this.#contentFetch = options.contentFetch ?? "eager";
3651
3660
  }
3652
3661
  async #state() {
3653
3662
  const state = await this.#store.open();
@@ -3976,6 +3985,7 @@ var Replica = class {
3976
3985
  changes[candidate.index].content = true;
3977
3986
  return false;
3978
3987
  });
3988
+ if (this.#contentFetch === "lazy") return { changes, blobs, fetched: 0 };
3979
3989
  let fetched2 = 0;
3980
3990
  for (let start = 0; start < needed.length; start += FETCH_CHUNK) {
3981
3991
  const chunk = needed.slice(start, start + FETCH_CHUNK);
@@ -3995,7 +4005,15 @@ var Replica = class {
3995
4005
  return { changes, blobs, fetched: fetched2 };
3996
4006
  }
3997
4007
  async #repair(attempt, cursor, nodeDid, coverage, retentionGrantCid) {
3998
- const pending = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
4008
+ if (this.#contentFetch === "lazy") return 0;
4009
+ const all = await this.#store.pendingRepairs(Number.MAX_SAFE_INTEGER);
4010
+ const now = this.#now();
4011
+ const pendingIds = new Set(all.map((entry) => repairId(entry)));
4012
+ for (const id of this.#repairTries.keys()) if (!pendingIds.has(id)) this.#repairTries.delete(id);
4013
+ const pending = all.filter((entry) => {
4014
+ const state = this.#repairTries.get(repairId(entry));
4015
+ return state === void 0 || state.tries < REPAIR_MAX_TRIES && now >= state.nextAttemptAt;
4016
+ });
3999
4017
  let repaired = 0;
4000
4018
  for (let start = 0; start < pending.length; start += REPAIR_BATCH) {
4001
4019
  const chunk = pending.slice(start, start + REPAIR_BATCH);
@@ -4007,7 +4025,14 @@ var Replica = class {
4007
4025
  const changes = [];
4008
4026
  for (const entry of chunk) {
4009
4027
  const bytes = acceptContent(entry, contents.get(entry.key));
4010
- if (bytes === void 0) continue;
4028
+ const id = repairId(entry);
4029
+ if (bytes === void 0) {
4030
+ const tries = (this.#repairTries.get(id)?.tries ?? 0) + 1;
4031
+ const wait = tries === 1 ? 0 : REPAIR_BACKOFF_BASE_MS * 2 ** (tries - 2);
4032
+ this.#repairTries.set(id, { tries, nextAttemptAt: this.#now() + wait });
4033
+ continue;
4034
+ }
4035
+ this.#repairTries.delete(id);
4011
4036
  blobs.set(entry.hash, bytes);
4012
4037
  changes.push({ ...entry, content: true });
4013
4038
  }
@@ -6943,6 +6968,7 @@ ${spec.prefix}`);
6943
6968
  store: handle.store,
6944
6969
  now,
6945
6970
  transportFor: (record) => deps.transportFor(record, handle.spec, handle.device),
6971
+ ...handle.spec.contentFetch === void 0 ? {} : { contentFetch: handle.spec.contentFetch },
6946
6972
  onCommit: () => pages += 1
6947
6973
  });
6948
6974
  let outcome;