@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.
- package/README.md +35 -13
- package/dist/board/editor/editor.d.ts +7 -5
- package/dist/board/editor/index.js +1 -1
- package/dist/board/editor/interaction.js +1 -1
- package/dist/board/export/index.d.ts +1 -6
- package/dist/board/export/index.js +16 -10
- package/dist/board/export/node/index.d.ts +2 -7
- package/dist/board/export/node/index.js +2 -5
- package/dist/board/export/scene.d.ts +3 -7
- package/dist/board/export/scene.js +22 -26
- package/dist/board/model/asset-key.d.ts +5 -2
- package/dist/board/model/asset-key.js +13 -5
- package/dist/board/model/index.d.ts +5 -4
- package/dist/board/model/index.js +4 -3
- package/dist/board/model/media-playback.d.ts +4 -6
- package/dist/board/model/media-playback.js +4 -4
- package/dist/board/model/read.d.ts +7 -0
- package/dist/board/model/read.js +18 -0
- package/dist/board/model/themes.d.ts +50 -0
- package/dist/board/model/themes.js +44 -6
- package/dist/board/render/backdrop-layer.d.ts +21 -0
- package/dist/board/render/backdrop-layer.js +227 -0
- package/dist/board/render/backdrop.d.ts +45 -0
- package/dist/board/render/backdrop.js +62 -0
- package/dist/board/render/index.d.ts +3 -2
- package/dist/board/render/index.js +7 -6
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/audio-card-renderer.js +2 -2
- package/dist/board/render/renderers/file-card-renderer.js +1 -1
- package/dist/board/render/renderers/frame-card-renderer.js +1 -1
- package/dist/board/render/renderers/model-card-renderer.js +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +2 -2
- package/dist/board/render/renderers/text-card-renderer.js +2 -2
- package/dist/board/render/renderers/video-card-renderer.js +1 -1
- package/dist/board/render/theme.d.ts +2 -0
- package/dist/board/render/theme.js +4 -2
- package/dist/board/replica/index.d.ts +3 -2
- package/dist/board/replica/index.js +34 -24
- package/dist/board/replica/indexeddb.js +14 -85
- package/dist/board/stage/assets.d.ts +4 -3
- package/dist/board/stage/assets.js +186 -83
- package/dist/board/stage/backdrop.d.ts +25 -0
- package/dist/board/stage/backdrop.js +104 -0
- package/dist/board/stage/index.d.ts +2 -1
- package/dist/board/stage/scene.js +12 -5
- package/dist/board/stage/stage.d.ts +7 -2
- package/dist/board/stage/stage.js +44 -24
- package/dist/board/stage/texture-pool.d.ts +1 -1
- package/dist/board/stage/texture-pool.js +4 -5
- package/dist/chunks/app-realtime.js +1 -1
- package/dist/chunks/environment.d.ts +34 -21
- package/dist/chunks/environment.js +4 -3
- package/dist/chunks/http-error.js +28 -5
- package/dist/chunks/http.d.ts +87 -18
- package/dist/chunks/http.js +1855 -59
- package/dist/chunks/websocket.d.ts +9 -1
- package/dist/http.d.ts +2 -2
- package/dist/http.js +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -1080
- package/dist/indexeddb.js +79 -0
- package/dist/media.d.ts +18 -32
- package/dist/media.js +81 -67
- package/dist/protocol/dist/board-model.d.ts +33 -26
- package/dist/protocol/dist/board-model.js +21 -18
- package/dist/protocol/dist/board.d.ts +21 -1
- package/dist/protocol/dist/board.js +5 -1
- package/dist/protocol/dist/core/media.d.ts +32 -9
- package/dist/protocol/dist/core/media.js +203 -48
- package/dist/protocol/dist/index.d.ts +3 -3
- package/dist/voice-input.js +1 -1
- package/dist/websocket.js +37 -5
- package/package.json +1 -1
- package/dist/board/render/board-background.d.ts +0 -18
- package/dist/board/render/board-background.js +0 -104
package/dist/chunks/http.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
"
|
|
641
|
-
"
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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({
|
|
647
|
+
background: BoardBackgroundSchema.default({}),
|
|
649
648
|
grid: z.object({
|
|
650
|
-
|
|
651
|
-
|
|
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$
|
|
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$
|
|
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([
|
|
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
|
-
})
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
3778
|
-
|
|
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 {
|
|
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 };
|