@mdbase-dev/sdk 0.1.0-beta.178 → 0.1.0-beta.180

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.
@@ -15691,12 +15691,12 @@ function appReleaseTrust() {
15691
15691
  // src/application/asset-pins.json
15692
15692
  var asset_pins_default = {
15693
15693
  runtime: {
15694
- sha256: "5f296cb57d8c1da1a9710f91a893efece2340ff893583b088feb531a16ced2ef",
15695
- bytes: 3849226,
15696
- maxBytes: 3849226,
15694
+ sha256: "be17475e2ca164642fb5b29b4bf003c03ad9a8dbacd8aebc135dad7e91b73585",
15695
+ bytes: 3849256,
15696
+ maxBytes: 3849256,
15697
15697
  source: {
15698
15698
  repository: "mdbase-dev/mdbase-connect",
15699
- commit: "61dd7650312f04e942de51536b6441142c79ec48"
15699
+ commit: "323d30aff40fc6f3f172d44f60cd7e67bb17d977"
15700
15700
  }
15701
15701
  },
15702
15702
  sqlite: {
@@ -17790,6 +17790,23 @@ var AppProtectedInstallationSignIn = class _AppProtectedInstallationSignIn {
17790
17790
  }
17791
17791
  /** Current approved/member cloud-copy metadata only; never readiness, keying,
17792
17792
  * or permission inferred from this response or retained consent snapshots. */
17793
+ /** The kept listing narrowed to this installation's consent. File actions
17794
+ * are never kept, so every row has none until the next read. */
17795
+ keptCollections() {
17796
+ if (this.state.collectionListing === void 0) return null;
17797
+ return Object.freeze(collectionListing(this.state.collectionListing).filter((c) => this.state.collectionIds.includes(c.collectionId) && this.modeConsented(c)).map((c) => Object.freeze({ ...c, fileActions: Object.freeze([]) })));
17798
+ }
17799
+ /** The approved collections as this installation last read them, or null
17800
+ * when no listing is kept. No request. It can be out of date in every way a
17801
+ * list can; `listApprovedCollections` is still the authority. */
17802
+ cachedApprovedCollections(installation) {
17803
+ this.consentGuard(installation)();
17804
+ try {
17805
+ return this.keptCollections();
17806
+ } catch {
17807
+ return null;
17808
+ }
17809
+ }
17793
17810
  listApprovedCollections(installation) {
17794
17811
  const check3 = this.consentGuard(installation);
17795
17812
  return this.exclusive(async () => {
@@ -17806,7 +17823,7 @@ var AppProtectedInstallationSignIn = class _AppProtectedInstallationSignIn {
17806
17823
  } catch (error4) {
17807
17824
  check3();
17808
17825
  if (error4 instanceof AppInstallationSignInError && error4.reason === "unavailable" && !error4.server && !nonOkResponses.has(error4) && this.state.collectionListing !== void 0)
17809
- return Object.freeze(collectionListing(this.state.collectionListing).filter((c) => this.state.collectionIds.includes(c.collectionId) && this.modeConsented(c)).map((c) => Object.freeze({ ...c, fileActions: Object.freeze([]) })));
17826
+ return this.keptCollections();
17810
17827
  if (error4 instanceof AppInstallationSignInError && (error4.server || nonOkResponses.has(error4)) && this.state.collectionListing !== void 0) {
17811
17828
  const state2 = { ...this.state };
17812
17829
  delete state2.collectionListing;
@@ -36924,6 +36941,18 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
36924
36941
  return "unavailable";
36925
36942
  }
36926
36943
  }
36944
+ /** Whether the replica on this device holds anything from the log. Every
36945
+ * log starts with the entries that admitted this device, so a replica
36946
+ * that knows no head is empty (its storage was lost), not up to date. */
36947
+ holdsLog() {
36948
+ try {
36949
+ if (!this.native) return false;
36950
+ const status = this.native.observations().status;
36951
+ return status.mode === "local_only" || status.headKnown > 0;
36952
+ } catch {
36953
+ return false;
36954
+ }
36955
+ }
36927
36956
  /** Diagnostics while waiting for a key (native sync status), else null. */
36928
36957
  keyWaitDetail() {
36929
36958
  try {
@@ -36932,6 +36961,15 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
36932
36961
  return null;
36933
36962
  }
36934
36963
  }
36964
+ /** For a collection opened before its log: has the reopened replica its
36965
+ * keys back from the log's control prefix? `failed` is what a waited open
36966
+ * refuses as `recovery_required`. */
36967
+ keyringSettled() {
36968
+ if (!this.native) throw fail15("binding");
36969
+ const observation = this.native.observations();
36970
+ if (observation.requiresReopen || observation.keyringRebuildFailed) return "failed";
36971
+ return observation.keyringRebuilding ? "pending" : "settled";
36972
+ }
36935
36973
  /** Local writes not yet confirmed by the log (native sync status). */
36936
36974
  pendingWrites() {
36937
36975
  try {
@@ -37016,16 +37054,17 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37016
37054
  * behind can finish its catch-up turn; without a pump it is fixed.
37017
37055
  * No status-based admission, write retry, key override or private grant. */
37018
37056
  waitForVerifiedRead(options) {
37019
- const { timeoutMs = 15e3, cold = false } = options;
37057
+ const { timeoutMs = 15e3, cold = false, local = false } = options;
37020
37058
  const signal = this.operationSignal(options.signal);
37021
37059
  this.checkCollection(signal);
37022
- if (!this.native || !this.collectionScope || this.adoptedEndpoint === null || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 15e3 || typeof cold !== "boolean" || cold && !this.pump)
37060
+ if (!this.native || !this.collectionScope || this.adoptedEndpoint === null || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 15e3 || typeof cold !== "boolean" || typeof local !== "boolean" || cold && (local || !this.pump))
37023
37061
  throw fail15("binding");
37062
+ if (local && !this.holdsLog()) return Promise.reject(fail15("unavailable"));
37024
37063
  if (this.readWork)
37025
37064
  return this.readWork.then(() => {
37026
37065
  this.checkCollection(signal);
37027
37066
  });
37028
- const work2 = this.verifyRead(signal, timeoutMs, cold, options.onProgress);
37067
+ const work2 = this.verifyRead(signal, timeoutMs, cold, local, options.onProgress);
37029
37068
  this.readWork = work2;
37030
37069
  void work2.finally(() => {
37031
37070
  if (this.readWork === work2) this.readWork = null;
@@ -37033,8 +37072,9 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37033
37072
  });
37034
37073
  return work2;
37035
37074
  }
37036
- async verifyRead(signal, timeoutMs, cold, onProgress) {
37075
+ async verifyRead(signal, timeoutMs, cold, local, onProgress) {
37037
37076
  const ctrl = new AbortController(), abort = () => ctrl.abort();
37077
+ const pump = () => local ? null : this.pump;
37038
37078
  this.readAbort = ctrl;
37039
37079
  signal.addEventListener("abort", abort, { once: true });
37040
37080
  let timer = null;
@@ -37057,7 +37097,7 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37057
37097
  };
37058
37098
  const observe = () => {
37059
37099
  this.checkCollection(signal);
37060
- const refusal2 = cold ? this.pump.terminalProblem : null;
37100
+ const refusal2 = cold ? pump().terminalProblem : null;
37061
37101
  if (isAppServerProblem(refusal2))
37062
37102
  throw new AppHttpLogError(appServerOutcome(refusal2) ?? "unavailable", refusal2);
37063
37103
  const observation = this.native.observations();
@@ -37068,14 +37108,14 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37068
37108
  };
37069
37109
  this.readVerified = false;
37070
37110
  try {
37071
- if (this.pump) {
37111
+ if (pump()) {
37072
37112
  const progress2 = () => {
37073
37113
  const observation = observe();
37074
37114
  return [
37075
37115
  observation.status.confirmedThrough,
37076
37116
  observation.status.installing?.done ?? 0,
37077
37117
  observation.status.installing?.total ?? 0,
37078
- this.pump.readProgress
37118
+ pump().readProgress
37079
37119
  ];
37080
37120
  };
37081
37121
  let high = progress2(), lastProgress = Date.now();
@@ -37089,7 +37129,7 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37089
37129
  deferred2 = null;
37090
37130
  }
37091
37131
  high = next.map((value2, i) => Math.max(value2, high[i]));
37092
- const active = this.pump.responseProgress();
37132
+ const active = pump().responseProgress();
37093
37133
  const ids = new Set(active.map((body) => body.id));
37094
37134
  for (const id4 of bodies.keys()) if (!ids.has(id4)) bodies.delete(id4);
37095
37135
  for (const body of active) {
@@ -37134,7 +37174,8 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37134
37174
  let delay = 250;
37135
37175
  while (!ctrl.signal.aborted) {
37136
37176
  this.native.tick();
37137
- if (this.pump) await Promise.race([this.pump.pump(), aborted]);
37177
+ const turn = pump();
37178
+ if (turn) await Promise.race([turn.pump(), aborted]);
37138
37179
  this.checkCollection(signal);
37139
37180
  const observation = observe();
37140
37181
  if (cold && initialSyncWait(observation.status) !== null) {
@@ -37153,7 +37194,7 @@ var AppWebCloudCopyHost = class _AppWebCloudCopyHost {
37153
37194
  return;
37154
37195
  } catch (error4) {
37155
37196
  this.checkCollection(signal);
37156
- if (!(error4 instanceof MdbaseError) || error4.code !== "unavailable" || !this.pump || ctrl.signal.aborted)
37197
+ if (!(error4 instanceof MdbaseError) || error4.code !== "unavailable" || !pump() || ctrl.signal.aborted)
37157
37198
  throw error4;
37158
37199
  }
37159
37200
  await waitForOpenRetry(delay, ctrl.signal);
@@ -37787,6 +37828,7 @@ var AppWebSignInSession = class {
37787
37828
  attested = false;
37788
37829
  acknowledged = false;
37789
37830
  consentPending = false;
37831
+ listCached = false;
37790
37832
  fileAccess = null;
37791
37833
  freshApproval = false;
37792
37834
  /** The window this session's approval (or file access request) is shown in. */
@@ -37980,7 +38022,15 @@ var AppWebSignInSession = class {
37980
38022
  if (this.flow.collectionConsentView()?.state === "pending") {
37981
38023
  this.consentPending = true;
37982
38024
  this.publish({ status: "authorizing" });
37983
- } else await this.refreshCollections();
38025
+ } else {
38026
+ let kept = null;
38027
+ try {
38028
+ kept = this.flow.cachedApprovedCollections(this.installation);
38029
+ } catch {
38030
+ kept = null;
38031
+ }
38032
+ await this.refreshCollections(kept);
38033
+ }
37984
38034
  } else this.publish({ status: view.state === "pending" ? "signed_out" : "authorizing" });
37985
38035
  return this.getSnapshot();
37986
38036
  });
@@ -38338,10 +38388,15 @@ var AppWebSignInSession = class {
38338
38388
  this.acknowledged = true;
38339
38389
  }
38340
38390
  }
38341
- async refreshCollections() {
38342
- const collections = await this.flow.listApprovedCollections(this.installation);
38391
+ /** `kept`: this installation's last listing instead of a read. It is not an
38392
+ * accepted inventory, so an approval missing from it is not a removal. */
38393
+ async refreshCollections(kept = null) {
38394
+ const collections = kept ?? await this.flow.listApprovedCollections(this.installation);
38343
38395
  this.check();
38344
- for (const id4 of this.flow.view().collectionIds) if (!collections.some((c) => c.collectionId === id4)) this.removedScopes.add(id4);
38396
+ this.listCached = kept !== null;
38397
+ if (!kept) {
38398
+ for (const id4 of this.flow.view().collectionIds) if (!collections.some((c) => c.collectionId === id4)) this.removedScopes.add(id4);
38399
+ }
38345
38400
  const visible = Object.freeze(collections.filter((c) => !this.forgotten.has(c.collectionId)));
38346
38401
  for (const slot of this.slots.values()) {
38347
38402
  if (slot.usable && slot.selected && !visible.some((c) => c.collectionId === slot.id)) {
@@ -38549,14 +38604,24 @@ var AppWebSignInSession = class {
38549
38604
  if (this.openingSlot === slot) this.openingSlot = null;
38550
38605
  slot.stopAssessment = selected.subscribe?.(() => {
38551
38606
  if (this.closing || this.slots.get(slot.id) !== slot || !slot.usable || slot.selected !== selected || slot.lifetime?.signal.aborted || !this.sameScope(slot.scope, slot.leaseScope) || slot.scope?.collection !== slot.id) return;
38552
- let ended = null;
38607
+ let ended2 = null;
38553
38608
  try {
38554
- ended = selected.ended ?? null;
38609
+ ended2 = selected.ended ?? null;
38555
38610
  } catch {
38556
- ended = null;
38611
+ ended2 = null;
38557
38612
  }
38558
- if (ended) {
38559
- this.accessEnded(slot, selected, ended);
38613
+ if (ended2) {
38614
+ this.accessEnded(slot, selected, ended2);
38615
+ return;
38616
+ }
38617
+ let failed2 = null;
38618
+ try {
38619
+ failed2 = selected.failure ?? null;
38620
+ } catch {
38621
+ failed2 = null;
38622
+ }
38623
+ if (failed2 !== null) {
38624
+ this.openFailedLate(slot, selected, failed2);
38560
38625
  return;
38561
38626
  }
38562
38627
  let next;
@@ -38567,6 +38632,26 @@ var AppWebSignInSession = class {
38567
38632
  }
38568
38633
  this.publishSlot(slot, next.status === "needs_unlock" ? { ...next, problem: null, transport: null, live: null } : { ...next, ...this.liveProblem(slot, selected) });
38569
38634
  }) ?? null;
38635
+ let ended = null;
38636
+ try {
38637
+ ended = selected.ended ?? null;
38638
+ } catch {
38639
+ ended = null;
38640
+ }
38641
+ let failed = null;
38642
+ try {
38643
+ failed = selected.failure ?? null;
38644
+ } catch {
38645
+ failed = null;
38646
+ }
38647
+ if (ended) {
38648
+ this.accessEnded(slot, selected, ended);
38649
+ return;
38650
+ }
38651
+ if (failed !== null) {
38652
+ this.openFailedLate(slot, selected, failed);
38653
+ return;
38654
+ }
38570
38655
  this.publishSlot(slot, { ...first, transport: null, live: null, openingProgress: null });
38571
38656
  }
38572
38657
  /** The adapter's own state, validated: `needs_unlock` only with its lock
@@ -38654,6 +38739,35 @@ var AppWebSignInSession = class {
38654
38739
  };
38655
38740
  stop();
38656
38741
  }
38742
+ /** A collection opened from its replica, whose log was then refused. The
38743
+ * same block and detail as a failed open; stopping it fences every page's
38744
+ * data ports. Local data, unsent edits included, stays on this device. */
38745
+ openFailedLate(slot, selected, error4) {
38746
+ if (slot.selected !== selected || !slot.usable) return;
38747
+ const { problem: problem4, detail } = webSignInProblem(error4);
38748
+ if (problem4 === "open_failed") {
38749
+ try {
38750
+ console.error("mdbase web sign-in open failed", detail.name, detail.code);
38751
+ } catch {
38752
+ }
38753
+ }
38754
+ slot.usable = false;
38755
+ this.publishSlot(slot, { status: "blocked", problem: problem4, problemDetail: detail, transport: null, live: null });
38756
+ const stop = () => {
38757
+ if (this.closing || this.options.signal.aborted || slot.selected !== selected || this.slots.get(slot.id) !== slot) return;
38758
+ if (this.pending) {
38759
+ void this.pending.catch(() => {
38760
+ }).then(stop);
38761
+ return;
38762
+ }
38763
+ void this.operation(async () => {
38764
+ await this.closeSlot(slot);
38765
+ this.publishSlot(slot, { status: "blocked", problem: problem4, problemDetail: detail });
38766
+ }, true).catch(() => {
38767
+ });
38768
+ };
38769
+ stop();
38770
+ }
38657
38771
  /** Live problem of an open collection: shown while it lasts, cleared after. */
38658
38772
  liveProblem(slot, selected) {
38659
38773
  let server = null;
@@ -38729,6 +38843,12 @@ var AppWebSignInSession = class {
38729
38843
  return this.getSnapshot();
38730
38844
  }, true);
38731
38845
  }
38846
+ /** The published list is the one this installation kept, shown by a
38847
+ * signed-in start; Connect's has not been read since. The owner calls
38848
+ * `refreshCollectionList` once, when that read can no longer delay a page. */
38849
+ collectionListCached() {
38850
+ return this.listCached;
38851
+ }
38732
38852
  /** Accepted COMPLETE inventory omission or terminal native/log refusal only.
38733
38853
  * Local Forget, failed GETs and transient transport failures do not qualify. */
38734
38854
  scopeWasRemoved(collectionId) {
@@ -39184,6 +39304,10 @@ var AppScopedSignInSession = class {
39184
39304
  await session.pollFileAccess(requestId);
39185
39305
  return this.getSnapshot();
39186
39306
  }
39307
+ /** Any of the sessions still shows the list it kept (a signed-in start). */
39308
+ collectionListCached() {
39309
+ return this.original.collectionListCached() || [...this.scoped.values()].some((actor) => actor.session.collectionListCached());
39310
+ }
39187
39311
  async refreshCollectionList() {
39188
39312
  await this.original.refreshCollectionList();
39189
39313
  for (const actor of this.scoped.values()) {
@@ -39919,6 +40043,7 @@ function createAppCollectionSql(source) {
39919
40043
  var PAUSED_AFTER_MS = 1e4;
39920
40044
  var BACKGROUND_FLUSH_MS = 12e4;
39921
40045
  var WARM_SYNC_MS = 2e3;
40046
+ var LOCAL_READ_MS = 5e3;
39922
40047
  var INITIAL_LOG_RETRY_MS = 15e3;
39923
40048
  var INITIAL_LOG_ATTEMPTS = 3;
39924
40049
  var AppWebCollectionError = class extends Error {
@@ -39959,6 +40084,10 @@ function createAppWebCollectionAdapter(source) {
39959
40084
  let assessment = "setup_required";
39960
40085
  let locked = null;
39961
40086
  let started = false;
40087
+ let logBound = false;
40088
+ let early = null;
40089
+ let failure2 = null;
40090
+ let keys2 = "settled";
39962
40091
  let push = null, paused = false, pausing = null, kicked = false;
39963
40092
  let ended = null;
39964
40093
  const attached = /* @__PURE__ */ new Map();
@@ -40119,6 +40248,79 @@ function createAppWebCollectionAdapter(source) {
40119
40248
  return false;
40120
40249
  }
40121
40250
  };
40251
+ const startLog = () => host.startCollectionLog({ signal: life.signal, fetch: options.fetch, allowLoopbackHttp: options.allowLoopbackHttp });
40252
+ const openPush = () => {
40253
+ try {
40254
+ push = typeof host.openLogPush === "function" ? host.openLogPush({
40255
+ push: (frame) => {
40256
+ if (!current2()) return;
40257
+ host.pushLog(frame);
40258
+ kick();
40259
+ },
40260
+ live: onLive,
40261
+ // Access ended: the replica has the `closed` push; drive so it
40262
+ // settles, then the session stops this collection in every tab.
40263
+ ended: (reason) => {
40264
+ ended = reason;
40265
+ kick();
40266
+ onLive(false);
40267
+ settleInitial?.();
40268
+ changed();
40269
+ }
40270
+ }) : null;
40271
+ } catch {
40272
+ push = null;
40273
+ }
40274
+ if (push) {
40275
+ push.start();
40276
+ onLive(false);
40277
+ } else setPaused(true);
40278
+ };
40279
+ const bindLog = async () => {
40280
+ const first = early;
40281
+ early = null;
40282
+ await (first ?? startLog());
40283
+ check3();
40284
+ logBound = true;
40285
+ openPush();
40286
+ };
40287
+ const settleKeys = () => {
40288
+ if (keys2 !== "pending" || !logBound) return;
40289
+ try {
40290
+ keys2 = host.keyringSettled?.() ?? "settled";
40291
+ } catch {
40292
+ return;
40293
+ }
40294
+ if (keys2 === "failed") refuse(new AppCloudCopyHostError("recovery_required"));
40295
+ };
40296
+ const refuse = (error4) => {
40297
+ failure2 = error4;
40298
+ started = false;
40299
+ if (timer !== null) clearTimeout(timer);
40300
+ timer = null;
40301
+ changed();
40302
+ };
40303
+ const bindFailed = (error4) => {
40304
+ let server = null;
40305
+ try {
40306
+ server = error4.server;
40307
+ } catch {
40308
+ server = null;
40309
+ }
40310
+ const outcome = isAppServerProblem(server) && appServerOutcome(server) !== null ? server : null;
40311
+ const foreign = !isAppServerProblem(server) && error4 instanceof AppCpAuthorityError && error4.reason === "response";
40312
+ const next = outcome ? null : foreign ? { kind: "unreachable" } : appLogHealth(error4);
40313
+ if (!outcome && (next === null || next.kind === "auth")) {
40314
+ refuse(error4);
40315
+ return;
40316
+ }
40317
+ unhealthy = next ? unhealthy + 1 : 0;
40318
+ if (outcome !== problem4 || JSON.stringify(next) !== JSON.stringify(health)) {
40319
+ problem4 = outcome;
40320
+ health = next;
40321
+ changed();
40322
+ }
40323
+ };
40122
40324
  const schedule = () => {
40123
40325
  if (!started || !current2()) return;
40124
40326
  if (!foreground && !flushing() && !kicked) {
@@ -40134,13 +40336,16 @@ function createAppWebCollectionAdapter(source) {
40134
40336
  if (!current2()) return;
40135
40337
  const wake = foreground && Date.now() >= wakeAt;
40136
40338
  if (wake) wakeAt = Number.POSITIVE_INFINITY;
40137
- const work2 = driving = wake ? host.wakeCollectionLog({ signal: life.signal }) : host.driveCollection({ signal: life.signal });
40339
+ const work2 = driving = !logBound ? bindLog().then(() => host.driveCollection({ signal: life.signal })) : wake ? host.wakeCollectionLog({ signal: life.signal }) : host.driveCollection({ signal: life.signal });
40138
40340
  void work2.then(
40139
40341
  () => {
40140
40342
  if (driving === work2) driving = null;
40141
40343
  failures = 0;
40142
40344
  turned = true;
40143
- if (current2()) observe();
40345
+ if (current2()) {
40346
+ observe();
40347
+ settleKeys();
40348
+ }
40144
40349
  settleInitial?.();
40145
40350
  schedule();
40146
40351
  },
@@ -40149,7 +40354,12 @@ function createAppWebCollectionAdapter(source) {
40149
40354
  (error4) => {
40150
40355
  if (driving === work2) driving = null;
40151
40356
  failures++;
40152
- if (current2()) observe();
40357
+ if (current2()) {
40358
+ if (logBound) {
40359
+ observe();
40360
+ settleKeys();
40361
+ } else bindFailed(error4);
40362
+ }
40153
40363
  turnFailed(error4);
40154
40364
  settleInitial?.();
40155
40365
  schedule();
@@ -40301,24 +40511,40 @@ function createAppWebCollectionAdapter(source) {
40301
40511
  report("opening_store");
40302
40512
  await host.openCollectionSql({ signal: life.signal, replicaId: receipt3.deviceId, endpoint: 0, openSql: openLedgerSql });
40303
40513
  check3();
40304
- report("connecting_log");
40305
- const retryUntil = Date.now() + INITIAL_LOG_RETRY_MS;
40306
- for (let attempt = 0; ; attempt++) {
40307
- try {
40308
- await host.startCollectionLog({ signal: life.signal, fetch: options.fetch, allowLoopbackHttp: options.allowLoopbackHttp });
40309
- check3();
40310
- break;
40311
- } catch (error4) {
40312
- check3();
40313
- const server = error4 instanceof AppCpAuthorityError && error4.reason === "unavailable" ? error4.server : null;
40314
- if (!isAppServerProblem(server) || server.status !== 503 || server.code !== "busy" || appServerOutcome(server) !== null || attempt + 1 >= INITIAL_LOG_ATTEMPTS) throw error4;
40315
- const wait2 = server.retryAfterMs ?? 1e3 * 2 ** attempt;
40316
- if (Date.now() + wait2 >= retryUntil) throw error4;
40317
- await waitForOpenRetry(wait2, life.signal);
40318
- check3();
40319
- if (Date.now() >= retryUntil) throw error4;
40320
- }
40514
+ let holdsLog = true;
40515
+ try {
40516
+ holdsLog = host.holdsLog?.() ?? true;
40517
+ } catch {
40518
+ holdsLog = false;
40321
40519
  }
40520
+ const localFirst = !isPrivate && holdsLog && await ledger.syncedBefore({ signal: life.signal });
40521
+ check3();
40522
+ const connectLog = async () => {
40523
+ report("connecting_log");
40524
+ const retryUntil = Date.now() + INITIAL_LOG_RETRY_MS;
40525
+ for (let attempt = 0; ; attempt++) {
40526
+ try {
40527
+ await startLog();
40528
+ check3();
40529
+ break;
40530
+ } catch (error4) {
40531
+ check3();
40532
+ const server = error4 instanceof AppCpAuthorityError && error4.reason === "unavailable" ? error4.server : null;
40533
+ if (!isAppServerProblem(server) || server.status !== 503 || server.code !== "busy" || appServerOutcome(server) !== null || attempt + 1 >= INITIAL_LOG_ATTEMPTS) throw error4;
40534
+ const wait2 = server.retryAfterMs ?? 1e3 * 2 ** attempt;
40535
+ if (Date.now() + wait2 >= retryUntil) throw error4;
40536
+ await waitForOpenRetry(wait2, life.signal);
40537
+ check3();
40538
+ if (Date.now() >= retryUntil) throw error4;
40539
+ }
40540
+ }
40541
+ logBound = true;
40542
+ };
40543
+ if (localFirst) {
40544
+ early = startLog();
40545
+ void early.catch(() => {
40546
+ });
40547
+ } else await connectLog();
40322
40548
  const generation = crypto.randomUUID();
40323
40549
  let attachOpened = null;
40324
40550
  let detachOpened = null;
@@ -40333,44 +40559,48 @@ function createAppWebCollectionAdapter(source) {
40333
40559
  }
40334
40560
  }
40335
40561
  const finishOpen = async () => {
40336
- const warm = await ledger.syncedBefore({ signal: life.signal });
40337
- check3();
40338
- report("syncing");
40339
- await host.waitForVerifiedRead({ signal: life.signal, cold: !warm, onProgress: report });
40562
+ const warm = isPrivate ? await ledger.syncedBefore({ signal: life.signal }) : localFirst;
40340
40563
  check3();
40341
- started = true;
40342
- host.onLogWork?.(kick);
40343
- schedule();
40344
- try {
40345
- push = typeof host.openLogPush === "function" ? host.openLogPush({
40346
- push: (frame) => {
40347
- if (!current2()) return;
40348
- host.pushLog(frame);
40349
- kick();
40350
- },
40351
- live: onLive,
40352
- // Access ended: the replica has the `closed` push; drive so it
40353
- // settles, then the session stops this collection in every tab.
40354
- ended: (reason) => {
40355
- ended = reason;
40356
- kick();
40357
- onLive(false);
40358
- settleInitial?.();
40359
- changed();
40360
- }
40361
- }) : null;
40362
- } catch {
40363
- push = null;
40564
+ let local = false;
40565
+ if (localFirst && !logBound) {
40566
+ try {
40567
+ await host.waitForVerifiedRead({ signal: life.signal, local: true, timeoutMs: LOCAL_READ_MS, onProgress: report });
40568
+ check3();
40569
+ local = true;
40570
+ } catch {
40571
+ check3();
40572
+ const first = early;
40573
+ early = null;
40574
+ const bound = first ? await first.then(() => true, () => false) : false;
40575
+ check3();
40576
+ if (bound) logBound = true;
40577
+ else await connectLog();
40578
+ }
40364
40579
  }
40365
- if (push) {
40366
- push.start();
40367
- onLive(false);
40368
- } else paused = true;
40369
- if (warm) {
40580
+ if (!local) {
40370
40581
  report("syncing");
40371
- await waitForWarmSync();
40582
+ await host.waitForVerifiedRead({ signal: life.signal, cold: !warm, onProgress: report });
40583
+ check3();
40584
+ }
40585
+ started = true;
40586
+ host.onLogWork?.(kick);
40587
+ if (local) {
40588
+ keys2 = "pending";
40589
+ kicked = true;
40590
+ schedule();
40591
+ pausing = setTimeout(() => {
40592
+ pausing = null;
40593
+ if (current2() && !push?.live) setPaused(true);
40594
+ }, PAUSED_AFTER_MS);
40595
+ } else {
40596
+ schedule();
40597
+ openPush();
40598
+ if (warm) {
40599
+ report("syncing");
40600
+ await waitForWarmSync();
40601
+ }
40602
+ check3();
40372
40603
  }
40373
- check3();
40374
40604
  report("checking_setup");
40375
40605
  void ledger.markSynced({ signal: life.signal }).catch(() => {
40376
40606
  });
@@ -40557,6 +40787,9 @@ function createAppWebCollectionAdapter(source) {
40557
40787
  get ended() {
40558
40788
  return current2() ? ended : null;
40559
40789
  },
40790
+ get failure() {
40791
+ return current2() ? failure2 : null;
40792
+ },
40560
40793
  connection,
40561
40794
  close,
40562
40795
  subscribe(listener) {
@@ -41280,6 +41513,8 @@ function wipeUnlockSecret(request3) {
41280
41513
  }
41281
41514
  }
41282
41515
  var APPLICATION_WORKER_MAX_CLIENTS = 32;
41516
+ var LIST_CONFIRM_MS = 2e3;
41517
+ var LIST_CONFIRM_TRIES = 3;
41283
41518
  var ApplicationWorkerHost = class {
41284
41519
  session;
41285
41520
  lifetime = new AbortController();
@@ -41293,6 +41528,10 @@ var ApplicationWorkerHost = class {
41293
41528
  sessionForeground = null;
41294
41529
  collecting = false;
41295
41530
  revalidationQueued = false;
41531
+ /** A signed-in start showed the kept collection list; Connect's is read once. */
41532
+ listConfirm = "idle";
41533
+ listTimer = null;
41534
+ listAttempts = 0;
41296
41535
  nativeRpc = 0;
41297
41536
  nativeWait = null;
41298
41537
  /** The latest proxy portal's person-closed report (page `approvalClosed`). */
@@ -41697,18 +41936,55 @@ var ApplicationWorkerHost = class {
41697
41936
  /** One inventory read, in the SAME queue as lifecycle actions. Visibility and
41698
41937
  * native transport wake immediately; metadata cannot make a tab's start/select
41699
41938
  * fail busy or overlap an original consent/receipt operation. */
41700
- revalidateForeground() {
41939
+ revalidateForeground(shown = true) {
41701
41940
  if (this.stopping || this.revalidationQueued) return;
41702
41941
  this.revalidationQueued = true;
41703
41942
  this.queue.push({ client: null, request: null, job: async () => {
41704
41943
  try {
41705
- if (this.foreground && this.session.getSnapshot().signIn?.state === "paired") await this.session.refreshCollectionList();
41944
+ if ((this.foreground || !shown) && this.session.getSnapshot().signIn?.state === "paired") await this.session.refreshCollectionList();
41706
41945
  } finally {
41707
41946
  this.revalidationQueued = false;
41708
41947
  }
41709
41948
  } });
41710
41949
  this.pump();
41711
41950
  }
41951
+ /** A signed-in start published the collection list this installation kept
41952
+ * and did not wait for Connect. Read Connect's list once, in the same queue,
41953
+ * where it cannot delay the page: after the page's next request (opening its
41954
+ * remembered collection), at once when a collection is already open, or
41955
+ * shortly after when no request comes (the chooser is showing). */
41956
+ confirmListing(after) {
41957
+ if (this.stopping || this.listConfirm === "done") return;
41958
+ if (after === "start") {
41959
+ if (this.listConfirm !== "idle" || !this.session.collectionListCached()) return;
41960
+ this.listConfirm = "armed";
41961
+ if (this.session.heldCollections().length === 0) {
41962
+ this.listTimer = setTimeout(() => {
41963
+ this.listTimer = null;
41964
+ this.confirmListing("timer");
41965
+ }, LIST_CONFIRM_MS);
41966
+ return;
41967
+ }
41968
+ } else if (this.listConfirm !== "armed") return;
41969
+ if (this.listTimer !== null) clearTimeout(this.listTimer);
41970
+ this.listTimer = null;
41971
+ this.listConfirm = "done";
41972
+ if (!this.session.collectionListCached()) return;
41973
+ const attempt = ++this.listAttempts;
41974
+ this.queue.push({ client: null, request: null, job: async () => {
41975
+ try {
41976
+ if (this.session.getSnapshot().signIn?.state === "paired") await this.session.refreshCollectionList();
41977
+ } catch {
41978
+ }
41979
+ if (this.stopping || !this.session.collectionListCached() || attempt >= LIST_CONFIRM_TRIES) return;
41980
+ this.listConfirm = "armed";
41981
+ this.listTimer = setTimeout(() => {
41982
+ this.listTimer = null;
41983
+ this.confirmListing("timer");
41984
+ }, LIST_CONFIRM_MS);
41985
+ } });
41986
+ this.pump();
41987
+ }
41712
41988
  /** `wake`: a page reported visible/focus/online, so a visible connection
41713
41989
  * syncs now even if it was already in the foreground. `report`: a tab told
41714
41990
  * us, so the session hears it even when the aggregate did not change. */
@@ -41848,6 +42124,7 @@ var ApplicationWorkerHost = class {
41848
42124
  this.active = null;
41849
42125
  this.reconcile();
41850
42126
  this.applyForeground();
42127
+ if (next.client !== null) this.confirmListing(next.request[3] === "start" ? "start" : "request");
41851
42128
  this.pump();
41852
42129
  }
41853
42130
  };
@@ -41992,6 +42269,8 @@ var ApplicationWorkerHost = class {
41992
42269
  * original SDK leases; a subsequent call returns the SAME rejection. */
41993
42270
  stop(detach = true) {
41994
42271
  if (this.stopping) return this.stopping;
42272
+ if (this.listTimer !== null) clearTimeout(this.listTimer);
42273
+ this.listTimer = null;
41995
42274
  const pending = this.pending;
41996
42275
  for (const item of this.queue) wipeUnlockSecret(item.request);
41997
42276
  this.queue.length = 0;