@neta-art/cohub 12.5.0 → 13.0.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.
Files changed (76) hide show
  1. package/README.md +35 -13
  2. package/dist/board/editor/editor.d.ts +7 -5
  3. package/dist/board/editor/index.js +1 -1
  4. package/dist/board/editor/interaction.js +1 -1
  5. package/dist/board/export/index.d.ts +1 -6
  6. package/dist/board/export/index.js +16 -10
  7. package/dist/board/export/node/index.d.ts +2 -7
  8. package/dist/board/export/node/index.js +2 -5
  9. package/dist/board/export/scene.d.ts +3 -7
  10. package/dist/board/export/scene.js +22 -26
  11. package/dist/board/model/asset-key.d.ts +5 -2
  12. package/dist/board/model/asset-key.js +13 -5
  13. package/dist/board/model/index.d.ts +5 -4
  14. package/dist/board/model/index.js +4 -3
  15. package/dist/board/model/media-playback.d.ts +4 -6
  16. package/dist/board/model/media-playback.js +4 -4
  17. package/dist/board/model/read.d.ts +7 -0
  18. package/dist/board/model/read.js +18 -0
  19. package/dist/board/model/themes.d.ts +50 -0
  20. package/dist/board/model/themes.js +44 -6
  21. package/dist/board/render/backdrop-layer.d.ts +21 -0
  22. package/dist/board/render/backdrop-layer.js +227 -0
  23. package/dist/board/render/backdrop.d.ts +45 -0
  24. package/dist/board/render/backdrop.js +62 -0
  25. package/dist/board/render/index.d.ts +3 -2
  26. package/dist/board/render/index.js +7 -6
  27. package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
  28. package/dist/board/render/renderers/audio-card-renderer.js +2 -2
  29. package/dist/board/render/renderers/file-card-renderer.js +1 -1
  30. package/dist/board/render/renderers/frame-card-renderer.js +1 -1
  31. package/dist/board/render/renderers/model-card-renderer.js +1 -1
  32. package/dist/board/render/renderers/shape-card-renderer.js +1 -1
  33. package/dist/board/render/renderers/task-card-renderer.js +2 -2
  34. package/dist/board/render/renderers/text-card-renderer.js +2 -2
  35. package/dist/board/render/renderers/video-card-renderer.js +1 -1
  36. package/dist/board/render/theme.d.ts +2 -0
  37. package/dist/board/render/theme.js +4 -2
  38. package/dist/board/replica/index.d.ts +3 -2
  39. package/dist/board/replica/index.js +34 -24
  40. package/dist/board/replica/indexeddb.js +14 -85
  41. package/dist/board/stage/assets.d.ts +4 -3
  42. package/dist/board/stage/assets.js +186 -83
  43. package/dist/board/stage/backdrop.d.ts +25 -0
  44. package/dist/board/stage/backdrop.js +104 -0
  45. package/dist/board/stage/index.d.ts +2 -1
  46. package/dist/board/stage/scene.js +12 -5
  47. package/dist/board/stage/stage.d.ts +7 -2
  48. package/dist/board/stage/stage.js +44 -24
  49. package/dist/board/stage/texture-pool.d.ts +1 -1
  50. package/dist/board/stage/texture-pool.js +4 -5
  51. package/dist/chunks/app-realtime.js +1 -1
  52. package/dist/chunks/environment.d.ts +34 -21
  53. package/dist/chunks/environment.js +4 -3
  54. package/dist/chunks/http-error.js +28 -5
  55. package/dist/chunks/http.d.ts +87 -18
  56. package/dist/chunks/http.js +1855 -59
  57. package/dist/chunks/websocket.d.ts +9 -1
  58. package/dist/http.d.ts +2 -2
  59. package/dist/http.js +1 -1
  60. package/dist/index.d.ts +9 -3
  61. package/dist/index.js +6 -1080
  62. package/dist/indexeddb.js +79 -0
  63. package/dist/media.d.ts +18 -32
  64. package/dist/media.js +81 -67
  65. package/dist/protocol/dist/board-model.d.ts +33 -26
  66. package/dist/protocol/dist/board-model.js +21 -18
  67. package/dist/protocol/dist/board.d.ts +21 -1
  68. package/dist/protocol/dist/board.js +5 -1
  69. package/dist/protocol/dist/core/media.d.ts +32 -9
  70. package/dist/protocol/dist/core/media.js +203 -48
  71. package/dist/protocol/dist/index.d.ts +3 -3
  72. package/dist/voice-input.js +1 -1
  73. package/dist/websocket.js +37 -5
  74. package/package.json +1 -1
  75. package/dist/board/render/board-background.d.ts +0 -18
  76. package/dist/board/render/board-background.js +0 -104
@@ -1,8 +1,6 @@
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
- //#region ../protocol/dist/board-constants.js
5
- const BOARD_ARROW_STROKE_SIZE = 2.5;
6
4
  const BOARD_DEFAULT_ITEM_SIZE = {
7
5
  width: 240,
8
6
  height: 160
@@ -628,27 +626,28 @@ const BOARD_ENTER_PRESETS = [
628
626
  "deal"
629
627
  ];
630
628
  const BoardBackgroundSchema = z.object({
631
- kind: z.enum([
632
- "solid",
633
- "dots",
634
- "grid",
635
- "image"
636
- ]).default("dots"),
637
629
  color: BoardColorSchema.optional(),
638
- imageUrl: z.string().url().max(4096).optional().describe("Background image for kind image."),
639
- fit: z.enum([
640
- "cover",
641
- "contain",
642
- "repeat"
643
- ]).optional(),
644
- opacity: ratio.optional()
645
- }).strict();
630
+ pattern: z.enum([
631
+ "none",
632
+ "dots",
633
+ "grid"
634
+ ]).optional().describe("Spaced by grid.size; omit to follow the theme."),
635
+ image: z.object({
636
+ src: BoardMediaSourceSchema,
637
+ fit: z.enum([
638
+ "cover",
639
+ "contain",
640
+ "repeat"
641
+ ]).default("cover").describe("cover and contain fill the view; repeat tiles the Board at one board unit per image pixel."),
642
+ opacity: ratio.optional()
643
+ }).strict().optional()
644
+ }).strict().describe("Layers drawn under every item: color, then image, then pattern. Without a color or image the theme's backdrop shows.");
646
645
  const BoardSettingsSchema = z.object({
647
646
  title: z.string().min(1).max(255).optional(),
648
- background: BoardBackgroundSchema.default({ kind: "dots" }),
647
+ background: BoardBackgroundSchema.default({}),
649
648
  grid: z.object({
650
- visible: z.boolean().default(false).describe("Show the grid overlay."),
651
- size: finite.min(4).max(512).default(24)
649
+ size: finite.min(4).max(512).default(24).describe("Pattern spacing and snap step, in board units."),
650
+ snap: z.boolean().default(false).describe("Snap moved and resized items to the grid.")
652
651
  }).strict().optional(),
653
652
  enter: z.object({
654
653
  preset: z.enum(BOARD_ENTER_PRESETS),
@@ -706,10 +705,14 @@ z.object({
706
705
  items: z.array(z.string().min(1)).max(5e4).optional(),
707
706
  animations: z.array(z.string().min(1)).max(1e4).optional(),
708
707
  limit: z.number().int().min(1).max(5e4).optional(),
709
- cursor: z.string().min(1).optional()
708
+ cursor: z.string().min(1).optional(),
709
+ since: z.number().int().nonnegative().optional()
710
710
  }).strict().refine((read) => !(read.cursor && (read.rect || read.within)), {
711
711
  path: ["cursor"],
712
712
  message: "cursor pages by id, so it cannot be combined with rect or within"
713
+ }).refine((read) => read.since === void 0 || Object.entries(read).every(([key, value]) => key === "since" || value === void 0), {
714
+ path: ["since"],
715
+ message: "since syncs the whole Board, so it cannot be combined with other read options"
713
716
  });
714
717
  z.object({
715
718
  patch: BoardPatchSchema,
@@ -763,6 +766,1082 @@ z.discriminatedUnion("type", [
763
766
  }).strict()
764
767
  ]);
765
768
  //#endregion
769
+ //#region ../protocol/dist/board-json.js
770
+ /** Structural equality for JSON-shaped semantic values. */
771
+ function boardJsonEquals(a, b) {
772
+ if (a === b) return true;
773
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
774
+ const aArray = Array.isArray(a);
775
+ const bArray = Array.isArray(b);
776
+ if (aArray !== bArray) return false;
777
+ if (aArray && bArray) return a.length === b.length && a.every((value, index) => boardJsonEquals(value, b[index]));
778
+ const aRecord = a;
779
+ const bRecord = b;
780
+ const keys = Object.keys(aRecord);
781
+ return keys.length === Object.keys(bRecord).length && keys.every((key) => Object.hasOwn(bRecord, key) && boardJsonEquals(aRecord[key], bRecord[key]));
782
+ }
783
+ //#endregion
784
+ //#region ../protocol/dist/board-layout.js
785
+ const IDENTITY_MATRIX = [
786
+ 1,
787
+ 0,
788
+ 0,
789
+ 1,
790
+ 0,
791
+ 0
792
+ ];
793
+ function multiplyMatrix(parent, child) {
794
+ const [a1, b1, c1, d1, tx1, ty1] = parent;
795
+ const [a2, b2, c2, d2, tx2, ty2] = child;
796
+ return [
797
+ a1 * a2 + c1 * b2,
798
+ b1 * a2 + d1 * b2,
799
+ a1 * c2 + c1 * d2,
800
+ b1 * c2 + d1 * d2,
801
+ a1 * tx2 + c1 * ty2 + tx1,
802
+ b1 * tx2 + d1 * ty2 + ty1
803
+ ];
804
+ }
805
+ function invertMatrix(matrix) {
806
+ const [a, b, c, d, tx, ty] = matrix;
807
+ const det = a * d - b * c || 1e-12;
808
+ return [
809
+ d / det,
810
+ -b / det,
811
+ -c / det,
812
+ a / det,
813
+ (c * ty - d * tx) / det,
814
+ (b * tx - a * ty) / det
815
+ ];
816
+ }
817
+ function applyMatrix(matrix, point) {
818
+ const [a, b, c, d, tx, ty] = matrix;
819
+ return {
820
+ x: a * point.x + c * point.y + tx,
821
+ y: b * point.x + d * point.y + ty
822
+ };
823
+ }
824
+ function scaleVector(scale) {
825
+ return typeof scale === "number" ? {
826
+ x: scale,
827
+ y: scale
828
+ } : scale;
829
+ }
830
+ function isWideCharacter(code) {
831
+ return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || code >= 131072 && code <= 262141;
832
+ }
833
+ function estimateTextWidth(text, fontSize) {
834
+ let ems = 0;
835
+ for (const character of text) {
836
+ const code = character.codePointAt(0) ?? 0;
837
+ if (character === " ") ems += .28;
838
+ else if (isWideCharacter(code)) ems += 1;
839
+ else if (/[il.,:;'|!]/.test(character)) ems += .3;
840
+ else if (/[mwMW@]/.test(character)) ems += .85;
841
+ else if (/[A-Z0-9]/.test(character)) ems += .64;
842
+ else ems += .55;
843
+ }
844
+ return ems * fontSize;
845
+ }
846
+ function estimateLines(text, { fontSize }, maxWidth) {
847
+ const measure = (line) => estimateTextWidth(line, fontSize);
848
+ const paragraphs = text.split("\n");
849
+ const lines = maxWidth ? paragraphs.flatMap((paragraph) => wrapParagraph(paragraph, maxWidth, measure)) : paragraphs;
850
+ return {
851
+ lines,
852
+ width: lines.reduce((max, line) => Math.max(max, measure(line)), 0)
853
+ };
854
+ }
855
+ function wrapParagraph(paragraph, width, measure) {
856
+ if (!paragraph) return [""];
857
+ const tokens = paragraph.match(/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]|[^\s\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]+|\s+/g) ?? [paragraph];
858
+ const lines = [];
859
+ let line = "";
860
+ for (const token of tokens) {
861
+ const candidate = line + token;
862
+ if (!line || measure(candidate.trimEnd()) <= width) {
863
+ line = candidate;
864
+ continue;
865
+ }
866
+ lines.push(line.trimEnd());
867
+ line = /^\s+$/.test(token) ? "" : token;
868
+ }
869
+ lines.push(line.trimEnd());
870
+ return lines;
871
+ }
872
+ const textSizeCache = /* @__PURE__ */ new Map();
873
+ const TEXT_CACHE_LIMIT = 4096;
874
+ function clampBoardFontSize(fontSize) {
875
+ return Math.min(512, Math.max(2, fontSize));
876
+ }
877
+ function layoutBoardText(props) {
878
+ const fontSize = clampBoardFontSize(props.fontSize ?? 24);
879
+ const key = `${fontSize}|${props.fontWeight}|${props.font}|${props.lineHeight}|${props.width ?? ""}|${props.text}`;
880
+ const cached = textSizeCache.get(key);
881
+ if (cached) return cached;
882
+ const font = {
883
+ fontSize,
884
+ fontWeight: props.fontWeight ?? 500,
885
+ font: props.font ?? "sans"
886
+ };
887
+ const { lines, width } = estimateLines(props.text || "", font, props.width || void 0);
888
+ const lineHeight = fontSize * (props.lineHeight ?? 4 / 3);
889
+ const layout = {
890
+ width: Math.max(props.width ?? 0, width, fontSize * .5),
891
+ height: Math.max(1, lines.length) * lineHeight,
892
+ lines,
893
+ lineHeight
894
+ };
895
+ if (textSizeCache.size >= TEXT_CACHE_LIMIT) textSizeCache.clear();
896
+ textSizeCache.set(key, layout);
897
+ return layout;
898
+ }
899
+ function drawSampleRadius(strokeWidth, pressure) {
900
+ const clamped = Math.min(1, Math.max(0, pressure));
901
+ return Math.max(.5, strokeWidth / 2 * (.5 + clamped));
902
+ }
903
+ function drawPointsBounds(points, strokeWidth) {
904
+ if (points.length === 0) return {
905
+ x: 0,
906
+ y: 0,
907
+ width: 1,
908
+ height: 1
909
+ };
910
+ let minX = Number.POSITIVE_INFINITY;
911
+ let minY = Number.POSITIVE_INFINITY;
912
+ let maxX = Number.NEGATIVE_INFINITY;
913
+ let maxY = Number.NEGATIVE_INFINITY;
914
+ for (const point of points) {
915
+ const radius = drawSampleRadius(strokeWidth, point.p);
916
+ minX = Math.min(minX, point.x - radius);
917
+ minY = Math.min(minY, point.y - radius);
918
+ maxX = Math.max(maxX, point.x + radius);
919
+ maxY = Math.max(maxY, point.y + radius);
920
+ }
921
+ return {
922
+ x: minX,
923
+ y: minY,
924
+ width: Math.max(1, maxX - minX),
925
+ height: Math.max(1, maxY - minY)
926
+ };
927
+ }
928
+ function pointsRect(points) {
929
+ let minX = Number.POSITIVE_INFINITY;
930
+ let minY = Number.POSITIVE_INFINITY;
931
+ let maxX = Number.NEGATIVE_INFINITY;
932
+ let maxY = Number.NEGATIVE_INFINITY;
933
+ for (const point of points) {
934
+ minX = Math.min(minX, point.x);
935
+ minY = Math.min(minY, point.y);
936
+ maxX = Math.max(maxX, point.x);
937
+ maxY = Math.max(maxY, point.y);
938
+ }
939
+ if (!Number.isFinite(minX)) return {
940
+ x: 0,
941
+ y: 0,
942
+ width: 1,
943
+ height: 1
944
+ };
945
+ return {
946
+ x: minX,
947
+ y: minY,
948
+ width: Math.max(1, maxX - minX),
949
+ height: Math.max(1, maxY - minY)
950
+ };
951
+ }
952
+ function isArrowBinding(end) {
953
+ return typeof end.item === "string";
954
+ }
955
+ function arrowBindings(item) {
956
+ if (item.type !== "arrow") return [];
957
+ const props = item.props;
958
+ const ids = [props.start, props.end].filter(isArrowBinding).map((end) => end.item);
959
+ return [...new Set(ids)];
960
+ }
961
+ function bendControl(start, end, bend) {
962
+ const mid = {
963
+ x: (start.x + end.x) / 2,
964
+ y: (start.y + end.y) / 2
965
+ };
966
+ if (!bend) return mid;
967
+ const dx = end.x - start.x;
968
+ const dy = end.y - start.y;
969
+ const length = Math.hypot(dx, dy) || 1;
970
+ const offset = bend * length;
971
+ return {
972
+ x: mid.x + -dy / length * offset,
973
+ y: mid.y + dx / length * offset
974
+ };
975
+ }
976
+ const MAX_DEPTH = 64;
977
+ function createBoardLayout(initialLookup, dependents) {
978
+ let lookup = initialLookup;
979
+ const boxes = /* @__PURE__ */ new Map();
980
+ const matrices = /* @__PURE__ */ new Map();
981
+ const bounds = /* @__PURE__ */ new Map();
982
+ function matrixOf(id, depth = 0) {
983
+ const cached = matrices.get(id);
984
+ if (cached) return cached;
985
+ const item = lookup(id);
986
+ if (!item) return IDENTITY_MATRIX;
987
+ const matrix = multiplyMatrix(item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX, localMatrixOf(id));
988
+ matrices.set(id, matrix);
989
+ return matrix;
990
+ }
991
+ function localMatrixOf(id) {
992
+ const item = lookup(id);
993
+ if (!item) return IDENTITY_MATRIX;
994
+ if (item.type === "arrow") return translate(item.position);
995
+ const box = boxOf(id);
996
+ const scale = scaleVector(item.scale);
997
+ const radians = item.rotation * Math.PI / 180;
998
+ const cos = Math.cos(radians);
999
+ const sin = Math.sin(radians);
1000
+ const ox = box.x + item.origin.x * box.width;
1001
+ const oy = box.y + item.origin.y * box.height;
1002
+ const a = cos * scale.x;
1003
+ const b = sin * scale.x;
1004
+ const c = -sin * scale.y;
1005
+ const d = cos * scale.y;
1006
+ return [
1007
+ a,
1008
+ b,
1009
+ c,
1010
+ d,
1011
+ item.position.x + ox - (a * ox + c * oy),
1012
+ item.position.y + oy - (b * ox + d * oy)
1013
+ ];
1014
+ }
1015
+ function boxOf(id, depth = 0) {
1016
+ const cached = boxes.get(id);
1017
+ if (cached) return cached;
1018
+ const item = lookup(id);
1019
+ let box;
1020
+ if (!item) box = {
1021
+ x: 0,
1022
+ y: 0,
1023
+ width: 1,
1024
+ height: 1
1025
+ };
1026
+ else if ("size" in item && item.size) box = {
1027
+ x: 0,
1028
+ y: 0,
1029
+ width: item.size.width,
1030
+ height: item.size.height
1031
+ };
1032
+ else if (item.type === "text") {
1033
+ const layout = layoutBoardText(item.props);
1034
+ box = {
1035
+ x: 0,
1036
+ y: 0,
1037
+ width: layout.width,
1038
+ height: layout.height
1039
+ };
1040
+ } else if (item.type === "draw") box = drawPointsBounds(item.props.points, item.style.strokeWidth ?? 4);
1041
+ else if (item.type === "arrow") {
1042
+ const props = item.props;
1043
+ const toLocal = invertMatrix(multiplyMatrix(parentOf(item, depth), translate(item.position)));
1044
+ const start = isArrowBinding(props.start) ? applyMatrix(toLocal, resolveBoundEnd(item, props.start, props.end, depth)) : props.start;
1045
+ const end = isArrowBinding(props.end) ? applyMatrix(toLocal, resolveBoundEnd(item, props.end, props.start, depth)) : props.end;
1046
+ const points = [
1047
+ start,
1048
+ end,
1049
+ ...props.waypoints
1050
+ ];
1051
+ if (props.bend) points.push(bendControl(start, end, props.bend));
1052
+ const rect = pointsRect(points);
1053
+ const pad = Math.max(8, (item.style.strokeWidth ?? 2.5) * 2);
1054
+ box = {
1055
+ x: rect.x - pad,
1056
+ y: rect.y - pad,
1057
+ width: rect.width + pad * 2,
1058
+ height: rect.height + pad * 2
1059
+ };
1060
+ } else box = {
1061
+ x: 0,
1062
+ y: 0,
1063
+ ...boardDefaultSize(item.type)
1064
+ };
1065
+ boxes.set(id, box);
1066
+ return box;
1067
+ }
1068
+ function parentOf(item, depth) {
1069
+ return item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX;
1070
+ }
1071
+ function resolveBoundEnd(arrow, end, other, depth) {
1072
+ const target = lookup(end.item);
1073
+ const arrowSpace = multiplyMatrix(parentOf(arrow, depth), translate(arrow.position));
1074
+ if (!target || target.type === "arrow" || depth >= MAX_DEPTH) return applyMatrix(arrowSpace, {
1075
+ x: 0,
1076
+ y: 0
1077
+ });
1078
+ const frame = frameOf(end.item, depth + 1);
1079
+ const toward = isArrowBinding(other) ? lookup(other.item) && lookup(other.item)?.type !== "arrow" ? center(frameOf(other.item, depth + 1)) : center(frame) : applyMatrix(arrowSpace, other);
1080
+ return anchorPoint(frame, end.anchor, toward);
1081
+ }
1082
+ function frameOf(id, depth = 0) {
1083
+ boxOf(id, depth);
1084
+ const box = boxes.get(id);
1085
+ const matrix = matrixOf(id, depth);
1086
+ const [a, b, c, d] = matrix;
1087
+ const centerPoint = applyMatrix(matrix, {
1088
+ x: box.x + box.width / 2,
1089
+ y: box.y + box.height / 2
1090
+ });
1091
+ const width = box.width * Math.hypot(a, b);
1092
+ const height = box.height * Math.hypot(c, d);
1093
+ return {
1094
+ x: centerPoint.x - width / 2,
1095
+ y: centerPoint.y - height / 2,
1096
+ width,
1097
+ height,
1098
+ rotation: Math.atan2(b, a) * 180 / Math.PI
1099
+ };
1100
+ }
1101
+ function boundsOf(id) {
1102
+ const cached = bounds.get(id);
1103
+ if (cached) return cached;
1104
+ const box = boxOf(id);
1105
+ const matrix = matrixOf(id);
1106
+ const rect = pointsRect([
1107
+ applyMatrix(matrix, {
1108
+ x: box.x,
1109
+ y: box.y
1110
+ }),
1111
+ applyMatrix(matrix, {
1112
+ x: box.x + box.width,
1113
+ y: box.y
1114
+ }),
1115
+ applyMatrix(matrix, {
1116
+ x: box.x + box.width,
1117
+ y: box.y + box.height
1118
+ }),
1119
+ applyMatrix(matrix, {
1120
+ x: box.x,
1121
+ y: box.y + box.height
1122
+ })
1123
+ ]);
1124
+ bounds.set(id, rect);
1125
+ return rect;
1126
+ }
1127
+ function setLookup(nextLookup) {
1128
+ lookup = nextLookup;
1129
+ }
1130
+ function invalidate(ids) {
1131
+ if (!ids) {
1132
+ boxes.clear();
1133
+ matrices.clear();
1134
+ bounds.clear();
1135
+ return;
1136
+ }
1137
+ const queue = [...ids];
1138
+ const seen = /* @__PURE__ */ new Set();
1139
+ while (queue.length) {
1140
+ const id = queue.pop();
1141
+ if (seen.has(id)) continue;
1142
+ seen.add(id);
1143
+ boxes.delete(id);
1144
+ matrices.delete(id);
1145
+ bounds.delete(id);
1146
+ if (dependents) for (const next of dependents(id)) queue.push(next);
1147
+ }
1148
+ }
1149
+ return {
1150
+ has: (id) => lookup(id) !== void 0,
1151
+ box: (id) => boxOf(id),
1152
+ matrix: (id) => matrixOf(id),
1153
+ localMatrix: (id) => localMatrixOf(id),
1154
+ parentMatrix: (id) => {
1155
+ const item = lookup(id);
1156
+ return item ? parentOf(item, 0) : IDENTITY_MATRIX;
1157
+ },
1158
+ bounds: boundsOf,
1159
+ frame: (id) => frameOf(id),
1160
+ arrowEnd: (id, which) => {
1161
+ const item = lookup(id);
1162
+ if (item?.type !== "arrow") return {
1163
+ x: 0,
1164
+ y: 0
1165
+ };
1166
+ const arrow = item;
1167
+ const [end, other] = which === "start" ? [arrow.props.start, arrow.props.end] : [arrow.props.end, arrow.props.start];
1168
+ return isArrowBinding(end) ? resolveBoundEnd(arrow, end, other, 0) : applyMatrix(matrixOf(id), end);
1169
+ },
1170
+ invalidate,
1171
+ setLookup
1172
+ };
1173
+ }
1174
+ function translate(point) {
1175
+ return [
1176
+ 1,
1177
+ 0,
1178
+ 0,
1179
+ 1,
1180
+ point.x,
1181
+ point.y
1182
+ ];
1183
+ }
1184
+ function center(frame) {
1185
+ return {
1186
+ x: frame.x + frame.width / 2,
1187
+ y: frame.y + frame.height / 2
1188
+ };
1189
+ }
1190
+ function rotateAround(point, pivot, degrees) {
1191
+ if (!degrees) return point;
1192
+ const radians = degrees * Math.PI / 180;
1193
+ const cos = Math.cos(radians);
1194
+ const sin = Math.sin(radians);
1195
+ const dx = point.x - pivot.x;
1196
+ const dy = point.y - pivot.y;
1197
+ return {
1198
+ x: pivot.x + dx * cos - dy * sin,
1199
+ y: pivot.y + dx * sin + dy * cos
1200
+ };
1201
+ }
1202
+ function framePoint(frame, nx, ny) {
1203
+ return rotateAround({
1204
+ x: frame.x + nx * frame.width,
1205
+ y: frame.y + ny * frame.height
1206
+ }, center(frame), frame.rotation);
1207
+ }
1208
+ function autoAnchorSide(frame, toward) {
1209
+ const local = rotateAround(toward, center(frame), -frame.rotation);
1210
+ const c = center(frame);
1211
+ const dx = (local.x - c.x) / Math.max(frame.width / 2, 1e-4);
1212
+ const dy = (local.y - c.y) / Math.max(frame.height / 2, 1e-4);
1213
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
1214
+ return dy >= 0 ? "bottom" : "top";
1215
+ }
1216
+ function sideAnchor(side, offset) {
1217
+ switch (side) {
1218
+ case "top": return {
1219
+ x: offset,
1220
+ y: 0
1221
+ };
1222
+ case "bottom": return {
1223
+ x: offset,
1224
+ y: 1
1225
+ };
1226
+ case "left": return {
1227
+ x: 0,
1228
+ y: offset
1229
+ };
1230
+ default: return {
1231
+ x: 1,
1232
+ y: offset
1233
+ };
1234
+ }
1235
+ }
1236
+ function anchorPoint(frame, anchor, toward) {
1237
+ if (anchor === "auto") {
1238
+ const normalized = sideAnchor(autoAnchorSide(frame, toward), .5);
1239
+ return framePoint(frame, normalized.x, normalized.y);
1240
+ }
1241
+ if ("side" in anchor) {
1242
+ const normalized = sideAnchor(anchor.side, anchor.offset);
1243
+ return framePoint(frame, normalized.x, normalized.y);
1244
+ }
1245
+ return framePoint(frame, anchor.x, anchor.y);
1246
+ }
1247
+ //#endregion
1248
+ //#region ../protocol/dist/board-patch.js
1249
+ function slot(map, key) {
1250
+ let value = map[key];
1251
+ if (!value) {
1252
+ value = {};
1253
+ map[key] = value;
1254
+ }
1255
+ return value;
1256
+ }
1257
+ function emptyDelta() {
1258
+ return {
1259
+ items: {},
1260
+ animations: {},
1261
+ tracks: {}
1262
+ };
1263
+ }
1264
+ function withoutEmpty(value) {
1265
+ for (const [key, entry] of Object.entries(value)) if (entry === void 0 || isRecord$2(entry) && Object.keys(entry).length === 0) delete value[key];
1266
+ return value;
1267
+ }
1268
+ function isRecord$2(value) {
1269
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1270
+ }
1271
+ function mergePatch(target, patch) {
1272
+ if (!isRecord$2(patch)) return patch;
1273
+ const result = isRecord$2(target) ? { ...target } : {};
1274
+ for (const [key, value] of Object.entries(patch)) if (value === null) delete result[key];
1275
+ else result[key] = mergePatch(result[key], value);
1276
+ return result;
1277
+ }
1278
+ function def(schema) {
1279
+ return schema._zod.def;
1280
+ }
1281
+ function unwrap(schema) {
1282
+ let current = schema;
1283
+ for (let guard = 0; guard < 16; guard += 1) {
1284
+ const d = def(current);
1285
+ if (d.type === "optional" || d.type === "default" || d.type === "nullable" || d.type === "readonly" || d.type === "prefault") current = d.innerType;
1286
+ else if (d.type === "pipe") current = d.in;
1287
+ else return current;
1288
+ }
1289
+ return current;
1290
+ }
1291
+ function unwrapObjectShape(schema) {
1292
+ const d = def(unwrap(schema));
1293
+ return d.type === "object" ? d.shape : null;
1294
+ }
1295
+ function schemaAtPath(schema, path) {
1296
+ let current = schema;
1297
+ for (const segment of path) {
1298
+ const d = def(unwrap(current));
1299
+ if (d.type === "object") {
1300
+ const shape = d.shape;
1301
+ if (!Object.hasOwn(shape, segment)) return null;
1302
+ current = shape[segment];
1303
+ } else if (d.type === "record") current = d.valueType;
1304
+ else if (d.type === "union") {
1305
+ const branch = d.options.find((option) => unwrapObjectShape(option)?.[segment]);
1306
+ if (!branch) return null;
1307
+ current = unwrapObjectShape(branch)[segment];
1308
+ } else return null;
1309
+ }
1310
+ return current;
1311
+ }
1312
+ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
1313
+ "type",
1314
+ "parent",
1315
+ "z",
1316
+ "locked",
1317
+ "metadata",
1318
+ "script"
1319
+ ]);
1320
+ const CAMERA_PROPERTIES = {
1321
+ focus: {
1322
+ kind: "discrete",
1323
+ schema: z.union([z.string().min(1), z.object({
1324
+ x: z.number().finite(),
1325
+ y: z.number().finite(),
1326
+ width: z.number().finite().positive(),
1327
+ height: z.number().finite().positive()
1328
+ }).strict()]).describe("Item id or world-space rect to fit with 48 px padding; holds until the next focus keyframe.")
1329
+ },
1330
+ zoom: {
1331
+ kind: "number",
1332
+ schema: z.number().finite().positive().describe("Pixels per world unit; overrides the zoom computed by focus."),
1333
+ multiplicative: true
1334
+ },
1335
+ center: {
1336
+ kind: "vec2",
1337
+ schema: BoardVec2Schema.describe("World-space viewport center; overrides focus positioning using the resolved zoom.")
1338
+ },
1339
+ shake: {
1340
+ kind: "number",
1341
+ schema: z.number().finite().nonnegative().describe("Shake amplitude in screen pixels, applied after focus, zoom and center.")
1342
+ }
1343
+ };
1344
+ const ANIMATION_PROPERTIES = { time: {
1345
+ kind: "number",
1346
+ schema: z.number().finite().nonnegative()
1347
+ } };
1348
+ function kindOf(schema, path) {
1349
+ const leaf = path[path.length - 1];
1350
+ if (path.length === 1 && leaf === "scale") return "scale";
1351
+ if (schema === BoardColorSchema || unwrap(schema) === BoardColorSchema) return "color";
1352
+ if (def(unwrap(schema)).type === "number") return "number";
1353
+ const shape = unwrapObjectShape(schema);
1354
+ if (shape && Object.keys(shape).length === 2 && "x" in shape && "y" in shape) return "vec2";
1355
+ return "discrete";
1356
+ }
1357
+ function resolveBoardProperty(targetType, property) {
1358
+ if (targetType === "camera") {
1359
+ const spec = CAMERA_PROPERTIES[property];
1360
+ return spec ? {
1361
+ ok: true,
1362
+ spec
1363
+ } : {
1364
+ ok: false,
1365
+ message: `camera has no property ${property}; expected ${Object.keys(CAMERA_PROPERTIES).join(", ")}`
1366
+ };
1367
+ }
1368
+ if (targetType === "animation") {
1369
+ const spec = ANIMATION_PROPERTIES[property];
1370
+ return spec ? {
1371
+ ok: true,
1372
+ spec
1373
+ } : {
1374
+ ok: false,
1375
+ message: `animations are driven through time`
1376
+ };
1377
+ }
1378
+ const path = property.split(".");
1379
+ if (path.some((segment) => !segment) || NOT_ANIMATABLE.has(path[0])) return {
1380
+ ok: false,
1381
+ message: `${property} cannot be animated`
1382
+ };
1383
+ const schema = boardItemSchema(targetType);
1384
+ if (!schema) return {
1385
+ ok: false,
1386
+ message: `unknown item type ${targetType}`
1387
+ };
1388
+ if (!isBuiltinItemType(targetType) && path[0] === "props") return {
1389
+ ok: true,
1390
+ spec: {
1391
+ kind: "discrete",
1392
+ schema: z.unknown()
1393
+ }
1394
+ };
1395
+ const leaf = schemaAtPath(schema, path);
1396
+ if (!leaf) return {
1397
+ ok: false,
1398
+ message: `${targetType} has no property ${property}`
1399
+ };
1400
+ const kind = kindOf(leaf, path);
1401
+ return {
1402
+ ok: true,
1403
+ spec: {
1404
+ kind,
1405
+ schema: unwrap(leaf),
1406
+ ...property === "opacity" || kind === "scale" ? { multiplicative: true } : {}
1407
+ }
1408
+ };
1409
+ }
1410
+ function lastKeyframeAt(trackPatches) {
1411
+ let end = 0;
1412
+ for (const track of trackPatches) {
1413
+ const keyframes = track?.keyframes;
1414
+ if (!Array.isArray(keyframes)) continue;
1415
+ for (const keyframe of keyframes) {
1416
+ const at = keyframe?.at;
1417
+ if (typeof at === "number" && Number.isFinite(at)) end = Math.max(end, at);
1418
+ }
1419
+ }
1420
+ return end > 0 ? end : void 0;
1421
+ }
1422
+ function trackReferences(track) {
1423
+ const refs = /* @__PURE__ */ new Set([track.target]);
1424
+ if (track.target === "camera" && track.property === "focus") {
1425
+ for (const keyframe of track.keyframes) if (typeof keyframe.value === "string") refs.add(keyframe.value);
1426
+ }
1427
+ refs.delete(BOARD_CAMERA_TARGET);
1428
+ return [...refs];
1429
+ }
1430
+ function pruneBoardTrack(track, deleted) {
1431
+ if (deleted.has(track.target)) return null;
1432
+ if (track.target !== "camera" || track.property !== "focus") return track;
1433
+ const keyframes = track.keyframes.filter((entry) => typeof entry.value !== "string" || !deleted.has(entry.value));
1434
+ if (keyframes.length === track.keyframes.length) return track;
1435
+ return keyframes.length ? {
1436
+ ...track,
1437
+ keyframes
1438
+ } : null;
1439
+ }
1440
+ function error(path, code, message) {
1441
+ return {
1442
+ severity: "error",
1443
+ code,
1444
+ path,
1445
+ message
1446
+ };
1447
+ }
1448
+ function stripId(value) {
1449
+ if (!("id" in value)) return value;
1450
+ const { id: _id, ...rest } = value;
1451
+ return rest;
1452
+ }
1453
+ function applyBoardPatch(state, input, options = {}) {
1454
+ const parsedPatch = BoardPatchSchema.safeParse(input);
1455
+ if (!parsedPatch.success) return {
1456
+ ok: false,
1457
+ diagnostics: parsedPatch.error.issues.map((issue) => error(issue.path.map(String).join("."), "INVALID_PATCH", issue.message))
1458
+ };
1459
+ const patch = parsedPatch.data;
1460
+ const diagnostics = [];
1461
+ const nextItems = /* @__PURE__ */ new Map();
1462
+ const nextAnimations = /* @__PURE__ */ new Map();
1463
+ const nextTracks = /* @__PURE__ */ new Map();
1464
+ let nextSettings;
1465
+ const item = (id) => nextItems.has(id) ? nextItems.get(id) ?? void 0 : state.item(id);
1466
+ const animation = (id) => nextAnimations.has(id) ? nextAnimations.get(id) ?? void 0 : state.animation(id);
1467
+ const setTrack = (animationId, trackId, value) => {
1468
+ const tracks = nextTracks.get(animationId) ?? /* @__PURE__ */ new Map();
1469
+ tracks.set(trackId, value);
1470
+ nextTracks.set(animationId, tracks);
1471
+ };
1472
+ const track = (animationId, trackId) => {
1473
+ const pending = nextTracks.get(animationId);
1474
+ if (pending?.has(trackId)) return pending.get(trackId) ?? void 0;
1475
+ return state.track(animationId, trackId);
1476
+ };
1477
+ if (patch.board !== void 0) {
1478
+ const merged = patch.board === null ? {} : mergePatch(state.settings(), patch.board);
1479
+ const parsed = BoardSettingsSchema.safeParse(merged);
1480
+ if (parsed.success) nextSettings = parsed.data;
1481
+ else for (const issue of parsed.error.issues) diagnostics.push(error(["board", ...issue.path.map(String)].join("."), "INVALID_BOARD", issue.message));
1482
+ }
1483
+ const zByParent = /* @__PURE__ */ new Map();
1484
+ for (const [id, value] of Object.entries(patch.items ?? {})) {
1485
+ if (value === null) {
1486
+ if (state.item(id)) nextItems.set(id, null);
1487
+ continue;
1488
+ }
1489
+ const current = state.item(id);
1490
+ const body = stripId(value);
1491
+ const candidate = !current || typeof body.type === "string" && body.type !== current.type ? { ...body } : mergePatch(current, body);
1492
+ if (candidate.z === void 0) {
1493
+ const parent = typeof candidate.parent === "string" ? candidate.parent : void 0;
1494
+ const key = parent ?? "";
1495
+ const top = zByParent.get(key) ?? state.maxZ(parent);
1496
+ candidate.z = current?.z ?? top + 1;
1497
+ if (!current) zByParent.set(key, top + 1);
1498
+ }
1499
+ const parsed = parseBoardItem(candidate, `items.${id}`);
1500
+ if (!parsed.ok) {
1501
+ diagnostics.push(...parsed.diagnostics);
1502
+ continue;
1503
+ }
1504
+ if (current && boardJsonEquals(current, parsed.item)) continue;
1505
+ nextItems.set(id, parsed.item);
1506
+ }
1507
+ for (const [id, value] of Object.entries(patch.animations ?? {})) {
1508
+ if (value === null) {
1509
+ if (!state.animation(id)) continue;
1510
+ nextAnimations.set(id, null);
1511
+ for (const trackId of state.trackIds(id)) setTrack(id, trackId, null);
1512
+ continue;
1513
+ }
1514
+ const { tracks, ...headerPatch } = stripId(value);
1515
+ if (state.item(id) || nextItems.get(id)) {
1516
+ diagnostics.push(error(`animations.${id}`, "ID_CONFLICT", `${id} is already an item id`));
1517
+ continue;
1518
+ }
1519
+ const current = state.animation(id);
1520
+ if (!current || Object.keys(headerPatch).length > 0) {
1521
+ const header = current ? mergePatch(current, headerPatch) : {
1522
+ ...headerPatch,
1523
+ ...headerPatch.duration === void 0 ? { duration: lastKeyframeAt([...Object.values(tracks ?? {}), ...state.trackIds(id).map((trackId) => state.track(id, trackId))]) } : {}
1524
+ };
1525
+ const parsed = BoardAnimationHeaderSchema.safeParse(header);
1526
+ if (!parsed.success) {
1527
+ for (const issue of parsed.error.issues) diagnostics.push(error([
1528
+ "animations",
1529
+ id,
1530
+ ...issue.path.map(String)
1531
+ ].join("."), "INVALID_ANIMATION", issue.message));
1532
+ continue;
1533
+ }
1534
+ if (!current || !boardJsonEquals(current, parsed.data)) nextAnimations.set(id, parsed.data);
1535
+ }
1536
+ for (const [trackId, trackPatch] of Object.entries(tracks ?? {})) {
1537
+ const existing = state.track(id, trackId);
1538
+ if (trackPatch === null) {
1539
+ if (existing) setTrack(id, trackId, null);
1540
+ continue;
1541
+ }
1542
+ const parsed = BoardTrackSchema.safeParse(existing ? mergePatch(existing, trackPatch) : trackPatch);
1543
+ if (!parsed.success) {
1544
+ for (const issue of parsed.error.issues) diagnostics.push(error([
1545
+ "animations",
1546
+ id,
1547
+ "tracks",
1548
+ trackId,
1549
+ ...issue.path.map(String)
1550
+ ].join("."), "INVALID_TRACK", issue.message));
1551
+ continue;
1552
+ }
1553
+ if (!existing || !boardJsonEquals(existing, parsed.data)) setTrack(id, trackId, parsed.data);
1554
+ }
1555
+ }
1556
+ for (const id of nextItems.keys()) if (nextItems.get(id) && animation(id)) diagnostics.push(error(`items.${id}`, "ID_CONFLICT", `${id} is already an animation id`));
1557
+ if (diagnostics.length) return {
1558
+ ok: false,
1559
+ diagnostics
1560
+ };
1561
+ const deletedItems = [...nextItems].filter(([, value]) => value === null).map(([id]) => id);
1562
+ const deletionLayout = createBoardLayout((id) => item(id) ?? state.item(id));
1563
+ const pendingDeletes = [...deletedItems];
1564
+ while (pendingDeletes.length) {
1565
+ const id = pendingDeletes.pop();
1566
+ for (const child of state.children(id)) {
1567
+ if (nextItems.has(child) && nextItems.get(child)?.parent !== id) continue;
1568
+ if (options.cascade) {
1569
+ if (nextItems.get(child) !== null) {
1570
+ nextItems.set(child, null);
1571
+ pendingDeletes.push(child);
1572
+ }
1573
+ } else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} still has child ${child}; delete it too or use cascade`));
1574
+ }
1575
+ for (const arrowId of state.binders(id)) {
1576
+ const arrow = item(arrowId);
1577
+ if (arrow?.type !== "arrow") continue;
1578
+ const props = { ...arrow.props };
1579
+ for (const which of ["start", "end"]) {
1580
+ const end = props[which];
1581
+ if (!isArrowBinding(end) || end.item !== id) continue;
1582
+ const world = options.resolveArrowEnd?.(arrowId, which) ?? deletionLayout.arrowEnd(arrowId, which);
1583
+ props[which] = applyMatrix(invertMatrix(deletionLayout.matrix(arrowId)), world);
1584
+ }
1585
+ nextItems.set(arrowId, {
1586
+ ...arrow,
1587
+ props
1588
+ });
1589
+ }
1590
+ for (const ref of state.trackRefs(id)) {
1591
+ const current = track(ref.animation, ref.track);
1592
+ if (!current || !trackReferences(current).includes(id)) continue;
1593
+ if (options.cascade) setTrack(ref.animation, ref.track, pruneBoardTrack(current, /* @__PURE__ */ new Set([id])));
1594
+ else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} is animated by ${ref.animation}.${ref.track}; delete the track or use cascade`));
1595
+ }
1596
+ }
1597
+ for (const [id, value] of nextAnimations) {
1598
+ if (value !== null) continue;
1599
+ for (const ref of state.trackRefs(id)) {
1600
+ if (ref.animation === id || track(ref.animation, ref.track) === void 0) continue;
1601
+ if (options.cascade) setTrack(ref.animation, ref.track, null);
1602
+ else diagnostics.push(error(`animations.${id}`, "ANIMATION_REFERENCED", `${id} is nested in ${ref.animation}.${ref.track}; delete the track or use cascade`));
1603
+ }
1604
+ }
1605
+ const validationState = {
1606
+ ...state,
1607
+ trackIds: (id) => [.../* @__PURE__ */ new Set([...state.trackIds(id), ...nextTracks.get(id)?.keys() ?? []])]
1608
+ };
1609
+ const validationItems = new Map(nextItems);
1610
+ const descendants = [];
1611
+ for (const [id, value] of nextItems) if (value && value.parent !== state.item(id)?.parent) descendants.push(...state.children(id));
1612
+ const seenDescendants = /* @__PURE__ */ new Set();
1613
+ while (descendants.length) {
1614
+ const id = descendants.pop();
1615
+ if (seenDescendants.has(id)) continue;
1616
+ seenDescendants.add(id);
1617
+ validationItems.set(id, item(id) ?? null);
1618
+ descendants.push(...state.children(id));
1619
+ }
1620
+ for (const [id, value] of validationItems) {
1621
+ if (!value) continue;
1622
+ if (value.parent !== void 0) {
1623
+ const parent = item(value.parent);
1624
+ if (!parent) diagnostics.push(error(`items.${id}.parent`, "PARENT_NOT_FOUND", `parent ${value.parent} does not exist`));
1625
+ else if (parent.type !== "frame") diagnostics.push(error(`items.${id}.parent`, "INVALID_PARENT", `parent ${value.parent} is a ${parent.type}; only frames contain items`));
1626
+ else {
1627
+ let cursor = value.parent;
1628
+ for (let depth = 0; cursor; depth += 1) {
1629
+ if (depth >= 64) {
1630
+ diagnostics.push(error(`items.${id}.parent`, "PARENT_DEPTH", "parent hierarchy exceeds 64 levels"));
1631
+ break;
1632
+ }
1633
+ if (cursor === id) {
1634
+ diagnostics.push(error(`items.${id}.parent`, "PARENT_CYCLE", `${id} cannot be inside itself`));
1635
+ break;
1636
+ }
1637
+ cursor = item(cursor)?.parent;
1638
+ }
1639
+ }
1640
+ }
1641
+ for (const bound of arrowBindings(value)) {
1642
+ const target = item(bound);
1643
+ if (!target) diagnostics.push(error(`items.${id}.props`, "BINDING_NOT_FOUND", `bound item ${bound} does not exist`));
1644
+ else if (bound === id || target.type === "arrow") diagnostics.push(error(`items.${id}.props`, "INVALID_BINDING", "arrows bind to items other than arrows"));
1645
+ }
1646
+ }
1647
+ for (const [id, value] of nextItems) {
1648
+ if (!value || !state.item(id) || state.item(id)?.type === value.type) continue;
1649
+ if (value.type !== "frame") {
1650
+ for (const child of state.children(id)) if (item(child)?.parent === id) diagnostics.push(error(`items.${child}.parent`, "INVALID_PARENT", `${id} is not a frame`));
1651
+ }
1652
+ if (value.type === "arrow") for (const binder of state.binders(id)) {
1653
+ const arrow = item(binder);
1654
+ if (arrow && arrowBindings(arrow).includes(id)) diagnostics.push(error(`items.${binder}.props`, "INVALID_BINDING", "arrows cannot bind to arrows"));
1655
+ }
1656
+ for (const ref of state.trackRefs(id)) {
1657
+ const current = track(ref.animation, ref.track);
1658
+ if (current) diagnostics.push(...validateTrack(`animations.${ref.animation}.tracks.${ref.track}`, ref.animation, current, item, animation, track, validationState));
1659
+ }
1660
+ }
1661
+ for (const [animationId, tracks] of nextTracks) {
1662
+ if (!animation(animationId)) {
1663
+ for (const [trackId, value] of tracks) if (value) diagnostics.push(error(`animations.${animationId}.tracks.${trackId}`, "ANIMATION_NOT_FOUND", `animation ${animationId} does not exist`));
1664
+ continue;
1665
+ }
1666
+ for (const [trackId, value] of tracks) {
1667
+ if (!value) continue;
1668
+ diagnostics.push(...validateTrack(`animations.${animationId}.tracks.${trackId}`, animationId, value, item, animation, track, validationState));
1669
+ }
1670
+ }
1671
+ if (diagnostics.length) return {
1672
+ ok: false,
1673
+ diagnostics
1674
+ };
1675
+ const before = emptyDelta();
1676
+ const after = emptyDelta();
1677
+ if (nextSettings && !boardJsonEquals(nextSettings, state.settings())) {
1678
+ before.board = state.settings();
1679
+ after.board = nextSettings;
1680
+ }
1681
+ for (const [id, value] of nextItems) {
1682
+ const previous = state.item(id) ?? null;
1683
+ if (boardJsonEquals(previous, value)) continue;
1684
+ before.items[id] = previous;
1685
+ after.items[id] = value;
1686
+ }
1687
+ for (const [id, value] of nextAnimations) {
1688
+ before.animations[id] = state.animation(id) ?? null;
1689
+ after.animations[id] = value;
1690
+ }
1691
+ for (const [animationId, tracks] of nextTracks) for (const [trackId, value] of tracks) {
1692
+ const previous = state.track(animationId, trackId) ?? null;
1693
+ if (boardJsonEquals(previous, value)) continue;
1694
+ slot(before.tracks, animationId)[trackId] = previous;
1695
+ slot(after.tracks, animationId)[trackId] = value;
1696
+ }
1697
+ return {
1698
+ ok: true,
1699
+ before: withoutEmpty(before),
1700
+ after: withoutEmpty(after),
1701
+ diagnostics: []
1702
+ };
1703
+ }
1704
+ function validateTrack(path, animationId, value, item, animation, track, state) {
1705
+ const diagnostics = [];
1706
+ let targetType;
1707
+ if (value.target === "camera") targetType = BOARD_CAMERA_TARGET;
1708
+ else if (item(value.target)) targetType = item(value.target).type;
1709
+ else if (animation(value.target)) {
1710
+ targetType = "animation";
1711
+ if (value.target === animationId || nestsInto(value.target, animationId, animation, track, state)) return [error(`${path}.target`, "ANIMATION_CYCLE", `${animationId} cannot contain itself through ${value.target}`)];
1712
+ } else return [error(`${path}.target`, "TARGET_NOT_FOUND", `target ${value.target} is not an item, animation or camera`)];
1713
+ const resolved = resolveBoardProperty(targetType, value.property);
1714
+ if (!resolved.ok) return [error(`${path}.property`, "INVALID_PROPERTY", resolved.message)];
1715
+ for (const [index, keyframe] of value.keyframes.entries()) {
1716
+ const parsed = resolved.spec.schema.safeParse(keyframe.value);
1717
+ if (!parsed.success) {
1718
+ diagnostics.push(error(`${path}.keyframes.${index}.value`, "INVALID_KEYFRAME", parsed.error.issues[0]?.message ?? "invalid value"));
1719
+ continue;
1720
+ }
1721
+ if (value.property === "focus" && typeof keyframe.value === "string" && !item(keyframe.value)) diagnostics.push(error(`${path}.keyframes.${index}.value`, "TARGET_NOT_FOUND", `focus item ${keyframe.value} does not exist`));
1722
+ }
1723
+ if (value.interpolation === "spline" && resolved.spec.kind !== "vec2") diagnostics.push(error(`${path}.interpolation`, "INVALID_TRACK", "spline interpolation needs a position-like property"));
1724
+ return diagnostics;
1725
+ }
1726
+ function nestsInto(from, into, animation, track, state) {
1727
+ const seen = /* @__PURE__ */ new Set();
1728
+ const stack = [from];
1729
+ while (stack.length) {
1730
+ const current = stack.pop();
1731
+ if (current === into) return true;
1732
+ if (seen.has(current) || !animation(current)) continue;
1733
+ seen.add(current);
1734
+ for (const trackId of state.trackIds(current)) {
1735
+ const value = track(current, trackId);
1736
+ if (value && value.property === "time" && animation(value.target)) stack.push(value.target);
1737
+ }
1738
+ }
1739
+ return false;
1740
+ }
1741
+ function createDocumentState(document) {
1742
+ let children = null;
1743
+ let binders = null;
1744
+ let refs = null;
1745
+ const index = () => {
1746
+ if (children) return;
1747
+ children = /* @__PURE__ */ new Map();
1748
+ binders = /* @__PURE__ */ new Map();
1749
+ refs = /* @__PURE__ */ new Map();
1750
+ for (const [id, value] of Object.entries(document.items)) {
1751
+ if (value.parent) children.set(value.parent, [...children.get(value.parent) ?? [], id]);
1752
+ for (const bound of arrowBindings(value)) binders.set(bound, [...binders.get(bound) ?? [], id]);
1753
+ }
1754
+ for (const [animationId, value] of Object.entries(document.animations)) for (const [trackId, track] of Object.entries(value.tracks)) for (const ref of trackReferences(track)) refs.set(ref, [...refs.get(ref) ?? [], {
1755
+ animation: animationId,
1756
+ track: trackId
1757
+ }]);
1758
+ };
1759
+ return {
1760
+ settings: () => document.board,
1761
+ item: (id) => document.items[id],
1762
+ animation: (id) => {
1763
+ const value = document.animations[id];
1764
+ if (!value) return void 0;
1765
+ const { tracks: _tracks, ...header } = value;
1766
+ return header;
1767
+ },
1768
+ track: (animationId, trackId) => document.animations[animationId]?.tracks[trackId],
1769
+ trackIds: (animationId) => Object.keys(document.animations[animationId]?.tracks ?? {}),
1770
+ children: (id) => {
1771
+ index();
1772
+ return children?.get(id) ?? [];
1773
+ },
1774
+ binders: (id) => {
1775
+ index();
1776
+ return binders?.get(id) ?? [];
1777
+ },
1778
+ trackRefs: (id) => {
1779
+ index();
1780
+ return refs?.get(id) ?? [];
1781
+ },
1782
+ maxZ: (parent) => {
1783
+ let max = 0;
1784
+ for (const value of Object.values(document.items)) if (value.parent === parent && (value.z ?? 0) > max) max = value.z ?? 0;
1785
+ return max;
1786
+ }
1787
+ };
1788
+ }
1789
+ function applyBoardDelta(document, delta) {
1790
+ const items = delta.items ? { ...document.items } : document.items;
1791
+ for (const [id, value] of Object.entries(delta.items ?? {})) if (value) items[id] = value;
1792
+ else delete items[id];
1793
+ const animations = delta.animations || delta.tracks ? { ...document.animations } : document.animations;
1794
+ for (const [id, value] of Object.entries(delta.animations ?? {})) if (value) animations[id] = {
1795
+ ...value,
1796
+ tracks: animations[id]?.tracks ?? {}
1797
+ };
1798
+ else delete animations[id];
1799
+ for (const [animationId, tracks] of Object.entries(delta.tracks ?? {})) {
1800
+ const current = animations[animationId];
1801
+ if (!current) continue;
1802
+ const nextTracks = { ...current.tracks };
1803
+ for (const [trackId, value] of Object.entries(tracks)) if (value) nextTracks[trackId] = value;
1804
+ else delete nextTracks[trackId];
1805
+ animations[animationId] = {
1806
+ ...current,
1807
+ tracks: nextTracks
1808
+ };
1809
+ }
1810
+ return {
1811
+ board: delta.board ?? document.board,
1812
+ items,
1813
+ animations
1814
+ };
1815
+ }
1816
+ function applyBoardPatchToDocument(document, patch, options = {}) {
1817
+ const result = applyBoardPatch(createDocumentState(document), patch, options);
1818
+ return result.ok ? {
1819
+ ok: true,
1820
+ document: applyBoardDelta(document, result.after),
1821
+ before: result.before,
1822
+ after: result.after
1823
+ } : result;
1824
+ }
1825
+ function parseBoardDocument(input) {
1826
+ if (!isRecord$2(input)) return {
1827
+ ok: false,
1828
+ diagnostics: [error("", "INVALID_DOCUMENT", "expected an object")]
1829
+ };
1830
+ const result = applyBoardPatchToDocument({
1831
+ board: BoardSettingsSchema.parse({}),
1832
+ items: {},
1833
+ animations: {}
1834
+ }, {
1835
+ ...isRecord$2(input.board) ? { board: input.board } : {},
1836
+ ...isRecord$2(input.items) ? { items: input.items } : {},
1837
+ ...isRecord$2(input.animations) ? { animations: input.animations } : {}
1838
+ });
1839
+ return result.ok ? {
1840
+ ok: true,
1841
+ document: result.document
1842
+ } : result;
1843
+ }
1844
+ //#endregion
766
1845
  //#region ../protocol/dist/model/metrics.js
767
1846
  const count = z.number().finite().nonnegative();
768
1847
  const usageSchema = z.object({
@@ -1447,7 +2526,7 @@ var CronJobsApi = class {
1447
2526
  //#region src/apis/generations.ts
1448
2527
  const DEFAULT_INTERVAL_MS = 1500;
1449
2528
  const DEFAULT_TIMEOUT_MS = 18e5;
1450
- function sleep$1(ms, signal) {
2529
+ function sleep$2(ms, signal) {
1451
2530
  if (signal?.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Generation wait aborted"));
1452
2531
  return new Promise((resolve, reject) => {
1453
2532
  const cleanup = () => signal?.removeEventListener("abort", onAbort);
@@ -1512,7 +2591,7 @@ var GenerationsApi = class {
1512
2591
  }
1513
2592
  const remainingMs = timeoutMs - (Date.now() - startedAt);
1514
2593
  if (remainingMs <= 0) throw new Error(`Generation task timed out after ${timeoutMs}ms`);
1515
- await sleep$1(Math.min(intervalMs, remainingMs), options.signal);
2594
+ await sleep$2(Math.min(intervalMs, remainingMs), options.signal);
1516
2595
  }
1517
2596
  }
1518
2597
  async createAndWait(request, options) {
@@ -1881,6 +2960,67 @@ function createBoardEntityId() {
1881
2960
  return `c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
1882
2961
  }
1883
2962
  //#endregion
2963
+ //#region src/board/model/json.ts
2964
+ function sameJson(a, b) {
2965
+ if (a === b) return true;
2966
+ if (!a || !b || typeof a !== "object" || typeof b !== "object") return false;
2967
+ if (Array.isArray(a) || Array.isArray(b)) {
2968
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
2969
+ for (let index = 0; index < a.length; index += 1) if (!sameJson(a[index], b[index])) return false;
2970
+ return true;
2971
+ }
2972
+ const left = a;
2973
+ const right = b;
2974
+ let members = 0;
2975
+ for (const key of Object.keys(left)) {
2976
+ if (left[key] === void 0) continue;
2977
+ members += 1;
2978
+ if (!sameJson(left[key], right[key])) return false;
2979
+ }
2980
+ for (const key of Object.keys(right)) if (right[key] !== void 0) members -= 1;
2981
+ return members === 0;
2982
+ }
2983
+ //#endregion
2984
+ //#region src/board/model/document-share.ts
2985
+ function shareRecord(previous, next) {
2986
+ let same = Object.keys(previous).length === Object.keys(next).length;
2987
+ const shared = {};
2988
+ for (const [id, value] of Object.entries(next)) {
2989
+ const current = previous[id];
2990
+ const kept = current !== void 0 && sameJson(current, value) ? current : value;
2991
+ if (kept !== current) same = false;
2992
+ shared[id] = kept;
2993
+ }
2994
+ return same ? previous : shared;
2995
+ }
2996
+ function shareBoardDocument(previous, next) {
2997
+ if (!previous) return next;
2998
+ const board = sameJson(previous.board, next.board) ? previous.board : next.board;
2999
+ const items = shareRecord(previous.items, next.items);
3000
+ const animations = shareRecord(previous.animations, next.animations);
3001
+ if (board === previous.board && items === previous.items && animations === previous.animations) return previous;
3002
+ return {
3003
+ board,
3004
+ items,
3005
+ animations
3006
+ };
3007
+ }
3008
+ //#endregion
3009
+ //#region src/board/model/read.ts
3010
+ function readBoardDocument(result, base = null) {
3011
+ if (result.changes) {
3012
+ if (!base) throw new Error(`Board ${result.id} returned changes without a document to apply them to.`);
3013
+ return result.changes.reduce(applyBoardDelta, base);
3014
+ }
3015
+ const parsed = parseBoardDocument({
3016
+ board: result.board,
3017
+ items: result.items ?? {},
3018
+ animations: result.animations ?? {}
3019
+ });
3020
+ if (!parsed.ok) throw new Error(`Board ${result.id} could not be read.`);
3021
+ return shareBoardDocument(base, parsed.document);
3022
+ }
3023
+ //#endregion
1884
3024
  //#region src/board/client/index.ts
1885
3025
  var BoardRealtimeClient = class {
1886
3026
  websocketClient;
@@ -1895,7 +3035,7 @@ var BoardRealtimeClient = class {
1895
3035
  if (!this.websocketClient) throw new Error("realtime transport is not configured for this client");
1896
3036
  ensureRealtimeConnected(this.websocketClient);
1897
3037
  const room = getRealtimeBoardRoom(this.boardId);
1898
- const releaseRoom = this.websocketClient.retainRooms([getRealtimeSpaceRoom(this.spaceId), room]);
3038
+ const releaseRoom = this.websocketClient.retainRooms([room]);
1899
3039
  const unsubscribe = this.websocketClient.on("event", (event) => {
1900
3040
  if (event.spaceId !== this.spaceId) return;
1901
3041
  if (event.type === "board.changed" && event.payload.boardId === this.boardId) {
@@ -1917,12 +3057,14 @@ var BoardRealtimeClient = class {
1917
3057
  }
1918
3058
  });
1919
3059
  const resync = handlers.resync;
1920
- const stopResync = resync ? this.websocketClient.on("subscribed", ({ rooms }) => {
3060
+ const stopResync = resync ? [this.websocketClient.on("subscribed", ({ rooms }) => {
1921
3061
  if (rooms.includes(room)) resync();
1922
- }) : null;
3062
+ }), this.websocketClient.on("subscribeError", ({ rejected }) => {
3063
+ if (rejected.some((entry) => entry.room === room && entry.code !== "UNAVAILABLE")) resync();
3064
+ })] : [];
1923
3065
  return () => {
1924
3066
  unsubscribe();
1925
- stopResync?.();
3067
+ for (const stop of stopResync) stop();
1926
3068
  releaseRoom();
1927
3069
  };
1928
3070
  }
@@ -1951,7 +3093,7 @@ var BoardClient = class {
1951
3093
  /** Read the Board, or part of it. Every value is complete, defaults included. */
1952
3094
  get(input = {}, customFetch) {
1953
3095
  const params = new URLSearchParams();
1954
- if (input.only?.length) params.set("only", input.only.join(","));
3096
+ if (input.only) params.set("only", input.only.join(","));
1955
3097
  if (input.rect) params.set("rect", [
1956
3098
  input.rect.x,
1957
3099
  input.rect.y,
@@ -1963,9 +3105,25 @@ var BoardClient = class {
1963
3105
  if (input.animations?.length) params.set("animations", input.animations.join(","));
1964
3106
  if (input.limit !== void 0) params.set("limit", String(input.limit));
1965
3107
  if (input.cursor) params.set("cursor", input.cursor);
3108
+ if (input.since !== void 0) params.set("since", String(input.since));
1966
3109
  const query = params.toString();
1967
3110
  return this.http.request(`${this.path}${query ? `?${query}` : ""}`, { fetch: customFetch });
1968
3111
  }
3112
+ /** Bring a cached document up to date in one request that carries only what changed since its version. */
3113
+ async sync(cached, customFetch) {
3114
+ const result = await this.get(cached ? { since: cached.version } : {}, customFetch);
3115
+ const document = readBoardDocument(result, cached?.document);
3116
+ const { id, title, version, updatedAt, playback } = result;
3117
+ return {
3118
+ id,
3119
+ title,
3120
+ version,
3121
+ updatedAt,
3122
+ playback,
3123
+ document,
3124
+ changed: document !== cached?.document || version !== cached.version
3125
+ };
3126
+ }
1969
3127
  /** Merge a patch into the Board: fields merge, `null` deletes. */
1970
3128
  apply(patch, options = {}) {
1971
3129
  return this.http.request(`${this.path}/apply`, {
@@ -2088,6 +3246,12 @@ var SpaceBoardsApi = class {
2088
3246
  boardId
2089
3247
  });
2090
3248
  }
3249
+ assetSource() {
3250
+ return {
3251
+ resolveFile: (path, options) => this.files.resolve(path, options),
3252
+ subscribe: (listener) => this.files.onUrlChange(listener)
3253
+ };
3254
+ }
2091
3255
  async open(target, customFetch) {
2092
3256
  return this.byId(isBoardPath(target) ? await this.resolveId(target, customFetch) : target);
2093
3257
  }
@@ -3000,7 +4164,7 @@ function getPatchMessageIdentity(event) {
3000
4164
  messageOrdinal: typeof event.payload.messageOrdinal === "number" ? event.payload.messageOrdinal : null
3001
4165
  };
3002
4166
  }
3003
- async function withTimeout(promise, timeoutMs) {
4167
+ async function withTimeout$1(promise, timeoutMs) {
3004
4168
  let timeout = null;
3005
4169
  try {
3006
4170
  return await Promise.race([promise, new Promise((_, reject) => {
@@ -3169,7 +4333,7 @@ var SessionGenerationStreamClient = class SessionGenerationStreamClient {
3169
4333
  async loadSnapshot() {
3170
4334
  if (!this.fetchStreamSnapshot) return null;
3171
4335
  try {
3172
- return (await withTimeout(this.fetchStreamSnapshot(), SNAPSHOT_RECOVERY_TIMEOUT_MS)).snapshot;
4336
+ return (await withTimeout$1(this.fetchStreamSnapshot(), SNAPSHOT_RECOVERY_TIMEOUT_MS)).snapshot;
3173
4337
  } catch (error) {
3174
4338
  console.warn("[SessionGenerationStreamClient] Failed to recover stream snapshot:", error);
3175
4339
  return null;
@@ -3543,6 +4707,638 @@ function buildSpaceInvitePath(input) {
3543
4707
  return `${buildSpacePath(input)}/join/${inviteCode}`;
3544
4708
  }
3545
4709
  //#endregion
4710
+ //#region src/apis/space-file-reads.ts
4711
+ const BATCH_SIZE = 32;
4712
+ const BATCH_CONCURRENCY = 2;
4713
+ const SINGLE_CONCURRENCY = 4;
4714
+ const BACKGROUND_DELAY_MS = 1500;
4715
+ const READ_TIMEOUT_MS = 3e4;
4716
+ function createLimiter(limit) {
4717
+ let active = 0;
4718
+ const lanes = [[], []];
4719
+ function pump() {
4720
+ while (active < limit) {
4721
+ const job = lanes[0].shift() ?? lanes[1].shift();
4722
+ if (!job) return;
4723
+ active += 1;
4724
+ job();
4725
+ }
4726
+ }
4727
+ return (run, background = false) => new Promise((resolve, reject) => {
4728
+ lanes[background ? 1 : 0].push(() => {
4729
+ run().then(resolve, reject).finally(() => {
4730
+ active -= 1;
4731
+ pump();
4732
+ });
4733
+ });
4734
+ pump();
4735
+ });
4736
+ }
4737
+ const fileReadPath = (spaceId, path) => `/api/spaces/${spaceId}/fs/file?${new URLSearchParams({ path })}`;
4738
+ function createSpaceFileReads(transport) {
4739
+ const queues = [/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map()];
4740
+ const timers = [null, null];
4741
+ const inFlight = /* @__PURE__ */ new Map();
4742
+ const batches = createLimiter(BATCH_CONCURRENCY);
4743
+ const singles = createLimiter(SINGLE_CONCURRENCY);
4744
+ const readOne = (spaceId, path, init = {}) => withTimeout((signal) => transport.requestRevalidated(fileReadPath(spaceId, path), {
4745
+ ...init,
4746
+ signal
4747
+ }));
4748
+ function schedule(lane) {
4749
+ timers[lane] ??= setTimeout(() => {
4750
+ timers[lane] = null;
4751
+ flush(lane);
4752
+ }, lane ? BACKGROUND_DELAY_MS : 0);
4753
+ }
4754
+ function settle(pending, outcome) {
4755
+ if (inFlight.get(pending.key) === pending) inFlight.delete(pending.key);
4756
+ if ("file" in outcome) pending.resolve(outcome.file);
4757
+ else pending.reject(outcome.error);
4758
+ }
4759
+ function flush(lane) {
4760
+ const groups = /* @__PURE__ */ new Map();
4761
+ for (const pending of queues[lane].values()) {
4762
+ inFlight.set(pending.key, pending);
4763
+ const id = `${pending.token}\u0000${pending.spaceId}`;
4764
+ const group = groups.get(id);
4765
+ if (group) group.push(pending);
4766
+ else groups.set(id, [pending]);
4767
+ }
4768
+ queues[lane].clear();
4769
+ for (const group of groups.values()) {
4770
+ if (group.length === 1) {
4771
+ readSingle(group[0], lane === 1);
4772
+ continue;
4773
+ }
4774
+ for (let index = 0; index < group.length; index += BATCH_SIZE) batches(() => readBatch(group.slice(index, index + BATCH_SIZE)), lane === 1);
4775
+ }
4776
+ }
4777
+ async function readSingle(pending, background = false) {
4778
+ const { spaceId, path, token } = pending;
4779
+ try {
4780
+ settle(pending, { file: await singles(() => readOne(spaceId, path, { accessToken: token }), background) });
4781
+ } catch (error) {
4782
+ settle(pending, { error });
4783
+ }
4784
+ }
4785
+ async function readBatch(chunk) {
4786
+ const { spaceId, token } = chunk[0];
4787
+ let response;
4788
+ try {
4789
+ response = await withTimeout((signal) => transport.request(`/api/spaces/${spaceId}/fs/files`, {
4790
+ method: "POST",
4791
+ headers: { "Content-Type": "application/json" },
4792
+ body: JSON.stringify({ paths: chunk.map((pending) => pending.path) }),
4793
+ accessToken: token,
4794
+ signal
4795
+ }));
4796
+ } catch {
4797
+ await Promise.all(chunk.map((pending) => readSingle(pending)));
4798
+ return;
4799
+ }
4800
+ const byPath = new Map(chunk.map((pending) => [pending.path, pending]));
4801
+ const take = (path) => {
4802
+ const pending = byPath.get(path);
4803
+ byPath.delete(path);
4804
+ return pending;
4805
+ };
4806
+ for (const file of [...response.files, ...response.preparing ?? []]) {
4807
+ const pending = take(file.path);
4808
+ if (pending) settle(pending, { file });
4809
+ }
4810
+ for (const { path, status, code, message } of response.errors) {
4811
+ const pending = take(path);
4812
+ if (pending) settle(pending, { error: new HttpError(message, status, {
4813
+ code,
4814
+ message
4815
+ }) });
4816
+ }
4817
+ await Promise.all([...byPath.values()].map((pending) => readSingle(pending)));
4818
+ }
4819
+ function share(token, spaceId, path, background) {
4820
+ const key = `${spaceId}\u0000${path}\u0000${token}`;
4821
+ let pending = inFlight.get(key) ?? queues[0].get(key);
4822
+ const deferred = queues[1].get(key);
4823
+ if (!pending && deferred && !background) {
4824
+ queues[1].delete(key);
4825
+ queues[0].set(key, deferred);
4826
+ schedule(0);
4827
+ }
4828
+ pending ??= deferred;
4829
+ if (!pending) {
4830
+ let resolve;
4831
+ let reject;
4832
+ const promise = new Promise((done, fail) => {
4833
+ resolve = done;
4834
+ reject = fail;
4835
+ });
4836
+ const lane = background ? 1 : 0;
4837
+ pending = {
4838
+ key,
4839
+ token,
4840
+ spaceId,
4841
+ path,
4842
+ promise,
4843
+ resolve,
4844
+ reject
4845
+ };
4846
+ queues[lane].set(key, pending);
4847
+ schedule(lane);
4848
+ }
4849
+ return pending.promise;
4850
+ }
4851
+ return {
4852
+ async read(spaceId, path, options = {}) {
4853
+ const own = options.token === void 0;
4854
+ const token = own ? await transport.credential() : options.token ?? null;
4855
+ try {
4856
+ return await abortable(share(token, spaceId, path, options.background === true), options.signal);
4857
+ } catch (error) {
4858
+ if (!own || !isUnauthorized(error)) throw error;
4859
+ return abortable(readOne(spaceId, path), options.signal);
4860
+ }
4861
+ },
4862
+ invalidate(spaceId, paths) {
4863
+ const stale = new Set([...paths].map((path) => `${spaceId}\u0000${path}`));
4864
+ for (const key of inFlight.keys()) if (stale.has(key.slice(0, key.lastIndexOf("\0")))) inFlight.delete(key);
4865
+ }
4866
+ };
4867
+ }
4868
+ const isUnauthorized = (error) => error instanceof HttpError && error.status === 401;
4869
+ async function withTimeout(run) {
4870
+ const controller = new AbortController();
4871
+ const timer = setTimeout(() => controller.abort(new DOMException("Space file read timed out", "TimeoutError")), READ_TIMEOUT_MS);
4872
+ try {
4873
+ return await run(controller.signal);
4874
+ } finally {
4875
+ clearTimeout(timer);
4876
+ }
4877
+ }
4878
+ function abortable(promise, signal) {
4879
+ if (!signal) return promise;
4880
+ if (signal.aborted) return Promise.reject(signal.reason);
4881
+ return new Promise((resolve, reject) => {
4882
+ const abort = () => reject(signal.reason);
4883
+ signal.addEventListener("abort", abort, { once: true });
4884
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
4885
+ });
4886
+ }
4887
+ //#endregion
4888
+ //#region src/indexeddb.ts
4889
+ const OPEN_TIMEOUT_MS = 3e3;
4890
+ const connections = /* @__PURE__ */ new Map();
4891
+ function open(name, version, upgrade) {
4892
+ const existing = connections.get(name);
4893
+ if (existing) return existing;
4894
+ const opening = new Promise((resolve, reject) => {
4895
+ const factory = globalThis.indexedDB;
4896
+ if (!factory) {
4897
+ reject(/* @__PURE__ */ new Error("IndexedDB is unavailable."));
4898
+ return;
4899
+ }
4900
+ let late = false;
4901
+ const timer = setTimeout(() => {
4902
+ late = true;
4903
+ reject(/* @__PURE__ */ new Error("IndexedDB did not open in time."));
4904
+ }, OPEN_TIMEOUT_MS);
4905
+ const request = factory.open(name, version);
4906
+ request.onupgradeneeded = () => upgrade(request.result);
4907
+ request.onsuccess = () => {
4908
+ clearTimeout(timer);
4909
+ const db = request.result;
4910
+ if (late) {
4911
+ db.close();
4912
+ return;
4913
+ }
4914
+ const forget = () => {
4915
+ if (connections.get(name) === opening) connections.delete(name);
4916
+ };
4917
+ db.onversionchange = () => {
4918
+ db.close();
4919
+ forget();
4920
+ };
4921
+ db.onclose = forget;
4922
+ resolve(db);
4923
+ };
4924
+ request.onerror = () => {
4925
+ clearTimeout(timer);
4926
+ reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB failed to open."));
4927
+ };
4928
+ });
4929
+ connections.set(name, opening);
4930
+ opening.catch(() => {
4931
+ if (connections.get(name) === opening) connections.delete(name);
4932
+ });
4933
+ return opening;
4934
+ }
4935
+ function indexedDb(name, version, upgrade) {
4936
+ async function transact(stores, mode, work, retry = true) {
4937
+ const db = await open(name, version, upgrade);
4938
+ let tx;
4939
+ try {
4940
+ tx = db.transaction(stores, mode);
4941
+ } catch (error) {
4942
+ if (!retry) throw error;
4943
+ db.close();
4944
+ connections.delete(name);
4945
+ return transact(stores, mode, work, false);
4946
+ }
4947
+ return new Promise((resolve, reject) => {
4948
+ const result = work(tx);
4949
+ tx.oncomplete = () => resolve(result());
4950
+ tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
4951
+ tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted."));
4952
+ });
4953
+ }
4954
+ return { transact: (stores, mode, work) => transact(stores, mode, work) };
4955
+ }
4956
+ function deleteByIndex(store, index, key) {
4957
+ const request = store.index(index).openKeyCursor(IDBKeyRange.only(key));
4958
+ request.onsuccess = () => {
4959
+ const cursor = request.result;
4960
+ if (!cursor) return;
4961
+ store.delete(cursor.primaryKey);
4962
+ cursor.continue();
4963
+ };
4964
+ }
4965
+ //#endregion
4966
+ //#region src/apis/space-file-url-store.ts
4967
+ const DATABASE = "cohub-file-urls";
4968
+ const STORE = "urls";
4969
+ const MAX_ENTRIES = 2e4;
4970
+ const TRIM_TO = 18e3;
4971
+ const TRIM_EVERY_WRITES = 500;
4972
+ const database = indexedDb(DATABASE, 1, (db) => {
4973
+ const store = db.createObjectStore(STORE, { keyPath: "key" });
4974
+ store.createIndex("scope", "scope");
4975
+ store.createIndex("verifiedAt", "verifiedAt");
4976
+ });
4977
+ const recordKey = (scope, spaceId, path) => `${scope}\u0000${spaceId}\u0000${path}`;
4978
+ let writes = 0;
4979
+ const fileUrlStore = {
4980
+ read(scope, spaceId, paths) {
4981
+ return database.transact([STORE], "readonly", (tx) => {
4982
+ const store = tx.objectStore(STORE);
4983
+ const requests = paths.map((path) => [path, store.get(recordKey(scope, spaceId, path))]);
4984
+ return () => {
4985
+ const found = /* @__PURE__ */ new Map();
4986
+ for (const [path, request] of requests) {
4987
+ const record = request.result;
4988
+ if (record) found.set(path, {
4989
+ url: record.url,
4990
+ verifiedAt: record.verifiedAt,
4991
+ media: record.media
4992
+ });
4993
+ }
4994
+ return found;
4995
+ };
4996
+ });
4997
+ },
4998
+ async write(scope, spaceId, changes) {
4999
+ await database.transact([STORE], "readwrite", (tx) => {
5000
+ const store = tx.objectStore(STORE);
5001
+ for (const [path, stored] of changes.put) store.put({
5002
+ ...stored,
5003
+ key: recordKey(scope, spaceId, path),
5004
+ scope
5005
+ });
5006
+ for (const path of changes.remove) store.delete(recordKey(scope, spaceId, path));
5007
+ return () => void 0;
5008
+ });
5009
+ writes += changes.put.size;
5010
+ if (writes < TRIM_EVERY_WRITES) return;
5011
+ writes = 0;
5012
+ await trim();
5013
+ }
5014
+ };
5015
+ function trim() {
5016
+ return database.transact([STORE], "readwrite", (tx) => {
5017
+ const store = tx.objectStore(STORE);
5018
+ const count = store.count();
5019
+ count.onsuccess = () => {
5020
+ let excess = count.result > MAX_ENTRIES ? count.result - TRIM_TO : 0;
5021
+ if (excess === 0) return;
5022
+ const cursor = store.index("verifiedAt").openKeyCursor();
5023
+ cursor.onsuccess = () => {
5024
+ const current = cursor.result;
5025
+ if (!current || excess-- <= 0) return;
5026
+ store.delete(current.primaryKey);
5027
+ current.continue();
5028
+ };
5029
+ };
5030
+ return () => void 0;
5031
+ });
5032
+ }
5033
+ async function clearFileUrlCache(options = {}) {
5034
+ if (!globalThis.indexedDB) return;
5035
+ await database.transact([STORE], "readwrite", (tx) => {
5036
+ const store = tx.objectStore(STORE);
5037
+ if (options.scope === void 0) store.clear();
5038
+ else deleteByIndex(store, "scope", options.scope);
5039
+ return () => void 0;
5040
+ });
5041
+ }
5042
+ //#endregion
5043
+ //#region src/apis/space-file-urls.ts
5044
+ const MEMORY_LIMIT = 4096;
5045
+ const INLINE_MEMORY_MAX_CHARS = 262144;
5046
+ const PREPARING_CONCURRENCY = 4;
5047
+ const PREPARING_TIMEOUT_MS = 15e3;
5048
+ /** Well inside the fs CDN manifest's 6.5 days; CDN URLs carry the file version and are not signed. */
5049
+ const STORED_MAX_AGE_MS = 2592e5;
5050
+ const UNWATCHED_TRUST_MS = 6e4;
5051
+ function inlineFileDataUrl(file) {
5052
+ const mimeType = file.mimeType ?? "application/octet-stream";
5053
+ return file.encoding === "base64" ? `data:${mimeType};base64,${file.content}` : `data:${mimeType};charset=utf-8,${encodeURIComponent(file.content)}`;
5054
+ }
5055
+ function resolvedOf(file) {
5056
+ if (file.delivery === "url" && file.url) return {
5057
+ url: file.url,
5058
+ inline: false,
5059
+ ...file.media ? { media: file.media } : {}
5060
+ };
5061
+ return {
5062
+ url: inlineFileDataUrl(file),
5063
+ inline: true
5064
+ };
5065
+ }
5066
+ const isGone = (error) => error instanceof HttpError && (error.status === 404 || error.status === 410);
5067
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5068
+ function remember(map, key, value) {
5069
+ map.delete(key);
5070
+ map.set(key, value);
5071
+ if (map.size > MEMORY_LIMIT) map.delete(map.keys().next().value);
5072
+ }
5073
+ function createSpaceFileUrls(options) {
5074
+ const { reads, cache } = options;
5075
+ const now = options.now ?? Date.now;
5076
+ const memory = /* @__PURE__ */ new Map();
5077
+ const invalidatedAt = /* @__PURE__ */ new Map();
5078
+ const epochs = /* @__PURE__ */ new Map();
5079
+ const serverReads = /* @__PURE__ */ new Map();
5080
+ const localReads = /* @__PURE__ */ new Map();
5081
+ const listeners = /* @__PURE__ */ new Set();
5082
+ const preparing = createLimiter(PREPARING_CONCURRENCY);
5083
+ let localBatch = null;
5084
+ let writes = null;
5085
+ let lastScope = null;
5086
+ const keyOf = (spaceId, path) => `${spaceId}\u0000${path}`;
5087
+ const epochOf = (spaceId) => epochs.get(spaceId) ?? 0;
5088
+ const persists = (scope) => cache && scope !== null && Boolean(globalThis.indexedDB);
5089
+ function persist(scope, spaceId, path, entry) {
5090
+ if (!writes) {
5091
+ const batch = /* @__PURE__ */ new Map();
5092
+ writes = batch;
5093
+ setTimeout(() => {
5094
+ writes = null;
5095
+ for (const [id, { put, remove }] of batch) {
5096
+ const [batchScope, batchSpace] = id.split("\0");
5097
+ fileUrlStore.write(batchScope, batchSpace, {
5098
+ put,
5099
+ remove
5100
+ }).catch(() => void 0);
5101
+ }
5102
+ }, 0);
5103
+ }
5104
+ const id = `${scope}\u0000${spaceId}`;
5105
+ const pending = writes.get(id) ?? {
5106
+ put: /* @__PURE__ */ new Map(),
5107
+ remove: /* @__PURE__ */ new Set()
5108
+ };
5109
+ writes.set(id, pending);
5110
+ if (entry) {
5111
+ pending.remove.delete(path);
5112
+ pending.put.set(path, {
5113
+ url: entry.url,
5114
+ verifiedAt: now(),
5115
+ ...entry.media ? { media: entry.media } : {}
5116
+ });
5117
+ } else {
5118
+ pending.put.delete(path);
5119
+ pending.remove.add(path);
5120
+ }
5121
+ }
5122
+ async function load(token, spaceId, path, background) {
5123
+ const first = await reads.read(spaceId, path, {
5124
+ background,
5125
+ token
5126
+ });
5127
+ if ("content" in first) return resolvedOf(first);
5128
+ return preparing(async () => {
5129
+ const deadline = now() + PREPARING_TIMEOUT_MS;
5130
+ let file = first;
5131
+ while (!("content" in file)) {
5132
+ const wait = Math.min(Math.max(250, Math.min(file.retryAfterMs, 2e3)), deadline - now());
5133
+ if (wait <= 0) return null;
5134
+ await sleep$1(wait);
5135
+ file = await reads.read(spaceId, path, { token });
5136
+ }
5137
+ return resolvedOf(file);
5138
+ }, background);
5139
+ }
5140
+ function server(token, scope, spaceId, path, background) {
5141
+ const key = keyOf(spaceId, path);
5142
+ const readKey = `${key}\u0000${token}`;
5143
+ const running = serverReads.get(readKey);
5144
+ if (running) {
5145
+ if (!background) reads.read(spaceId, path, { token }).catch(() => void 0);
5146
+ return running;
5147
+ }
5148
+ const epoch = epochOf(spaceId);
5149
+ const read = load(token, spaceId, path, background).then((resolved) => {
5150
+ const current = serverReads.get(readKey) === read;
5151
+ if (current) serverReads.delete(readKey);
5152
+ if (!resolved) return null;
5153
+ const entry = {
5154
+ ...resolved,
5155
+ scope,
5156
+ epoch,
5157
+ verifiedAt: now()
5158
+ };
5159
+ if (current && cache) adopt(key, spaceId, path, entry);
5160
+ return entry;
5161
+ }, (error) => {
5162
+ const current = serverReads.get(readKey) === read;
5163
+ if (current) serverReads.delete(readKey);
5164
+ if (current && cache && isGone(error)) {
5165
+ memory.delete(key);
5166
+ if (persists(scope)) persist(scope, spaceId, path, null);
5167
+ }
5168
+ throw error;
5169
+ });
5170
+ serverReads.set(readKey, read);
5171
+ return read;
5172
+ }
5173
+ function adopt(key, spaceId, path, entry) {
5174
+ const previous = memory.get(key);
5175
+ if (!entry.inline || entry.url.length <= INLINE_MEMORY_MAX_CHARS) remember(memory, key, entry);
5176
+ if (!entry.inline && persists(entry.scope)) persist(entry.scope, spaceId, path, entry);
5177
+ if (previous && previous.url !== entry.url) emit(spaceId, path);
5178
+ }
5179
+ function revalidate(token, scope, spaceId, path) {
5180
+ server(token, scope, spaceId, path, true).catch(() => void 0);
5181
+ }
5182
+ function readLocal(token, scope, spaceId, path) {
5183
+ const key = keyOf(spaceId, path);
5184
+ const running = localReads.get(key);
5185
+ if (running) return running;
5186
+ let resolve;
5187
+ const read = new Promise((done) => {
5188
+ resolve = done;
5189
+ });
5190
+ const forget = () => {
5191
+ if (localReads.get(key) === read) localReads.delete(key);
5192
+ };
5193
+ read.then(forget, forget);
5194
+ localReads.set(key, read);
5195
+ if (!localBatch) {
5196
+ const batch = [];
5197
+ localBatch = batch;
5198
+ queueMicrotask(() => {
5199
+ localBatch = null;
5200
+ const groups = /* @__PURE__ */ new Map();
5201
+ for (const item of batch) {
5202
+ const id = `${item.scope}\u0000${item.spaceId}`;
5203
+ const group = groups.get(id);
5204
+ if (group) group.push(item);
5205
+ else groups.set(id, [item]);
5206
+ }
5207
+ for (const group of groups.values()) readStored(group);
5208
+ });
5209
+ }
5210
+ localBatch.push({
5211
+ key,
5212
+ token,
5213
+ scope,
5214
+ spaceId,
5215
+ path,
5216
+ resolve
5217
+ });
5218
+ return read;
5219
+ }
5220
+ async function readStored(group) {
5221
+ const { scope, spaceId } = group[0];
5222
+ const records = await fileUrlStore.read(scope, spaceId, group.map((item) => item.path)).catch(() => /* @__PURE__ */ new Map());
5223
+ for (const { key, token, path, resolve } of group) {
5224
+ const record = records.get(path);
5225
+ if (!(record && now() - record.verifiedAt <= STORED_MAX_AGE_MS && (invalidatedAt.get(key) ?? -1) < record.verifiedAt)) {
5226
+ resolve(server(token, scope, spaceId, path, false));
5227
+ continue;
5228
+ }
5229
+ const entry = {
5230
+ url: record.url,
5231
+ inline: false,
5232
+ ...record.media ? { media: record.media } : {},
5233
+ scope,
5234
+ epoch: -1,
5235
+ verifiedAt: record.verifiedAt
5236
+ };
5237
+ if (!memory.has(key)) remember(memory, key, entry);
5238
+ revalidate(token, scope, spaceId, path);
5239
+ resolve(Promise.resolve(entry));
5240
+ }
5241
+ }
5242
+ function emit(spaceId, path) {
5243
+ for (const listener of listeners) listener(spaceId, path);
5244
+ }
5245
+ function invalidate(spaceId, paths) {
5246
+ const stale = [...new Set(paths)];
5247
+ if (stale.length === 0) return;
5248
+ reads.invalidate(spaceId, stale);
5249
+ const at = now();
5250
+ const buffered = lastScope === null ? void 0 : writes?.get(`${lastScope}\u0000${spaceId}`);
5251
+ const keys = /* @__PURE__ */ new Set();
5252
+ for (const path of stale) {
5253
+ const key = keyOf(spaceId, path);
5254
+ keys.add(key);
5255
+ memory.delete(key);
5256
+ localReads.delete(key);
5257
+ remember(invalidatedAt, key, at);
5258
+ buffered?.put.delete(path);
5259
+ }
5260
+ for (const readKey of serverReads.keys()) if (keys.has(readKey.slice(0, readKey.lastIndexOf("\0")))) serverReads.delete(readKey);
5261
+ if (persists(lastScope)) fileUrlStore.write(lastScope, spaceId, {
5262
+ put: /* @__PURE__ */ new Map(),
5263
+ remove: stale
5264
+ }).catch(() => void 0);
5265
+ }
5266
+ return {
5267
+ async resolve(spaceId, path, request = {}) {
5268
+ const purpose = request.purpose ?? "preview";
5269
+ const token = await options.credential();
5270
+ const scope = cache ? await options.scope().catch(() => null) : null;
5271
+ if (scope !== null) lastScope = scope;
5272
+ const key = keyOf(spaceId, path);
5273
+ const hit = cache && !request.fresh ? memory.get(key) : void 0;
5274
+ let entry;
5275
+ if (hit && hit.scope === scope) {
5276
+ remember(memory, key, hit);
5277
+ const unwatched = !options.watched(spaceId) && now() - hit.verifiedAt > UNWATCHED_TRUST_MS;
5278
+ if (hit.epoch !== epochOf(spaceId) || unwatched) revalidate(token, scope, spaceId, path);
5279
+ entry = hit;
5280
+ } else entry = await settle((persists(scope) && !request.fresh ? readLocal(token, scope, spaceId, path) : server(token, scope, spaceId, path, false)).catch(async (error) => {
5281
+ if (!isUnauthorized(error)) throw error;
5282
+ await reads.read(spaceId, path, { signal: request.signal });
5283
+ request.signal?.throwIfAborted();
5284
+ return server(await options.credential(), scope, spaceId, path, false);
5285
+ }), request);
5286
+ if (!entry || purpose === "playback" && entry.inline) return null;
5287
+ return entry.media ? {
5288
+ url: entry.url,
5289
+ media: entry.media
5290
+ } : { url: entry.url };
5291
+ },
5292
+ applyFsChange(spaceId, payload) {
5293
+ if (payload.resync) epochs.set(spaceId, epochOf(spaceId) + 1);
5294
+ const paths = /* @__PURE__ */ new Set();
5295
+ for (const change of payload.changes ?? []) {
5296
+ if (change.path) paths.add(change.path);
5297
+ if (change.oldPath) paths.add(change.oldPath);
5298
+ }
5299
+ invalidate(spaceId, paths);
5300
+ for (const path of paths) emit(spaceId, path);
5301
+ },
5302
+ subscribe(listener) {
5303
+ listeners.add(listener);
5304
+ return () => {
5305
+ listeners.delete(listener);
5306
+ };
5307
+ }
5308
+ };
5309
+ }
5310
+ function createSpaceFileServices(transport, websocket) {
5311
+ const reads = createSpaceFileReads(transport);
5312
+ const urls = createSpaceFileUrls({
5313
+ reads,
5314
+ credential: () => transport.credential(),
5315
+ scope: () => transport.cacheScope(),
5316
+ watched: (spaceId) => websocket?.isRoomLive(getRealtimeSpaceRoom(spaceId)) ?? false,
5317
+ cache: websocket !== null && typeof window !== "undefined"
5318
+ });
5319
+ websocket?.on("event", (event) => {
5320
+ if (event.type === "space.fs.changed" && event.spaceId) urls.applyFsChange(event.spaceId, event.payload);
5321
+ });
5322
+ return {
5323
+ reads,
5324
+ urls
5325
+ };
5326
+ }
5327
+ function settle(read, { timeoutMs = PREPARING_TIMEOUT_MS, signal }) {
5328
+ if (signal?.aborted) return Promise.reject(signal.reason);
5329
+ return new Promise((resolve, reject) => {
5330
+ const timer = setTimeout(() => finish(() => resolve(null)), Math.max(0, timeoutMs));
5331
+ const abort = () => finish(() => reject(signal?.reason));
5332
+ signal?.addEventListener("abort", abort, { once: true });
5333
+ function finish(done) {
5334
+ clearTimeout(timer);
5335
+ signal?.removeEventListener("abort", abort);
5336
+ done();
5337
+ }
5338
+ read.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
5339
+ });
5340
+ }
5341
+ //#endregion
3546
5342
  //#region src/apis/spaces.ts
3547
5343
  async function fetchTurnObject(url, fetchImpl = globalThis.fetch, signal) {
3548
5344
  const response = await fetchImpl(url, { signal });
@@ -3671,10 +5467,6 @@ var SpacesApi = class {
3671
5467
  });
3672
5468
  }
3673
5469
  };
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
5470
  function spaceFileAbortReason(signal) {
3679
5471
  return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
3680
5472
  }
@@ -3748,22 +5540,34 @@ var SpacePublicFilesApi = class {
3748
5540
  var SpaceFilesApi = class {
3749
5541
  transport;
3750
5542
  spaceId;
3751
- constructor(transport, spaceId) {
5543
+ services;
5544
+ constructor(transport, spaceId, services) {
3752
5545
  this.transport = transport;
3753
5546
  this.spaceId = spaceId;
5547
+ this.services = services;
3754
5548
  }
3755
5549
  list(path = "", options) {
3756
5550
  return listDirectory(this.transport, `/api/spaces/${this.spaceId}/fs/tree`, path, options);
3757
5551
  }
3758
5552
  read(path, customFetch, signal) {
3759
- const params = new URLSearchParams({ path });
3760
- return this.transport.requestRevalidated(`/api/spaces/${this.spaceId}/fs/file?${params.toString()}`, {
5553
+ if (customFetch) return this.transport.requestRevalidated(fileReadPath(this.spaceId, path), {
3761
5554
  fetch: customFetch,
3762
5555
  signal
3763
5556
  });
5557
+ return this.services.reads.read(this.spaceId, path, { signal });
3764
5558
  }
3765
5559
  /** Resolve a browser-ready file URL, waiting for CDN delivery when necessary. */
3766
- async resolveUrl(path, options = {}) {
5560
+ resolve(path, options = {}) {
5561
+ const { fetch: customFetch, ...request } = options;
5562
+ if (customFetch) return this.resolveThrough(path, customFetch, options);
5563
+ return this.services.urls.resolve(this.spaceId, path, request);
5564
+ }
5565
+ onUrlChange(listener) {
5566
+ return this.services.urls.subscribe((spaceId, path) => {
5567
+ if (spaceId === this.spaceId) listener(path);
5568
+ });
5569
+ }
5570
+ async resolveThrough(path, customFetch, options) {
3767
5571
  const purpose = options.purpose ?? "preview";
3768
5572
  const requestedTimeoutMs = options.timeoutMs ?? 15e3;
3769
5573
  const timeoutMs = Number.isFinite(requestedTimeoutMs) ? Math.max(0, requestedTimeoutMs) : 15e3;
@@ -3771,11 +5575,14 @@ var SpaceFilesApi = class {
3771
5575
  const deadline = createSpaceFileDeadlineSignal(options.signal, timeoutMs);
3772
5576
  try {
3773
5577
  while (true) {
3774
- const file = await this.read(path, options.fetch, deadline.signal);
5578
+ const file = await this.read(path, customFetch, deadline.signal);
3775
5579
  if (deadline.timedOut()) return null;
3776
5580
  if ("content" in file) {
3777
- if (file.delivery === "url" && file.url) return file.url;
3778
- return purpose === "preview" ? inlineFileDataUrl(file) : null;
5581
+ if (file.delivery === "url" && file.url) return file.media ? {
5582
+ url: file.url,
5583
+ media: file.media
5584
+ } : { url: file.url };
5585
+ return purpose === "preview" ? { url: inlineFileDataUrl(file) } : null;
3779
5586
  }
3780
5587
  const remainingMs = deadlineAt - Date.now();
3781
5588
  if (remainingMs <= 0) return null;
@@ -4049,14 +5856,9 @@ var SessionTurnsClient = class {
4049
5856
  });
4050
5857
  }
4051
5858
  };
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
5859
  const onRoomRejected = (websocketClient, room, onRejected) => websocketClient.on("event", (event) => {
4058
5860
  if (event.type !== "system.subscribe.error") return;
4059
- if (event.payload.rejected?.some((entry) => entry.room === room)) onRejected(event);
5861
+ if (event.payload.rejected?.some((entry) => entry.room === room && entry.code !== "UNAVAILABLE")) onRejected(event);
4060
5862
  });
4061
5863
  var SessionRealtimeClient = class {
4062
5864
  websocketClient;
@@ -4314,14 +6116,6 @@ var SpaceEventsApi = class {
4314
6116
  handler(event);
4315
6117
  return;
4316
6118
  }
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
6119
  if (type === "app.version.published" && event.type === "app.version.published") {
4326
6120
  handler(event);
4327
6121
  return;
@@ -4844,11 +6638,11 @@ var SpaceClient = class {
4844
6638
  boards;
4845
6639
  wallet;
4846
6640
  commerce;
4847
- constructor(id, transport, websocketClient) {
6641
+ constructor(id, transport, websocketClient, fileServices) {
4848
6642
  this.id = id;
4849
6643
  this.transport = transport;
4850
6644
  this.websocketClient = websocketClient;
4851
- this.files = new SpaceFilesApi(transport, id);
6645
+ this.files = new SpaceFilesApi(transport, id, fileServices);
4852
6646
  this.publicFiles = new SpacePublicFilesApi(transport, id);
4853
6647
  this.sessions = new SpaceSessionsApi(transport, id, websocketClient);
4854
6648
  this.turns = new SpaceTurnsApi(transport, id);
@@ -5812,6 +7606,7 @@ var CohubHttpClient = class {
5812
7606
  return this.desktop;
5813
7607
  }
5814
7608
  transport;
7609
+ fileServices;
5815
7610
  constructor(options = {}) {
5816
7611
  const apiBaseUrl = resolveApiBaseUrl(options);
5817
7612
  const executionToken = resolveExecutionToken();
@@ -5820,6 +7615,7 @@ var CohubHttpClient = class {
5820
7615
  getAccessToken: () => executionToken
5821
7616
  };
5822
7617
  this.transport = new HttpTransport(resolvedOptions);
7618
+ this.fileServices = createSpaceFileServices(this.transport, null);
5823
7619
  this.spaces = new SpacesApi(this.transport);
5824
7620
  this.channels = new ChannelsApi(this.transport);
5825
7621
  this.user = new UserApi(this.transport, apiBaseUrl, options.setStoredAuthToken, options.clearStoredAuthToken);
@@ -5842,9 +7638,9 @@ var CohubHttpClient = class {
5842
7638
  this.desktop = new DesktopCommandsApi(this.transport);
5843
7639
  }
5844
7640
  space(spaceId) {
5845
- return new SpaceClient(spaceId, this.transport, null);
7641
+ return new SpaceClient(spaceId, this.transport, null, this.fileServices);
5846
7642
  }
5847
7643
  };
5848
7644
  const createHttpClient = (options) => new CohubHttpClient(options);
5849
7645
  //#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 };
7646
+ export { defaultDesktopCommandTimeoutMs as $, readRtcTransportStats as A, ModelsApi as B, createSessionGenerationStreamClient as C, SpaceDisplaysApi as D, createSessionPatchReducer as E, 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, SessionAccessApi as P, DESKTOP_COMMAND_VERSION as Q, SkillsApi as R, SessionGenerationStreamClient as S, SessionPatchReducer as T, 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 _, BoardAnimationSchema as _t, WorkCommerceApi as a, NAVIGATION_ERROR_MESSAGE_MAX_LENGTH as at, buildSpaceInvitePath as b, parseBoardItem as bt, AppsApi as c, DISPLAY_INPUT_MAX_TEXT as ct, UserApi as d, splitDisplayText as dt, isDesktopCallMethod as et, TasksApi as f, parseBoardDocument 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, ReferralsApi as l, DISPLAY_SYSTEM_ACTIONS as lt, SpacePublicFilesApi as m, BOARD_MIME_TYPE as mt, createHttpClient as n, parseDesktopCommand as nt, DesktopCommandsApi as o, DISPLAY_ELEMENT_REF_PATTERN as ot, SpaceClient as p, BOARD_EXTENSION 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, BoardPatchSchema as vt, parseAssistantMessageCommit as w, buildSpacePath as x, normalizeBoardRemoteUrl as xt, PublicInviteApi as y, BoardTrackSchema as yt, PromptsApi as z };