| 842 | // share one in-flight promise. The resolved handle is cached |
| 843 | // until close() or the backend's `closed` promise fires. |
| 844 | #handleFor(id: string): Promise<BackendHandle> { |
| 845 | const cached = this.#handles.get(id); |
| 846 | if (cached !== undefined) return Promise.resolve(cached); |
| 847 | const inflight = this.#connecting.get(id); |
| 848 | if (inflight !== undefined) return inflight; |
| 849 | const backend = this.#backendsById.get(id); |
| 850 | if (backend === undefined) { |
| 851 | return Promise.reject(new Error(`Workspace: no backend with id ${JSON.stringify(id)}`)); |
| 852 | } |
| 853 | const generation = this.#connectionGeneration; |
| 854 | let promise!: Promise<BackendHandle>; |
| 855 | promise = (async () => { |
| 856 | const handle = await withSpan( |
| 857 | this.#observer, |
| 858 | "workspace.connect", |
| 859 | { "workspace.backend.id": id, "workspace.backend.type": backend.type }, |
| 860 | () => |
| 861 | backend.connect({ |
| 862 | db: this.#db, |
| 863 | waitUntil: this.#waitUntil, |
| 864 | fs: this.#fs, |
| 865 | git: this.#gitFactory ? this.git : DISABLED_GIT_CLIENT, |
| 866 | artifacts: this.#artifacts, |
| 867 | }), |
| 868 | ); |
| 869 | if (generation !== this.#connectionGeneration) { |
| 870 | await handle.close().catch(() => undefined); |
| 871 | throw new Error(`Workspace closed while backend ${JSON.stringify(id)} was connecting.`); |
| 872 | } |
| 873 | // Reconcile watermarks before publishing the handle. If the |
| 874 | // remote restarted between our pushes / fetches it has lost |
| 875 | // state we thought it had; reset the local cursors so the |
| 876 | // next tick rebaselines. |
| 877 | // |
| 878 | // A backend that declares sync: "none" has no remote store |
| 879 | // to reconcile against; skip the pass entirely. |
| 880 | if (handle.sync !== "none") { |
| 881 | await reconcileWatermarks(this.#db, handle.rpc.sync, id); |
| 882 | } |
| 883 | if (generation !== this.#connectionGeneration) { |
| 884 | await handle.close().catch(() => undefined); |
| 885 | throw new Error(`Workspace closed while backend ${JSON.stringify(id)} was connecting.`); |
| 886 | } |
| 887 | this.#handles.set(id, handle); |
| 888 | // Watch the transport for mid-session loss. Backends without |
| 889 | // a `closed` promise (in-process fakes) opt out by omitting |
| 890 | // it; we only react when it's wired. |
| 891 | if (handle.closed) { |
| 892 | handle.closed |
| 893 | .catch(() => {}) |
| 894 | .then(() => { |
| 895 | // Only clear if this handle is still the current one |
| 896 | // for this id. A close() that already ran will have |
| 897 | // dropped the entry; a subsequent #handleFor may have |
| 898 | // installed a new one. |
| 899 | if (this.#handles.get(id) === handle) { |
| 900 | this.#handles.delete(id); |
| 901 | this.#shells.delete(id); |