@neta-art/cohub 12.5.0 → 12.6.0

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.
@@ -1,5 +1,5 @@
1
1
  import { S as getRealtimeSpaceRoom, c as HttpError, l as HttpTransport, s as isHttpErrorCode, x as getRealtimeBoardRoom, z as isUuid } from "./http-error.js";
2
- import { l as resolveExecutionToken, o as resolveApiBaseUrl } from "./environment.js";
2
+ import { s as resolveApiBaseUrl, u as resolveExecutionToken } from "./environment.js";
3
3
  import { z } from "zod";
4
4
  //#region ../protocol/dist/board-constants.js
5
5
  const BOARD_ARROW_STROKE_SIZE = 2.5;
@@ -1447,7 +1447,7 @@ var CronJobsApi = class {
1447
1447
  //#region src/apis/generations.ts
1448
1448
  const DEFAULT_INTERVAL_MS = 1500;
1449
1449
  const DEFAULT_TIMEOUT_MS = 18e5;
1450
- function sleep$1(ms, signal) {
1450
+ function sleep$2(ms, signal) {
1451
1451
  if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Generation wait aborted"));
1452
1452
  return new Promise((resolve, reject) => {
1453
1453
  const cleanup = () => signal?.removeEventListener("abort", onAbort);
@@ -1512,7 +1512,7 @@ var GenerationsApi = class {
1512
1512
  }
1513
1513
  const remainingMs = timeoutMs - (Date.now() - startedAt);
1514
1514
  if (remainingMs <= 0) throw new Error(`Generation task timed out after ${timeoutMs}ms`);
1515
- await sleep$1(Math.min(intervalMs, remainingMs), options.signal);
1515
+ await sleep$2(Math.min(intervalMs, remainingMs), options.signal);
1516
1516
  }
1517
1517
  }
1518
1518
  async createAndWait(request, options) {
@@ -1895,7 +1895,7 @@ var BoardRealtimeClient = class {
1895
1895
  if (!this.websocketClient) throw new Error("realtime transport is not configured for this client");
1896
1896
  ensureRealtimeConnected(this.websocketClient);
1897
1897
  const room = getRealtimeBoardRoom(this.boardId);
1898
- const releaseRoom = this.websocketClient.retainRooms([getRealtimeSpaceRoom(this.spaceId), room]);
1898
+ const releaseRoom = this.websocketClient.retainRooms([room]);
1899
1899
  const unsubscribe = this.websocketClient.on("event", (event) => {
1900
1900
  if (event.spaceId !== this.spaceId) return;
1901
1901
  if (event.type === "board.changed" && event.payload.boardId === this.boardId) {
@@ -1917,12 +1917,14 @@ var BoardRealtimeClient = class {
1917
1917
  }
1918
1918
  });
1919
1919
  const resync = handlers.resync;
1920
- const stopResync = resync ? this.websocketClient.on("subscribed", ({ rooms }) => {
1920
+ const stopResync = resync ? [this.websocketClient.on("subscribed", ({ rooms }) => {
1921
1921
  if (rooms.includes(room)) resync();
1922
- }) : null;
1922
+ }), this.websocketClient.on("subscribeError", ({ rejected }) => {
1923
+ if (rejected.some((entry) => entry.room === room && entry.code !== "UNAVAILABLE")) resync();
1924
+ })] : [];
1923
1925
  return () => {
1924
1926
  unsubscribe();
1925
- stopResync?.();
1927
+ for (const stop of stopResync) stop();
1926
1928
  releaseRoom();
1927
1929
  };
1928
1930
  }
@@ -1951,7 +1953,7 @@ var BoardClient = class {
1951
1953
  /** Read the Board, or part of it. Every value is complete, defaults included. */
1952
1954
  get(input = {}, customFetch) {
1953
1955
  const params = new URLSearchParams();
1954
- if (input.only?.length) params.set("only", input.only.join(","));
1956
+ if (input.only) params.set("only", input.only.join(","));
1955
1957
  if (input.rect) params.set("rect", [
1956
1958
  input.rect.x,
1957
1959
  input.rect.y,
@@ -2088,6 +2090,19 @@ var SpaceBoardsApi = class {
2088
2090
  boardId
2089
2091
  });
2090
2092
  }
2093
+ assetSource() {
2094
+ return {
2095
+ resolveFileUrl: (path, options) => this.files.resolveUrl(path, {
2096
+ purpose: "preview",
2097
+ fresh: options?.fresh
2098
+ }),
2099
+ resolvePlaybackUrl: (path, options) => this.files.resolveUrl(path, {
2100
+ purpose: "playback",
2101
+ fresh: options?.fresh
2102
+ }),
2103
+ subscribe: (listener) => this.files.onUrlChange(listener)
2104
+ };
2105
+ }
2091
2106
  async open(target, customFetch) {
2092
2107
  return this.byId(isBoardPath(target) ? await this.resolveId(target, customFetch) : target);
2093
2108
  }
@@ -3000,7 +3015,7 @@ function getPatchMessageIdentity(event) {
3000
3015
  messageOrdinal: typeof event.payload.messageOrdinal === "number" ? event.payload.messageOrdinal : null
3001
3016
  };
3002
3017
  }
3003
- async function withTimeout(promise, timeoutMs) {
3018
+ async function withTimeout$1(promise, timeoutMs) {
3004
3019
  let timeout = null;
3005
3020
  try {
3006
3021
  return await Promise.race([promise, new Promise((_, reject) => {
@@ -3169,7 +3184,7 @@ var SessionGenerationStreamClient = class SessionGenerationStreamClient {
3169
3184
  async loadSnapshot() {
3170
3185
  if (!this.fetchStreamSnapshot) return null;
3171
3186
  try {
3172
- return (await withTimeout(this.fetchStreamSnapshot(), SNAPSHOT_RECOVERY_TIMEOUT_MS)).snapshot;
3187
+ return (await withTimeout$1(this.fetchStreamSnapshot(), SNAPSHOT_RECOVERY_TIMEOUT_MS)).snapshot;
3173
3188
  } catch (error) {
3174
3189
  console.warn("[SessionGenerationStreamClient] Failed to recover stream snapshot:", error);
3175
3190
  return null;
@@ -3543,6 +3558,631 @@ function buildSpaceInvitePath(input) {
3543
3558
  return `${buildSpacePath(input)}/join/${inviteCode}`;
3544
3559
  }
3545
3560
  //#endregion
3561
+ //#region src/apis/space-file-reads.ts
3562
+ const BATCH_SIZE = 32;
3563
+ const BATCH_CONCURRENCY = 2;
3564
+ const SINGLE_CONCURRENCY = 4;
3565
+ const BACKGROUND_DELAY_MS = 1500;
3566
+ const READ_TIMEOUT_MS = 3e4;
3567
+ function createLimiter(limit) {
3568
+ let active = 0;
3569
+ const lanes = [[], []];
3570
+ function pump() {
3571
+ while (active < limit) {
3572
+ const job = lanes[0].shift() ?? lanes[1].shift();
3573
+ if (!job) return;
3574
+ active += 1;
3575
+ job();
3576
+ }
3577
+ }
3578
+ return (run, background = false) => new Promise((resolve, reject) => {
3579
+ lanes[background ? 1 : 0].push(() => {
3580
+ run().then(resolve, reject).finally(() => {
3581
+ active -= 1;
3582
+ pump();
3583
+ });
3584
+ });
3585
+ pump();
3586
+ });
3587
+ }
3588
+ const fileReadPath = (spaceId, path) => `/api/spaces/${spaceId}/fs/file?${new URLSearchParams({ path })}`;
3589
+ function createSpaceFileReads(transport) {
3590
+ const queues = [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()];
3591
+ const timers = [null, null];
3592
+ const inFlight = /* @__PURE__ */ new Map();
3593
+ const batches = createLimiter(BATCH_CONCURRENCY);
3594
+ const singles = createLimiter(SINGLE_CONCURRENCY);
3595
+ const readOne = (spaceId, path, init = {}) => withTimeout((signal) => transport.requestRevalidated(fileReadPath(spaceId, path), {
3596
+ ...init,
3597
+ signal
3598
+ }));
3599
+ function schedule(lane) {
3600
+ timers[lane] ??= setTimeout(() => {
3601
+ timers[lane] = null;
3602
+ flush(lane);
3603
+ }, lane ? BACKGROUND_DELAY_MS : 0);
3604
+ }
3605
+ function settle(pending, outcome) {
3606
+ if (inFlight.get(pending.key) === pending) inFlight.delete(pending.key);
3607
+ if ("file" in outcome) pending.resolve(outcome.file);
3608
+ else pending.reject(outcome.error);
3609
+ }
3610
+ function flush(lane) {
3611
+ const groups = /* @__PURE__ */ new Map();
3612
+ for (const pending of queues[lane].values()) {
3613
+ inFlight.set(pending.key, pending);
3614
+ const id = `${pending.token}\u0000${pending.spaceId}`;
3615
+ const group = groups.get(id);
3616
+ if (group) group.push(pending);
3617
+ else groups.set(id, [pending]);
3618
+ }
3619
+ queues[lane].clear();
3620
+ for (const group of groups.values()) {
3621
+ if (group.length === 1) {
3622
+ readSingle(group[0], lane === 1);
3623
+ continue;
3624
+ }
3625
+ for (let index = 0; index < group.length; index += BATCH_SIZE) batches(() => readBatch(group.slice(index, index + BATCH_SIZE)), lane === 1);
3626
+ }
3627
+ }
3628
+ async function readSingle(pending, background = false) {
3629
+ const { spaceId, path, token } = pending;
3630
+ try {
3631
+ settle(pending, { file: await singles(() => readOne(spaceId, path, { accessToken: token }), background) });
3632
+ } catch (error) {
3633
+ settle(pending, { error });
3634
+ }
3635
+ }
3636
+ async function readBatch(chunk) {
3637
+ const { spaceId, token } = chunk[0];
3638
+ let response;
3639
+ try {
3640
+ response = await withTimeout((signal) => transport.request(`/api/spaces/${spaceId}/fs/files`, {
3641
+ method: "POST",
3642
+ headers: { "Content-Type": "application/json" },
3643
+ body: JSON.stringify({ paths: chunk.map((pending) => pending.path) }),
3644
+ accessToken: token,
3645
+ signal
3646
+ }));
3647
+ } catch {
3648
+ await Promise.all(chunk.map((pending) => readSingle(pending)));
3649
+ return;
3650
+ }
3651
+ const byPath = new Map(chunk.map((pending) => [pending.path, pending]));
3652
+ const take = (path) => {
3653
+ const pending = byPath.get(path);
3654
+ byPath.delete(path);
3655
+ return pending;
3656
+ };
3657
+ for (const file of [...response.files, ...response.preparing ?? []]) {
3658
+ const pending = take(file.path);
3659
+ if (pending) settle(pending, { file });
3660
+ }
3661
+ for (const { path, status, code, message } of response.errors) {
3662
+ const pending = take(path);
3663
+ if (pending) settle(pending, { error: new HttpError(message, status, {
3664
+ code,
3665
+ message
3666
+ }) });
3667
+ }
3668
+ await Promise.all([...byPath.values()].map((pending) => readSingle(pending)));
3669
+ }
3670
+ function share(token, spaceId, path, background) {
3671
+ const key = `${spaceId}\u0000${path}\u0000${token}`;
3672
+ let pending = inFlight.get(key) ?? queues[0].get(key);
3673
+ const deferred = queues[1].get(key);
3674
+ if (!pending && deferred && !background) {
3675
+ queues[1].delete(key);
3676
+ queues[0].set(key, deferred);
3677
+ schedule(0);
3678
+ }
3679
+ pending ??= deferred;
3680
+ if (!pending) {
3681
+ let resolve;
3682
+ let reject;
3683
+ const promise = new Promise((done, fail) => {
3684
+ resolve = done;
3685
+ reject = fail;
3686
+ });
3687
+ const lane = background ? 1 : 0;
3688
+ pending = {
3689
+ key,
3690
+ token,
3691
+ spaceId,
3692
+ path,
3693
+ promise,
3694
+ resolve,
3695
+ reject
3696
+ };
3697
+ queues[lane].set(key, pending);
3698
+ schedule(lane);
3699
+ }
3700
+ return pending.promise;
3701
+ }
3702
+ return {
3703
+ async read(spaceId, path, options = {}) {
3704
+ const own = options.token === void 0;
3705
+ const token = own ? await transport.credential() : options.token ?? null;
3706
+ try {
3707
+ return await abortable(share(token, spaceId, path, options.background === true), options.signal);
3708
+ } catch (error) {
3709
+ if (!own || !isUnauthorized(error)) throw error;
3710
+ return abortable(readOne(spaceId, path), options.signal);
3711
+ }
3712
+ },
3713
+ invalidate(spaceId, paths) {
3714
+ const stale = new Set([...paths].map((path) => `${spaceId}\u0000${path}`));
3715
+ for (const key of inFlight.keys()) if (stale.has(key.slice(0, key.lastIndexOf("\0")))) inFlight.delete(key);
3716
+ }
3717
+ };
3718
+ }
3719
+ const isUnauthorized = (error) => error instanceof HttpError && error.status === 401;
3720
+ async function withTimeout(run) {
3721
+ const controller = new AbortController();
3722
+ const timer = setTimeout(() => controller.abort(new DOMException("Space file read timed out", "TimeoutError")), READ_TIMEOUT_MS);
3723
+ try {
3724
+ return await run(controller.signal);
3725
+ } finally {
3726
+ clearTimeout(timer);
3727
+ }
3728
+ }
3729
+ function abortable(promise, signal) {
3730
+ if (!signal) return promise;
3731
+ if (signal.aborted) return Promise.reject(signal.reason);
3732
+ return new Promise((resolve, reject) => {
3733
+ const abort = () => reject(signal.reason);
3734
+ signal.addEventListener("abort", abort, { once: true });
3735
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
3736
+ });
3737
+ }
3738
+ //#endregion
3739
+ //#region src/indexeddb.ts
3740
+ const OPEN_TIMEOUT_MS = 3e3;
3741
+ const connections = /* @__PURE__ */ new Map();
3742
+ function open(name, version, upgrade) {
3743
+ const existing = connections.get(name);
3744
+ if (existing) return existing;
3745
+ const opening = new Promise((resolve, reject) => {
3746
+ const factory = globalThis.indexedDB;
3747
+ if (!factory) {
3748
+ reject(/* @__PURE__ */ new Error("IndexedDB is unavailable."));
3749
+ return;
3750
+ }
3751
+ let late = false;
3752
+ const timer = setTimeout(() => {
3753
+ late = true;
3754
+ reject(/* @__PURE__ */ new Error("IndexedDB did not open in time."));
3755
+ }, OPEN_TIMEOUT_MS);
3756
+ const request = factory.open(name, version);
3757
+ request.onupgradeneeded = () => upgrade(request.result);
3758
+ request.onsuccess = () => {
3759
+ clearTimeout(timer);
3760
+ const db = request.result;
3761
+ if (late) {
3762
+ db.close();
3763
+ return;
3764
+ }
3765
+ const forget = () => {
3766
+ if (connections.get(name) === opening) connections.delete(name);
3767
+ };
3768
+ db.onversionchange = () => {
3769
+ db.close();
3770
+ forget();
3771
+ };
3772
+ db.onclose = forget;
3773
+ resolve(db);
3774
+ };
3775
+ request.onerror = () => {
3776
+ clearTimeout(timer);
3777
+ reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB failed to open."));
3778
+ };
3779
+ });
3780
+ connections.set(name, opening);
3781
+ opening.catch(() => {
3782
+ if (connections.get(name) === opening) connections.delete(name);
3783
+ });
3784
+ return opening;
3785
+ }
3786
+ function indexedDb(name, version, upgrade) {
3787
+ async function transact(stores, mode, work, retry = true) {
3788
+ const db = await open(name, version, upgrade);
3789
+ let tx;
3790
+ try {
3791
+ tx = db.transaction(stores, mode);
3792
+ } catch (error) {
3793
+ if (!retry) throw error;
3794
+ db.close();
3795
+ connections.delete(name);
3796
+ return transact(stores, mode, work, false);
3797
+ }
3798
+ return new Promise((resolve, reject) => {
3799
+ const result = work(tx);
3800
+ tx.oncomplete = () => resolve(result());
3801
+ tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
3802
+ tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted."));
3803
+ });
3804
+ }
3805
+ return { transact: (stores, mode, work) => transact(stores, mode, work) };
3806
+ }
3807
+ function deleteByIndex(store, index, key) {
3808
+ const request = store.index(index).openKeyCursor(IDBKeyRange.only(key));
3809
+ request.onsuccess = () => {
3810
+ const cursor = request.result;
3811
+ if (!cursor) return;
3812
+ store.delete(cursor.primaryKey);
3813
+ cursor.continue();
3814
+ };
3815
+ }
3816
+ //#endregion
3817
+ //#region src/apis/space-file-url-store.ts
3818
+ const DATABASE = "cohub-file-urls";
3819
+ const STORE = "urls";
3820
+ const MAX_ENTRIES = 2e4;
3821
+ const TRIM_TO = 18e3;
3822
+ const TRIM_EVERY_WRITES = 500;
3823
+ const database = indexedDb(DATABASE, 1, (db) => {
3824
+ const store = db.createObjectStore(STORE, { keyPath: "key" });
3825
+ store.createIndex("scope", "scope");
3826
+ store.createIndex("verifiedAt", "verifiedAt");
3827
+ });
3828
+ const recordKey = (scope, spaceId, path) => `${scope}\u0000${spaceId}\u0000${path}`;
3829
+ let writes = 0;
3830
+ const fileUrlStore = {
3831
+ read(scope, spaceId, paths) {
3832
+ return database.transact([STORE], "readonly", (tx) => {
3833
+ const store = tx.objectStore(STORE);
3834
+ const requests = paths.map((path) => [path, store.get(recordKey(scope, spaceId, path))]);
3835
+ return () => {
3836
+ const found = /* @__PURE__ */ new Map();
3837
+ for (const [path, request] of requests) {
3838
+ const record = request.result;
3839
+ if (record) found.set(path, {
3840
+ url: record.url,
3841
+ verifiedAt: record.verifiedAt
3842
+ });
3843
+ }
3844
+ return found;
3845
+ };
3846
+ });
3847
+ },
3848
+ async write(scope, spaceId, changes) {
3849
+ await database.transact([STORE], "readwrite", (tx) => {
3850
+ const store = tx.objectStore(STORE);
3851
+ for (const [path, { url, verifiedAt }] of changes.put) store.put({
3852
+ key: recordKey(scope, spaceId, path),
3853
+ scope,
3854
+ url,
3855
+ verifiedAt
3856
+ });
3857
+ for (const path of changes.remove) store.delete(recordKey(scope, spaceId, path));
3858
+ return () => void 0;
3859
+ });
3860
+ writes += changes.put.size;
3861
+ if (writes < TRIM_EVERY_WRITES) return;
3862
+ writes = 0;
3863
+ await trim();
3864
+ }
3865
+ };
3866
+ function trim() {
3867
+ return database.transact([STORE], "readwrite", (tx) => {
3868
+ const store = tx.objectStore(STORE);
3869
+ const count = store.count();
3870
+ count.onsuccess = () => {
3871
+ let excess = count.result > MAX_ENTRIES ? count.result - TRIM_TO : 0;
3872
+ if (excess === 0) return;
3873
+ const cursor = store.index("verifiedAt").openKeyCursor();
3874
+ cursor.onsuccess = () => {
3875
+ const current = cursor.result;
3876
+ if (!current || excess-- <= 0) return;
3877
+ store.delete(current.primaryKey);
3878
+ current.continue();
3879
+ };
3880
+ };
3881
+ return () => void 0;
3882
+ });
3883
+ }
3884
+ async function clearFileUrlCache(options = {}) {
3885
+ if (!globalThis.indexedDB) return;
3886
+ await database.transact([STORE], "readwrite", (tx) => {
3887
+ const store = tx.objectStore(STORE);
3888
+ if (options.scope === void 0) store.clear();
3889
+ else deleteByIndex(store, "scope", options.scope);
3890
+ return () => void 0;
3891
+ });
3892
+ }
3893
+ //#endregion
3894
+ //#region src/apis/space-file-urls.ts
3895
+ const MEMORY_LIMIT = 4096;
3896
+ const INLINE_MEMORY_MAX_CHARS = 262144;
3897
+ const PREPARING_CONCURRENCY = 4;
3898
+ const PREPARING_TIMEOUT_MS = 15e3;
3899
+ /** Well inside the fs CDN manifest's 6.5 days; CDN URLs carry the file version and are not signed. */
3900
+ const STORED_MAX_AGE_MS = 2592e5;
3901
+ const UNWATCHED_TRUST_MS = 6e4;
3902
+ function inlineFileDataUrl(file) {
3903
+ const mimeType = file.mimeType ?? "application/octet-stream";
3904
+ return file.encoding === "base64" ? `data:${mimeType};base64,${file.content}` : `data:${mimeType};charset=utf-8,${encodeURIComponent(file.content)}`;
3905
+ }
3906
+ function resolvedOf(file) {
3907
+ if (file.delivery === "url" && file.url) return {
3908
+ url: file.url,
3909
+ inline: false
3910
+ };
3911
+ return {
3912
+ url: inlineFileDataUrl(file),
3913
+ inline: true
3914
+ };
3915
+ }
3916
+ const isGone = (error) => error instanceof HttpError && (error.status === 404 || error.status === 410);
3917
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3918
+ function remember(map, key, value) {
3919
+ map.delete(key);
3920
+ map.set(key, value);
3921
+ if (map.size > MEMORY_LIMIT) map.delete(map.keys().next().value);
3922
+ }
3923
+ function createSpaceFileUrls(options) {
3924
+ const { reads, cache } = options;
3925
+ const now = options.now ?? Date.now;
3926
+ const memory = /* @__PURE__ */ new Map();
3927
+ const invalidatedAt = /* @__PURE__ */ new Map();
3928
+ const epochs = /* @__PURE__ */ new Map();
3929
+ const serverReads = /* @__PURE__ */ new Map();
3930
+ const localReads = /* @__PURE__ */ new Map();
3931
+ const listeners = /* @__PURE__ */ new Set();
3932
+ const preparing = createLimiter(PREPARING_CONCURRENCY);
3933
+ let localBatch = null;
3934
+ let writes = null;
3935
+ let lastScope = null;
3936
+ const keyOf = (spaceId, path) => `${spaceId}\u0000${path}`;
3937
+ const epochOf = (spaceId) => epochs.get(spaceId) ?? 0;
3938
+ const persists = (scope) => cache && scope !== null && Boolean(globalThis.indexedDB);
3939
+ function persist(scope, spaceId, path, url) {
3940
+ if (!writes) {
3941
+ const batch = /* @__PURE__ */ new Map();
3942
+ writes = batch;
3943
+ setTimeout(() => {
3944
+ writes = null;
3945
+ for (const [id, { put, remove }] of batch) {
3946
+ const [batchScope, batchSpace] = id.split("\0");
3947
+ fileUrlStore.write(batchScope, batchSpace, {
3948
+ put,
3949
+ remove
3950
+ }).catch(() => void 0);
3951
+ }
3952
+ }, 0);
3953
+ }
3954
+ const id = `${scope}\u0000${spaceId}`;
3955
+ const pending = writes.get(id) ?? {
3956
+ put: /* @__PURE__ */ new Map(),
3957
+ remove: /* @__PURE__ */ new Set()
3958
+ };
3959
+ writes.set(id, pending);
3960
+ if (url) {
3961
+ pending.remove.delete(path);
3962
+ pending.put.set(path, {
3963
+ url,
3964
+ verifiedAt: now()
3965
+ });
3966
+ } else {
3967
+ pending.put.delete(path);
3968
+ pending.remove.add(path);
3969
+ }
3970
+ }
3971
+ async function load(token, spaceId, path, background) {
3972
+ const first = await reads.read(spaceId, path, {
3973
+ background,
3974
+ token
3975
+ });
3976
+ if ("content" in first) return resolvedOf(first);
3977
+ return preparing(async () => {
3978
+ const deadline = now() + PREPARING_TIMEOUT_MS;
3979
+ let file = first;
3980
+ while (!("content" in file)) {
3981
+ const wait = Math.min(Math.max(250, Math.min(file.retryAfterMs, 2e3)), deadline - now());
3982
+ if (wait <= 0) return null;
3983
+ await sleep$1(wait);
3984
+ file = await reads.read(spaceId, path, { token });
3985
+ }
3986
+ return resolvedOf(file);
3987
+ }, background);
3988
+ }
3989
+ function server(token, scope, spaceId, path, background) {
3990
+ const key = keyOf(spaceId, path);
3991
+ const readKey = `${key}\u0000${token}`;
3992
+ const running = serverReads.get(readKey);
3993
+ if (running) {
3994
+ if (!background) reads.read(spaceId, path, { token }).catch(() => void 0);
3995
+ return running;
3996
+ }
3997
+ const epoch = epochOf(spaceId);
3998
+ const read = load(token, spaceId, path, background).then((resolved) => {
3999
+ const current = serverReads.get(readKey) === read;
4000
+ if (current) serverReads.delete(readKey);
4001
+ if (!resolved) return null;
4002
+ const entry = {
4003
+ ...resolved,
4004
+ scope,
4005
+ epoch,
4006
+ verifiedAt: now()
4007
+ };
4008
+ if (current && cache) adopt(key, spaceId, path, entry);
4009
+ return entry;
4010
+ }, (error) => {
4011
+ const current = serverReads.get(readKey) === read;
4012
+ if (current) serverReads.delete(readKey);
4013
+ if (current && cache && isGone(error)) {
4014
+ memory.delete(key);
4015
+ if (persists(scope)) persist(scope, spaceId, path, null);
4016
+ }
4017
+ throw error;
4018
+ });
4019
+ serverReads.set(readKey, read);
4020
+ return read;
4021
+ }
4022
+ function adopt(key, spaceId, path, entry) {
4023
+ const previous = memory.get(key);
4024
+ if (!entry.inline || entry.url.length <= INLINE_MEMORY_MAX_CHARS) remember(memory, key, entry);
4025
+ if (!entry.inline && persists(entry.scope)) persist(entry.scope, spaceId, path, entry.url);
4026
+ if (previous && previous.url !== entry.url) emit(spaceId, path);
4027
+ }
4028
+ function revalidate(token, scope, spaceId, path) {
4029
+ server(token, scope, spaceId, path, true).catch(() => void 0);
4030
+ }
4031
+ function readLocal(token, scope, spaceId, path) {
4032
+ const key = keyOf(spaceId, path);
4033
+ const running = localReads.get(key);
4034
+ if (running) return running;
4035
+ let resolve;
4036
+ const read = new Promise((done) => {
4037
+ resolve = done;
4038
+ });
4039
+ const forget = () => {
4040
+ if (localReads.get(key) === read) localReads.delete(key);
4041
+ };
4042
+ read.then(forget, forget);
4043
+ localReads.set(key, read);
4044
+ if (!localBatch) {
4045
+ const batch = [];
4046
+ localBatch = batch;
4047
+ queueMicrotask(() => {
4048
+ localBatch = null;
4049
+ const groups = /* @__PURE__ */ new Map();
4050
+ for (const item of batch) {
4051
+ const id = `${item.scope}\u0000${item.spaceId}`;
4052
+ const group = groups.get(id);
4053
+ if (group) group.push(item);
4054
+ else groups.set(id, [item]);
4055
+ }
4056
+ for (const group of groups.values()) readStored(group);
4057
+ });
4058
+ }
4059
+ localBatch.push({
4060
+ key,
4061
+ token,
4062
+ scope,
4063
+ spaceId,
4064
+ path,
4065
+ resolve
4066
+ });
4067
+ return read;
4068
+ }
4069
+ async function readStored(group) {
4070
+ const { scope, spaceId } = group[0];
4071
+ const records = await fileUrlStore.read(scope, spaceId, group.map((item) => item.path)).catch(() => /* @__PURE__ */ new Map());
4072
+ for (const { key, token, path, resolve } of group) {
4073
+ const record = records.get(path);
4074
+ if (!(record && now() - record.verifiedAt <= STORED_MAX_AGE_MS && (invalidatedAt.get(key) ?? -1) < record.verifiedAt)) {
4075
+ resolve(server(token, scope, spaceId, path, false));
4076
+ continue;
4077
+ }
4078
+ const entry = {
4079
+ url: record.url,
4080
+ inline: false,
4081
+ scope,
4082
+ epoch: -1,
4083
+ verifiedAt: record.verifiedAt
4084
+ };
4085
+ if (!memory.has(key)) remember(memory, key, entry);
4086
+ revalidate(token, scope, spaceId, path);
4087
+ resolve(Promise.resolve(entry));
4088
+ }
4089
+ }
4090
+ function emit(spaceId, path) {
4091
+ for (const listener of listeners) listener(spaceId, path);
4092
+ }
4093
+ function invalidate(spaceId, paths) {
4094
+ const stale = [...new Set(paths)];
4095
+ if (stale.length === 0) return;
4096
+ reads.invalidate(spaceId, stale);
4097
+ const at = now();
4098
+ const buffered = lastScope === null ? void 0 : writes?.get(`${lastScope}\u0000${spaceId}`);
4099
+ const keys = /* @__PURE__ */ new Set();
4100
+ for (const path of stale) {
4101
+ const key = keyOf(spaceId, path);
4102
+ keys.add(key);
4103
+ memory.delete(key);
4104
+ localReads.delete(key);
4105
+ remember(invalidatedAt, key, at);
4106
+ buffered?.put.delete(path);
4107
+ }
4108
+ for (const readKey of serverReads.keys()) if (keys.has(readKey.slice(0, readKey.lastIndexOf("\0")))) serverReads.delete(readKey);
4109
+ if (persists(lastScope)) fileUrlStore.write(lastScope, spaceId, {
4110
+ put: /* @__PURE__ */ new Map(),
4111
+ remove: stale
4112
+ }).catch(() => void 0);
4113
+ }
4114
+ return {
4115
+ async resolve(spaceId, path, request = {}) {
4116
+ const purpose = request.purpose ?? "preview";
4117
+ const token = await options.credential();
4118
+ const scope = cache ? await options.scope().catch(() => null) : null;
4119
+ if (scope !== null) lastScope = scope;
4120
+ const key = keyOf(spaceId, path);
4121
+ const hit = cache && !request.fresh ? memory.get(key) : void 0;
4122
+ let entry;
4123
+ if (hit && hit.scope === scope) {
4124
+ remember(memory, key, hit);
4125
+ const unwatched = !options.watched(spaceId) && now() - hit.verifiedAt > UNWATCHED_TRUST_MS;
4126
+ if (hit.epoch !== epochOf(spaceId) || unwatched) revalidate(token, scope, spaceId, path);
4127
+ entry = hit;
4128
+ } else entry = await settle((persists(scope) && !request.fresh ? readLocal(token, scope, spaceId, path) : server(token, scope, spaceId, path, false)).catch(async (error) => {
4129
+ if (!isUnauthorized(error)) throw error;
4130
+ await reads.read(spaceId, path, { signal: request.signal });
4131
+ request.signal?.throwIfAborted();
4132
+ return server(await options.credential(), scope, spaceId, path, false);
4133
+ }), request);
4134
+ return !entry || purpose === "playback" && entry.inline ? null : entry.url;
4135
+ },
4136
+ applyFsChange(spaceId, payload) {
4137
+ if (payload.resync) epochs.set(spaceId, epochOf(spaceId) + 1);
4138
+ const paths = /* @__PURE__ */ new Set();
4139
+ for (const change of payload.changes ?? []) {
4140
+ if (change.path) paths.add(change.path);
4141
+ if (change.oldPath) paths.add(change.oldPath);
4142
+ }
4143
+ invalidate(spaceId, paths);
4144
+ for (const path of paths) emit(spaceId, path);
4145
+ },
4146
+ subscribe(listener) {
4147
+ listeners.add(listener);
4148
+ return () => {
4149
+ listeners.delete(listener);
4150
+ };
4151
+ }
4152
+ };
4153
+ }
4154
+ function createSpaceFileServices(transport, websocket) {
4155
+ const reads = createSpaceFileReads(transport);
4156
+ const urls = createSpaceFileUrls({
4157
+ reads,
4158
+ credential: () => transport.credential(),
4159
+ scope: () => transport.cacheScope(),
4160
+ watched: (spaceId) => websocket?.isRoomLive(getRealtimeSpaceRoom(spaceId)) ?? false,
4161
+ cache: websocket !== null && typeof window !== "undefined"
4162
+ });
4163
+ websocket?.on("event", (event) => {
4164
+ if (event.type === "space.fs.changed" && event.spaceId) urls.applyFsChange(event.spaceId, event.payload);
4165
+ });
4166
+ return {
4167
+ reads,
4168
+ urls
4169
+ };
4170
+ }
4171
+ function settle(read, { timeoutMs = PREPARING_TIMEOUT_MS, signal }) {
4172
+ if (signal?.aborted) return Promise.reject(signal.reason);
4173
+ return new Promise((resolve, reject) => {
4174
+ const timer = setTimeout(() => finish(() => resolve(null)), Math.max(0, timeoutMs));
4175
+ const abort = () => finish(() => reject(signal?.reason));
4176
+ signal?.addEventListener("abort", abort, { once: true });
4177
+ function finish(done) {
4178
+ clearTimeout(timer);
4179
+ signal?.removeEventListener("abort", abort);
4180
+ done();
4181
+ }
4182
+ read.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
4183
+ });
4184
+ }
4185
+ //#endregion
3546
4186
  //#region src/apis/spaces.ts
3547
4187
  async function fetchTurnObject(url, fetchImpl = globalThis.fetch, signal) {
3548
4188
  const response = await fetchImpl(url, { signal });
@@ -3671,10 +4311,6 @@ var SpacesApi = class {
3671
4311
  });
3672
4312
  }
3673
4313
  };
3674
- function inlineFileDataUrl(file) {
3675
- const mimeType = file.mimeType ?? "application/octet-stream";
3676
- return file.encoding === "base64" ? `data:${mimeType};base64,${file.content}` : `data:${mimeType};charset=utf-8,${encodeURIComponent(file.content)}`;
3677
- }
3678
4314
  function spaceFileAbortReason(signal) {
3679
4315
  return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
3680
4316
  }
@@ -3748,22 +4384,34 @@ var SpacePublicFilesApi = class {
3748
4384
  var SpaceFilesApi = class {
3749
4385
  transport;
3750
4386
  spaceId;
3751
- constructor(transport, spaceId) {
4387
+ services;
4388
+ constructor(transport, spaceId, services) {
3752
4389
  this.transport = transport;
3753
4390
  this.spaceId = spaceId;
4391
+ this.services = services;
3754
4392
  }
3755
4393
  list(path = "", options) {
3756
4394
  return listDirectory(this.transport, `/api/spaces/${this.spaceId}/fs/tree`, path, options);
3757
4395
  }
3758
4396
  read(path, customFetch, signal) {
3759
- const params = new URLSearchParams({ path });
3760
- return this.transport.requestRevalidated(`/api/spaces/${this.spaceId}/fs/file?${params.toString()}`, {
4397
+ if (customFetch) return this.transport.requestRevalidated(fileReadPath(this.spaceId, path), {
3761
4398
  fetch: customFetch,
3762
4399
  signal
3763
4400
  });
4401
+ return this.services.reads.read(this.spaceId, path, { signal });
3764
4402
  }
3765
4403
  /** Resolve a browser-ready file URL, waiting for CDN delivery when necessary. */
3766
- async resolveUrl(path, options = {}) {
4404
+ resolveUrl(path, options = {}) {
4405
+ const { fetch: customFetch, ...request } = options;
4406
+ if (customFetch) return this.resolveUrlThrough(path, customFetch, options);
4407
+ return this.services.urls.resolve(this.spaceId, path, request);
4408
+ }
4409
+ onUrlChange(listener) {
4410
+ return this.services.urls.subscribe((spaceId, path) => {
4411
+ if (spaceId === this.spaceId) listener(path);
4412
+ });
4413
+ }
4414
+ async resolveUrlThrough(path, customFetch, options) {
3767
4415
  const purpose = options.purpose ?? "preview";
3768
4416
  const requestedTimeoutMs = options.timeoutMs ?? 15e3;
3769
4417
  const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(0, requestedTimeoutMs) : 15e3;
@@ -3771,7 +4419,7 @@ var SpaceFilesApi = class {
3771
4419
  const deadline = createSpaceFileDeadlineSignal(options.signal, timeoutMs);
3772
4420
  try {
3773
4421
  while (true) {
3774
- const file = await this.read(path, options.fetch, deadline.signal);
4422
+ const file = await this.read(path, customFetch, deadline.signal);
3775
4423
  if (deadline.timedOut()) return null;
3776
4424
  if ("content" in file) {
3777
4425
  if (file.delivery === "url" && file.url) return file.url;
@@ -4049,14 +4697,9 @@ var SessionTurnsClient = class {
4049
4697
  });
4050
4698
  }
4051
4699
  };
4052
- /**
4053
- * Routes a rejected subscription for `room` (for example a missing
4054
- * `space.view` permission) to `onRejected`. Without this an unauthorized
4055
- * subscriber would simply never receive events, with no way to tell.
4056
- */
4057
4700
  const onRoomRejected = (websocketClient, room, onRejected) => websocketClient.on("event", (event) => {
4058
4701
  if (event.type !== "system.subscribe.error") return;
4059
- if (event.payload.rejected?.some((entry) => entry.room === room)) onRejected(event);
4702
+ if (event.payload.rejected?.some((entry) => entry.room === room && entry.code !== "UNAVAILABLE")) onRejected(event);
4060
4703
  });
4061
4704
  var SessionRealtimeClient = class {
4062
4705
  websocketClient;
@@ -4314,14 +4957,6 @@ var SpaceEventsApi = class {
4314
4957
  handler(event);
4315
4958
  return;
4316
4959
  }
4317
- if (type === "board.changed" && event.type === "board.changed") {
4318
- handler(event);
4319
- return;
4320
- }
4321
- if (type === "board.playback.changed" && event.type === "board.playback.changed") {
4322
- handler(event);
4323
- return;
4324
- }
4325
4960
  if (type === "app.version.published" && event.type === "app.version.published") {
4326
4961
  handler(event);
4327
4962
  return;
@@ -4844,11 +5479,11 @@ var SpaceClient = class {
4844
5479
  boards;
4845
5480
  wallet;
4846
5481
  commerce;
4847
- constructor(id, transport, websocketClient) {
5482
+ constructor(id, transport, websocketClient, fileServices) {
4848
5483
  this.id = id;
4849
5484
  this.transport = transport;
4850
5485
  this.websocketClient = websocketClient;
4851
- this.files = new SpaceFilesApi(transport, id);
5486
+ this.files = new SpaceFilesApi(transport, id, fileServices);
4852
5487
  this.publicFiles = new SpacePublicFilesApi(transport, id);
4853
5488
  this.sessions = new SpaceSessionsApi(transport, id, websocketClient);
4854
5489
  this.turns = new SpaceTurnsApi(transport, id);
@@ -5812,6 +6447,7 @@ var CohubHttpClient = class {
5812
6447
  return this.desktop;
5813
6448
  }
5814
6449
  transport;
6450
+ fileServices;
5815
6451
  constructor(options = {}) {
5816
6452
  const apiBaseUrl = resolveApiBaseUrl(options);
5817
6453
  const executionToken = resolveExecutionToken();
@@ -5820,6 +6456,7 @@ var CohubHttpClient = class {
5820
6456
  getAccessToken: () => executionToken
5821
6457
  };
5822
6458
  this.transport = new HttpTransport(resolvedOptions);
6459
+ this.fileServices = createSpaceFileServices(this.transport, null);
5823
6460
  this.spaces = new SpacesApi(this.transport);
5824
6461
  this.channels = new ChannelsApi(this.transport);
5825
6462
  this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
@@ -5842,9 +6479,9 @@ var CohubHttpClient = class {
5842
6479
  this.desktop = new DesktopCommandsApi(this.transport);
5843
6480
  }
5844
6481
  space(spaceId) {
5845
- return new SpaceClient(spaceId, this.transport, null);
6482
+ return new SpaceClient(spaceId, this.transport, null, this.fileServices);
5846
6483
  }
5847
6484
  };
5848
6485
  const createHttpClient = (options) => new CohubHttpClient(options);
5849
6486
  //#endregion
5850
- export { isTerminalDesktopCommandStatus as $, BoardClient as A, CronJobsApi as B, SessionPatchReducer as C, isBuiltinItemType as Ct, connectDisplay as D, boardDefaultSize as Dt, DisplayConnection as E, BOARD_ARROW_STROKE_SIZE as Et, PublicAssetsApi as F, DESKTOP_COMMAND_OPEN_TIMEOUT_MS as G, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS as H, SkillsApi as I, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS as J, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES as K, PromptsApi as L, SessionAccessApi as M, ReferencesApi as N, readRtcTransportStats as O, SearchApi as P, isDesktopCallMethod as Q, ModelsApi as R, parseAssistantMessageCommit as S, boardItemSchema as St, SpaceDisplaysApi as T, normalizeBoardRemoteUrl as Tt, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS as U, ChannelsApi as V, DESKTOP_COMMAND_MAX_TIMEOUT_MS as W, DESKTOP_COMMAND_VERSION as X, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS as Y, defaultDesktopCommandTimeoutMs as Z, PublicInviteApi as _, BoardColorSchema as _t, WorkCommerceApi as a, DISPLAY_INPUT_MAX_SCHEDULE_MS as at, SessionGenerationStreamClient as b, BoardTrackSchema as bt, AppsApi as c, normalizeDisplayPoint as ct, UserApi as d, BOARD_MIME_TYPE as dt, parseDesktopCommand as et, TasksApi as f, BOARD_CAMERA_TARGET as ft, SpacesApi as g, BoardAnimationSchema as gt, SpaceWebhooksApi as h, BoardAnimationHeaderSchema as ht, AppCommerceApi as i, DISPLAY_ELEMENT_REF_PATTERN as it, ensureRealtimeConnected as j, waitForIceGathering as k, ReferralsApi as l, splitDisplayText as lt, SpacePublicFilesApi as m, BOARD_ITEM_TYPES as mt, createHttpClient as n, resolveOpenSurface as nt, DesktopCommandsApi as o, DISPLAY_INPUT_MAX_TEXT as ot, SpaceClient as p, BOARD_COLOR_TOKENS as pt, DESKTOP_COMMAND_PENDING_TTL_SECONDS as q, RoomsApi as r, NAVIGATION_ERROR_MESSAGE_MAX_LENGTH as rt, UiCommandsApi as s, DISPLAY_SYSTEM_ACTIONS as st, CohubHttpClient as t, parseDesktopCommandId as tt, UsersApi as u, BOARD_EXTENSION as ut, buildSpaceInvitePath as v, BoardPatchSchema as vt, createSessionPatchReducer as w, parseBoardItem as wt, createSessionGenerationStreamClient as x, BoardVec2Schema as xt, buildSpacePath as y, BoardSettingsSchema as yt, GenerationsApi as z };
6487
+ export { defaultDesktopCommandTimeoutMs as $, readRtcTransportStats as A, ModelsApi as B, createSessionGenerationStreamClient as C, BoardVec2Schema as Ct, SpaceDisplaysApi as D, normalizeBoardRemoteUrl as Dt, createSessionPatchReducer as E, parseBoardItem as Et, ReferencesApi as F, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS as G, CronJobsApi as H, SearchApi as I, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES as J, DESKTOP_COMMAND_MAX_TIMEOUT_MS as K, PublicAssetsApi as L, BoardClient as M, ensureRealtimeConnected as N, DisplayConnection as O, BOARD_ARROW_STROKE_SIZE as Ot, SessionAccessApi as P, DESKTOP_COMMAND_VERSION as Q, SkillsApi as R, SessionGenerationStreamClient as S, BoardTrackSchema as St, SessionPatchReducer as T, isBuiltinItemType as Tt, ChannelsApi as U, GenerationsApi as V, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS as W, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS as X, DESKTOP_COMMAND_PENDING_TTL_SECONDS as Y, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS as Z, createSpaceFileServices as _, BoardAnimationHeaderSchema as _t, WorkCommerceApi as a, NAVIGATION_ERROR_MESSAGE_MAX_LENGTH as at, buildSpaceInvitePath as b, BoardPatchSchema as bt, AppsApi as c, DISPLAY_INPUT_MAX_TEXT as ct, UserApi as d, splitDisplayText as dt, isDesktopCallMethod as et, TasksApi as f, BOARD_EXTENSION as ft, SpacesApi as g, BOARD_ITEM_TYPES as gt, SpaceWebhooksApi as h, BOARD_COLOR_TOKENS as ht, AppCommerceApi as i, resolveOpenSurface as it, waitForIceGathering as j, connectDisplay as k, boardDefaultSize as kt, ReferralsApi as l, DISPLAY_SYSTEM_ACTIONS as lt, SpacePublicFilesApi as m, BOARD_CAMERA_TARGET as mt, createHttpClient as n, parseDesktopCommand as nt, DesktopCommandsApi as o, DISPLAY_ELEMENT_REF_PATTERN as ot, SpaceClient as p, BOARD_MIME_TYPE as pt, DESKTOP_COMMAND_OPEN_TIMEOUT_MS as q, RoomsApi as r, parseDesktopCommandId as rt, UiCommandsApi as s, DISPLAY_INPUT_MAX_SCHEDULE_MS as st, CohubHttpClient as t, isTerminalDesktopCommandStatus as tt, UsersApi as u, normalizeDisplayPoint as ut, clearFileUrlCache as v, BoardAnimationSchema as vt, parseAssistantMessageCommit as w, boardItemSchema as wt, buildSpacePath as x, BoardSettingsSchema as xt, PublicInviteApi as y, BoardColorSchema as yt, PromptsApi as z };