@neta-art/cohub 9.0.1 → 10.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 +100 -103
- package/dist/board/editor/align.d.ts +11 -0
- package/dist/board/editor/align.js +72 -0
- package/dist/board/editor/animation.d.ts +14 -0
- package/dist/board/editor/animation.js +172 -0
- package/dist/board/editor/camera.d.ts +32 -0
- package/dist/board/editor/camera.js +137 -0
- package/dist/board/editor/clipboard.d.ts +41 -0
- package/dist/board/editor/clipboard.js +122 -0
- package/dist/board/editor/connection-ports.d.ts +19 -0
- package/dist/board/editor/connection-ports.js +85 -0
- package/dist/board/editor/context.d.ts +111 -0
- package/dist/board/editor/context.js +30 -0
- package/dist/board/editor/copy.d.ts +17 -0
- package/dist/board/editor/copy.js +116 -0
- package/dist/board/editor/creation.d.ts +47 -0
- package/dist/board/editor/creation.js +224 -0
- package/dist/board/editor/deletion.d.ts +9 -0
- package/dist/board/editor/deletion.js +90 -0
- package/dist/board/editor/document-edits.d.ts +22 -0
- package/dist/board/editor/document-edits.js +193 -0
- package/dist/board/editor/document.d.ts +35 -0
- package/dist/board/editor/document.js +255 -0
- package/dist/board/editor/editor.d.ts +210 -0
- package/dist/board/editor/editor.js +429 -0
- package/dist/board/editor/index.d.ts +18 -0
- package/dist/board/editor/index.js +12 -0
- package/dist/board/editor/interaction.d.ts +115 -0
- package/dist/board/editor/interaction.js +584 -0
- package/dist/board/editor/query.d.ts +27 -0
- package/dist/board/editor/query.js +201 -0
- package/dist/board/editor/remote.d.ts +11 -0
- package/dist/board/editor/remote.js +55 -0
- package/dist/board/editor/selection-transform.d.ts +24 -0
- package/dist/board/editor/selection-transform.js +81 -0
- package/dist/board/editor/selection.d.ts +33 -0
- package/dist/board/editor/selection.js +196 -0
- package/dist/board/editor/snapping.d.ts +33 -0
- package/dist/board/editor/snapping.js +75 -0
- package/dist/board/editor/snapshots.d.ts +27 -0
- package/dist/board/editor/snapshots.js +132 -0
- package/dist/board/editor/spatial.d.ts +41 -0
- package/dist/board/editor/spatial.js +191 -0
- package/dist/board/editor/state.d.ts +12 -0
- package/dist/board/editor/state.js +49 -0
- package/dist/board/editor/tool.d.ts +10 -0
- package/dist/board/editor/tool.js +19 -0
- package/dist/board/editor/wheel.d.ts +5 -0
- package/dist/board/editor/wheel.js +17 -0
- package/dist/board/export/index.d.ts +5 -4
- package/dist/board/export/index.js +4 -3
- package/dist/board/export/node/index.d.ts +55 -0
- package/dist/board/{headless → export/node}/index.js +12 -12
- package/dist/board/export/node/sketch.d.ts +19 -0
- package/dist/board/{headless → export/node}/sketch.js +6 -6
- package/dist/board/export/scene.d.ts +6 -4
- package/dist/board/export/scene.js +4 -3
- package/dist/board/{animation.d.ts → model/animation.d.ts} +4 -4
- package/dist/board/{animation.js → model/animation.js} +3 -3
- package/dist/board/{core → model}/arrow-geometry.d.ts +2 -2
- package/dist/board/{core → model}/arrow-geometry.js +2 -2
- package/dist/board/{core → model}/draw-geometry.d.ts +2 -2
- package/dist/board/{core → model}/draw-geometry.js +1 -1
- package/dist/board/model/draw-input.d.ts +14 -0
- package/dist/board/model/draw-input.js +28 -0
- package/dist/board/{core → model}/export-assets.d.ts +1 -1
- package/dist/board/{core → model}/export-assets.js +1 -1
- package/dist/board/{core → model}/export-plan.d.ts +2 -2
- package/dist/board/{core → model}/export-plan.js +2 -2
- package/dist/board/{core → model}/file-preview.d.ts +1 -1
- package/dist/board/{core → model}/file-preview.js +1 -1
- package/dist/board/{core → model}/file-snapshot.d.ts +1 -1
- package/dist/board/{core → model}/file-snapshot.js +1 -1
- package/dist/board/{geometry.d.ts → model/geometry.d.ts} +3 -3
- package/dist/board/{geometry.js → model/geometry.js} +1 -1
- package/dist/board/{image-key.d.ts → model/image-key.d.ts} +2 -2
- package/dist/board/{image-key.js → model/image-key.js} +1 -1
- package/dist/board/model/index.d.ts +35 -0
- package/dist/board/{index.js → model/index.js} +26 -22
- package/dist/board/model/items/builtin.d.ts +5 -0
- package/dist/board/model/items/builtin.js +119 -0
- package/dist/board/model/items/create.d.ts +59 -0
- package/dist/board/model/items/create.js +271 -0
- package/dist/board/model/items/definition.d.ts +35 -0
- package/dist/board/model/items/definition.js +18 -0
- package/dist/board/model/items/id.d.ts +5 -0
- package/dist/board/model/items/id.js +11 -0
- package/dist/board/model/items/index.d.ts +6 -0
- package/dist/board/model/items/index.js +6 -0
- package/dist/board/model/items/registry.d.ts +35 -0
- package/dist/board/model/items/registry.js +84 -0
- package/dist/board/{media-playback.d.ts → model/media-playback.d.ts} +2 -2
- package/dist/board/{media-playback.js → model/media-playback.js} +1 -1
- package/dist/board/{media.d.ts → model/media.d.ts} +1 -1
- package/dist/board/{media.js → model/media.js} +1 -1
- package/dist/board/{core → model}/palette.d.ts +1 -1
- package/dist/board/{core → model}/palette.js +1 -1
- package/dist/board/{presets.d.ts → model/presets.d.ts} +3 -3
- package/dist/board/{presets.js → model/presets.js} +1 -1
- package/dist/board/{replay.d.ts → model/replay.d.ts} +5 -5
- package/dist/board/{replay.js → model/replay.js} +2 -2
- package/dist/board/{core → model}/scene.d.ts +1 -1
- package/dist/board/{core → model}/scene.js +1 -1
- package/dist/board/model/shape-types.d.ts +8 -0
- package/dist/board/model/shape-types.js +8 -0
- package/dist/board/{task.d.ts → model/task.d.ts} +4 -4
- package/dist/board/{task.js → model/task.js} +3 -3
- package/dist/board/{core → model}/text-metrics.d.ts +1 -1
- package/dist/board/{core → model}/text-metrics.js +1 -1
- package/dist/board/{core → model}/tool-styles.d.ts +1 -1
- package/dist/board/{core → model}/tool-styles.js +1 -1
- package/dist/board/{runtime/board-player.d.ts → player/index.d.ts} +2 -2
- package/dist/board/{runtime/board-player.js → player/index.js} +5 -4
- package/dist/board/render/board-background.d.ts +1 -1
- package/dist/board/render/clip.d.ts +1 -1
- package/dist/board/render/index.d.ts +2 -2
- package/dist/board/render/index.js +2 -2
- package/dist/board/render/media-interaction.d.ts +2 -2
- package/dist/board/render/media-interaction.js +1 -1
- package/dist/board/render/palette.d.ts +1 -1
- package/dist/board/render/palette.js +1 -1
- package/dist/board/render/renderers/arrow-card-renderer.js +1 -1
- package/dist/board/render/renderers/board-renderer-registry.d.ts +11 -6
- package/dist/board/render/renderers/board-renderer-registry.js +15 -15
- package/dist/board/render/renderers/draw-card-renderer.js +1 -1
- package/dist/board/render/renderers/far-plate.js +1 -1
- package/dist/board/render/renderers/file-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/file-card-renderer.js +2 -2
- package/dist/board/render/renderers/image-card-renderer.d.ts +1 -1
- package/dist/board/render/renderers/shape-card-renderer.js +1 -1
- package/dist/board/render/renderers/task-card-renderer.js +1 -1
- package/dist/board/render/renderers/text-card-renderer.js +1 -1
- package/dist/board/render/renderers/video-card-renderer.d.ts +1 -1
- package/dist/board/render/stroke.d.ts +1 -1
- package/dist/board/render/stroke.js +1 -1
- package/dist/board/render/text-measurement.js +1 -1
- package/dist/board/replica/index.d.ts +70 -0
- package/dist/board/replica/index.js +276 -0
- package/dist/board/stage/assets.d.ts +41 -0
- package/dist/board/stage/assets.js +350 -0
- package/dist/board/stage/index.d.ts +8 -0
- package/dist/board/stage/index.js +8 -0
- package/dist/board/stage/lru.d.ts +32 -0
- package/dist/board/stage/lru.js +29 -0
- package/dist/board/stage/pointer.d.ts +8 -0
- package/dist/board/stage/pointer.js +33 -0
- package/dist/board/stage/scene.d.ts +85 -0
- package/dist/board/stage/scene.js +512 -0
- package/dist/board/stage/stage.d.ts +66 -0
- package/dist/board/stage/stage.js +568 -0
- package/dist/board/stage/texture-pool.d.ts +24 -0
- package/dist/board/stage/texture-pool.js +154 -0
- package/dist/board/stage/views.d.ts +38 -0
- package/dist/board/stage/views.js +93 -0
- package/dist/chunks/environment.d.ts +24 -1
- package/dist/chunks/http.d.ts +99 -66
- package/dist/chunks/http.js +214 -207
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/http.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +20 -4
- package/dist/media.d.ts +1 -10
- package/dist/media.js +16 -27
- package/dist/protocol/dist/board-patch.js +1 -1
- package/dist/protocol/dist/board.d.ts +28 -2
- package/dist/protocol/dist/board.js +3 -1
- package/dist/protocol/dist/core/media.d.ts +5 -0
- package/dist/protocol/dist/core/media.js +39 -0
- package/dist/protocol/dist/index.d.ts +5 -5
- package/dist/types.d.ts +1 -1
- package/package.json +20 -8
- package/dist/board/core/shape-definition.d.ts +0 -23
- package/dist/board/core/shape-definition.js +0 -45
- package/dist/board/core/shape-types.d.ts +0 -38
- package/dist/board/core/shape-types.js +0 -22
- package/dist/board/headless/index.d.ts +0 -55
- package/dist/board/headless/sketch.d.ts +0 -19
- package/dist/board/index.d.ts +0 -30
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { applyBoardDelta, applyBoardPatchToDocument, parseBoardDocument } from "../../protocol/dist/board-patch.js";
|
|
2
|
+
import { createBoardEntityId } from "../model/items/id.js";
|
|
3
|
+
//#region src/board/replica/index.ts
|
|
4
|
+
function memoryStorage() {
|
|
5
|
+
return {
|
|
6
|
+
listPending: async () => [],
|
|
7
|
+
putPending: async () => void 0,
|
|
8
|
+
deletePending: async () => void 0,
|
|
9
|
+
readDocument: async () => null,
|
|
10
|
+
writeDocument: async () => void 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const RETRY_BASE_MS = 1e3;
|
|
14
|
+
const RETRY_MAX_MS = 3e4;
|
|
15
|
+
const PERSIST_INTERVAL_MS = 500;
|
|
16
|
+
function isRejection(error) {
|
|
17
|
+
const status = error.status;
|
|
18
|
+
return typeof status === "number" && status >= 400 && status < 500 && ![
|
|
19
|
+
401,
|
|
20
|
+
403,
|
|
21
|
+
408,
|
|
22
|
+
429
|
|
23
|
+
].includes(status);
|
|
24
|
+
}
|
|
25
|
+
function errorMessage(error) {
|
|
26
|
+
return error instanceof Error ? error.message : "Board sync failed.";
|
|
27
|
+
}
|
|
28
|
+
function createBoardReplica(options) {
|
|
29
|
+
const remote = options.remote;
|
|
30
|
+
const storage = options.storage ?? memoryStorage();
|
|
31
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
32
|
+
let unsubscribeRemote = null;
|
|
33
|
+
let server = null;
|
|
34
|
+
let version = 0;
|
|
35
|
+
let title = null;
|
|
36
|
+
let playback = null;
|
|
37
|
+
let playbackRevision = 0;
|
|
38
|
+
let loaded = false;
|
|
39
|
+
let readError = null;
|
|
40
|
+
let writeError = null;
|
|
41
|
+
let pending = [];
|
|
42
|
+
let document = null;
|
|
43
|
+
let sending = false;
|
|
44
|
+
let refreshing = null;
|
|
45
|
+
let refreshAgain = false;
|
|
46
|
+
let retryTimer = null;
|
|
47
|
+
let retryDelay = RETRY_BASE_MS;
|
|
48
|
+
let disposed = false;
|
|
49
|
+
let unpersisted = null;
|
|
50
|
+
let persistTimer = null;
|
|
51
|
+
function subscribeRemote() {
|
|
52
|
+
if (!remote.subscribe) return null;
|
|
53
|
+
try {
|
|
54
|
+
return remote.subscribe({
|
|
55
|
+
changed: (event) => receive(event.payload),
|
|
56
|
+
playback: (event) => receivePlayback(event.payload.playback)
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function recompute() {
|
|
63
|
+
if (!server) {
|
|
64
|
+
document = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let next = server;
|
|
68
|
+
for (const entry of pending) {
|
|
69
|
+
const result = applyBoardPatchToDocument(next, entry.patch);
|
|
70
|
+
if (result.ok) next = result.document;
|
|
71
|
+
}
|
|
72
|
+
document = next;
|
|
73
|
+
}
|
|
74
|
+
function snapshot() {
|
|
75
|
+
return {
|
|
76
|
+
document,
|
|
77
|
+
version,
|
|
78
|
+
title,
|
|
79
|
+
playback,
|
|
80
|
+
loaded,
|
|
81
|
+
pending: pending.length,
|
|
82
|
+
error: writeError ?? readError
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
let current = snapshot();
|
|
86
|
+
function emit() {
|
|
87
|
+
if (disposed) return;
|
|
88
|
+
current = snapshot();
|
|
89
|
+
for (const listener of [...listeners]) listener(current);
|
|
90
|
+
}
|
|
91
|
+
function adoptServer(next, nextVersion, persist) {
|
|
92
|
+
server = next;
|
|
93
|
+
version = nextVersion;
|
|
94
|
+
recompute();
|
|
95
|
+
if (persist) schedulePersist(nextVersion, next);
|
|
96
|
+
}
|
|
97
|
+
function schedulePersist(nextVersion, next) {
|
|
98
|
+
unpersisted = {
|
|
99
|
+
version: nextVersion,
|
|
100
|
+
document: next
|
|
101
|
+
};
|
|
102
|
+
persistTimer ??= setTimeout(persistNow, options.persistIntervalMs ?? PERSIST_INTERVAL_MS);
|
|
103
|
+
}
|
|
104
|
+
function persistNow() {
|
|
105
|
+
if (persistTimer) clearTimeout(persistTimer);
|
|
106
|
+
persistTimer = null;
|
|
107
|
+
const entry = unpersisted;
|
|
108
|
+
unpersisted = null;
|
|
109
|
+
if (entry) storage.writeDocument(entry.version, entry.document).catch(() => void 0);
|
|
110
|
+
}
|
|
111
|
+
function adoptRead(result, readPlaybackRevision) {
|
|
112
|
+
const parsed = parseBoardDocument({
|
|
113
|
+
board: result.board,
|
|
114
|
+
items: result.items ?? {},
|
|
115
|
+
animations: result.animations ?? {}
|
|
116
|
+
});
|
|
117
|
+
if (!parsed.ok) throw new Error(`Board ${result.id} could not be read.`);
|
|
118
|
+
title = result.title;
|
|
119
|
+
if (readPlaybackRevision === playbackRevision) playback = result.playback;
|
|
120
|
+
loaded = true;
|
|
121
|
+
adoptServer(parsed.document, result.version, true);
|
|
122
|
+
}
|
|
123
|
+
function refresh() {
|
|
124
|
+
if (refreshing) {
|
|
125
|
+
refreshAgain = true;
|
|
126
|
+
return refreshing;
|
|
127
|
+
}
|
|
128
|
+
refreshing = (async () => {
|
|
129
|
+
try {
|
|
130
|
+
do {
|
|
131
|
+
refreshAgain = false;
|
|
132
|
+
const readPlaybackRevision = playbackRevision;
|
|
133
|
+
const result = await remote.get();
|
|
134
|
+
if (disposed) return;
|
|
135
|
+
if (result.version >= version || !loaded) adoptRead(result, readPlaybackRevision);
|
|
136
|
+
readError = null;
|
|
137
|
+
emit();
|
|
138
|
+
} while (refreshAgain && !disposed);
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
readError = errorMessage(cause);
|
|
141
|
+
emit();
|
|
142
|
+
} finally {
|
|
143
|
+
refreshing = null;
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
return refreshing;
|
|
147
|
+
}
|
|
148
|
+
function scheduleRetry() {
|
|
149
|
+
if (retryTimer || disposed) return;
|
|
150
|
+
retryTimer = setTimeout(() => {
|
|
151
|
+
retryTimer = null;
|
|
152
|
+
flush();
|
|
153
|
+
}, retryDelay);
|
|
154
|
+
retryDelay = Math.min(RETRY_MAX_MS, retryDelay * 2);
|
|
155
|
+
}
|
|
156
|
+
async function flush() {
|
|
157
|
+
if (sending || disposed || !loaded) return;
|
|
158
|
+
sending = true;
|
|
159
|
+
try {
|
|
160
|
+
while (pending.length && !disposed) {
|
|
161
|
+
const entry = pending[0];
|
|
162
|
+
try {
|
|
163
|
+
const result = await remote.apply(entry.patch, {
|
|
164
|
+
mutationId: entry.mutationId,
|
|
165
|
+
...options.clientId ? { clientId: options.clientId } : {}
|
|
166
|
+
});
|
|
167
|
+
const stillPending = pending.some((item) => item.mutationId === entry.mutationId);
|
|
168
|
+
pending = pending.filter((item) => item.mutationId !== entry.mutationId);
|
|
169
|
+
storage.deletePending(entry.mutationId).catch(() => void 0);
|
|
170
|
+
if (stillPending && server && !result.replayed && result.status === "applied" && result.version === version + 1) {
|
|
171
|
+
const next = applyBoardPatchToDocument(server, entry.patch);
|
|
172
|
+
if (next.ok) adoptServer(next.document, result.version, true);
|
|
173
|
+
else refresh();
|
|
174
|
+
} else if (result.version > version) refresh();
|
|
175
|
+
else recompute();
|
|
176
|
+
retryDelay = RETRY_BASE_MS;
|
|
177
|
+
writeError = null;
|
|
178
|
+
emit();
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
if (isRejection(cause)) {
|
|
181
|
+
pending = pending.filter((item) => item.mutationId !== entry.mutationId);
|
|
182
|
+
storage.deletePending(entry.mutationId).catch(() => void 0);
|
|
183
|
+
writeError = errorMessage(cause);
|
|
184
|
+
recompute();
|
|
185
|
+
emit();
|
|
186
|
+
refresh();
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
writeError = errorMessage(cause);
|
|
190
|
+
emit();
|
|
191
|
+
scheduleRetry();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
sending = false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function receive(payload) {
|
|
200
|
+
if (!loaded) {
|
|
201
|
+
refresh();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (payload.version <= version) return;
|
|
205
|
+
if (payload.baseVersion !== version || !payload.after || !server) {
|
|
206
|
+
refresh();
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
adoptServer(applyBoardDelta(server, payload.after), payload.version, true);
|
|
210
|
+
if (pending.some((entry) => entry.mutationId === payload.mutationId)) {
|
|
211
|
+
pending = pending.filter((entry) => entry.mutationId !== payload.mutationId);
|
|
212
|
+
storage.deletePending(payload.mutationId).catch(() => void 0);
|
|
213
|
+
recompute();
|
|
214
|
+
}
|
|
215
|
+
emit();
|
|
216
|
+
}
|
|
217
|
+
function receivePlayback(next) {
|
|
218
|
+
playbackRevision += 1;
|
|
219
|
+
playback = next;
|
|
220
|
+
emit();
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
get state() {
|
|
224
|
+
return current;
|
|
225
|
+
},
|
|
226
|
+
subscribe(listener) {
|
|
227
|
+
listeners.add(listener);
|
|
228
|
+
return () => listeners.delete(listener);
|
|
229
|
+
},
|
|
230
|
+
async start() {
|
|
231
|
+
unsubscribeRemote ??= subscribeRemote();
|
|
232
|
+
const [cached, stored] = await Promise.all([storage.readDocument().catch(() => null), storage.listPending().catch(() => [])]);
|
|
233
|
+
if (disposed) return;
|
|
234
|
+
pending = stored;
|
|
235
|
+
if (cached && !server) {
|
|
236
|
+
const parsed = parseBoardDocument(cached.document);
|
|
237
|
+
if (parsed.ok) adoptServer(parsed.document, cached.version, false);
|
|
238
|
+
}
|
|
239
|
+
emit();
|
|
240
|
+
await refresh();
|
|
241
|
+
await flush();
|
|
242
|
+
},
|
|
243
|
+
apply(patch) {
|
|
244
|
+
const entry = {
|
|
245
|
+
mutationId: createBoardEntityId(),
|
|
246
|
+
patch
|
|
247
|
+
};
|
|
248
|
+
pending = [...pending, entry];
|
|
249
|
+
recompute();
|
|
250
|
+
emit();
|
|
251
|
+
return storage.putPending(entry).catch(() => void 0).then(() => flush());
|
|
252
|
+
},
|
|
253
|
+
receive,
|
|
254
|
+
receivePlayback,
|
|
255
|
+
refresh,
|
|
256
|
+
retry() {
|
|
257
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
258
|
+
retryTimer = null;
|
|
259
|
+
retryDelay = RETRY_BASE_MS;
|
|
260
|
+
readError = null;
|
|
261
|
+
writeError = null;
|
|
262
|
+
emit();
|
|
263
|
+
return refresh().then(flush);
|
|
264
|
+
},
|
|
265
|
+
dispose() {
|
|
266
|
+
disposed = true;
|
|
267
|
+
unsubscribeRemote?.();
|
|
268
|
+
unsubscribeRemote = null;
|
|
269
|
+
listeners.clear();
|
|
270
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
271
|
+
persistNow();
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
export { createBoardReplica };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BoardSceneItem } from "../model/scene.js";
|
|
2
|
+
import "../model/index.js";
|
|
3
|
+
import { LruBudget } from "./lru.js";
|
|
4
|
+
import { BoardImageTexturePool } from "./texture-pool.js";
|
|
5
|
+
import { Texture } from "pixi.js";
|
|
6
|
+
//#region src/board/stage/assets.d.ts
|
|
7
|
+
type BoardAssetMedia = "image" | "video";
|
|
8
|
+
declare function boardAssetKey(item: BoardSceneItem): string | null;
|
|
9
|
+
type BoardAssetManager = {
|
|
10
|
+
assetKey: (item: BoardSceneItem) => string | null;
|
|
11
|
+
invalidatePath: (path: string) => void;
|
|
12
|
+
requestItem: (item: BoardSceneItem) => void;
|
|
13
|
+
getTexture: (key: string) => Texture | null;
|
|
14
|
+
getNaturalSize: (key: string) => {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
} | null;
|
|
18
|
+
hasError: (key: string) => boolean;
|
|
19
|
+
acquire: (key: string) => void;
|
|
20
|
+
release: (key: string) => void;
|
|
21
|
+
withTextures: <T>(items: BoardSceneItem[], use: (textures: Map<string, Texture>) => T | Promise<T>, options?: {
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
}) => Promise<T>;
|
|
24
|
+
subscribe: (listener: () => void) => () => void;
|
|
25
|
+
destroy: () => void;
|
|
26
|
+
};
|
|
27
|
+
type BoardAssetManagerOptions = {
|
|
28
|
+
/** Resolve a Space file path to a URL a texture can load from; null when unavailable. */
|
|
29
|
+
resolveFileUrl: (path: string) => Promise<string | null>;
|
|
30
|
+
concurrency?: number;
|
|
31
|
+
videoConcurrency?: number;
|
|
32
|
+
loadVideoPreviews?: boolean;
|
|
33
|
+
lruBudget?: LruBudget;
|
|
34
|
+
imageTexturePool?: BoardImageTexturePool;
|
|
35
|
+
loadTexture?: (url: string, media: BoardAssetMedia) => Promise<Texture | null>;
|
|
36
|
+
unloadTexture?: (url: string, texture: Texture | null, media: BoardAssetMedia) => Promise<void>;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
};
|
|
39
|
+
declare function createBoardAssetManager(options: BoardAssetManagerOptions): BoardAssetManager;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { BoardAssetManager, BoardAssetManagerOptions, boardAssetKey, createBoardAssetManager };
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { mediaPreviewCandidates } from "../../media.js";
|
|
2
|
+
import { featuredTaskArtifact } from "../model/task.js";
|
|
3
|
+
import { boardImageKeySource, imageAssetKey } from "../model/image-key.js";
|
|
4
|
+
import { loadVideoThumbnailTexture, videoTextureNaturalSize } from "../render/video-thumbnail.js";
|
|
5
|
+
import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
|
|
6
|
+
import { boardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
7
|
+
//#region src/board/stage/assets.ts
|
|
8
|
+
const TASK_TEXTURE_SIZE = 1536;
|
|
9
|
+
function taskPreviewKey(artifact) {
|
|
10
|
+
if (!artifact || artifact.type === "text") return null;
|
|
11
|
+
const [still] = mediaPreviewCandidates(artifact, { size: TASK_TEXTURE_SIZE });
|
|
12
|
+
if (still) return `url:${still}`;
|
|
13
|
+
return artifact.type === "video" ? `video-url:${encodeURIComponent(artifact.url)}` : null;
|
|
14
|
+
}
|
|
15
|
+
function boardAssetKey(item) {
|
|
16
|
+
if (item.type === "task") return taskPreviewKey(featuredTaskArtifact(item.props.snapshot.artifacts));
|
|
17
|
+
if (item.type === "video") return `video:${encodeURIComponent(item.props.src)}:${item.props.snapshot?.mtimeMs ?? "unknown"}`;
|
|
18
|
+
return imageAssetKey(item);
|
|
19
|
+
}
|
|
20
|
+
function keySource(key) {
|
|
21
|
+
let baseKey = key;
|
|
22
|
+
if (key.startsWith("local:")) {
|
|
23
|
+
const separator = key.indexOf(":", 6);
|
|
24
|
+
if (separator < 0) return null;
|
|
25
|
+
baseKey = key.slice(separator + 1);
|
|
26
|
+
}
|
|
27
|
+
if (baseKey.startsWith("video-url:")) try {
|
|
28
|
+
return {
|
|
29
|
+
kind: "url",
|
|
30
|
+
media: "video",
|
|
31
|
+
value: decodeURIComponent(baseKey.slice(10))
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (baseKey.startsWith("video:")) {
|
|
37
|
+
const separator = baseKey.lastIndexOf(":");
|
|
38
|
+
if (separator <= 6) return null;
|
|
39
|
+
try {
|
|
40
|
+
return {
|
|
41
|
+
kind: "file",
|
|
42
|
+
media: "video",
|
|
43
|
+
value: decodeURIComponent(baseKey.slice(6, separator))
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const image = boardImageKeySource(baseKey);
|
|
50
|
+
return image ? {
|
|
51
|
+
...image,
|
|
52
|
+
media: "image"
|
|
53
|
+
} : null;
|
|
54
|
+
}
|
|
55
|
+
const MAX_RETRY_DELAY = 3e4;
|
|
56
|
+
function footprintOf(texture) {
|
|
57
|
+
if (!texture) return 0;
|
|
58
|
+
return (texture.width || 0) * (texture.height || 0) * 4;
|
|
59
|
+
}
|
|
60
|
+
function createBoardAssetManager(options) {
|
|
61
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
62
|
+
const videoConcurrency = Math.max(1, Math.min(options.videoConcurrency ?? 2, concurrency));
|
|
63
|
+
const budget = options.lruBudget ?? DEFAULT_LRU_BUDGET;
|
|
64
|
+
const now = options.now ?? (() => Date.now());
|
|
65
|
+
const imagePool = options.imageTexturePool ?? boardImageTexturePool;
|
|
66
|
+
const useSharedImagePool = !options.loadTexture && !options.unloadTexture;
|
|
67
|
+
const loadTexture = options.loadTexture ?? ((url, media) => media === "video" ? loadVideoThumbnailTexture(url) : useSharedImagePool ? imagePool.acquire(url) : loadBoardImageTexture(url));
|
|
68
|
+
const pendingUnloads = /* @__PURE__ */ new Map();
|
|
69
|
+
const unloadTexture = options.unloadTexture ?? (async (url, texture, media) => {
|
|
70
|
+
if (media === "video" || !url) {
|
|
71
|
+
texture?.destroy(true);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (useSharedImagePool) {
|
|
75
|
+
await imagePool.release(url);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await unloadBoardImageTexture(url, texture);
|
|
79
|
+
});
|
|
80
|
+
function releaseTexture(key, url, texture) {
|
|
81
|
+
const media = keySource(key)?.media ?? "image";
|
|
82
|
+
const pendingKey = `${media}:${url}`;
|
|
83
|
+
const promise = unloadTexture(url, texture, media).catch(() => {}).then(() => {
|
|
84
|
+
if (url && pendingUnloads.get(pendingKey) === promise) pendingUnloads.delete(pendingKey);
|
|
85
|
+
});
|
|
86
|
+
if (url) pendingUnloads.set(pendingKey, promise);
|
|
87
|
+
}
|
|
88
|
+
const resolveFileUrl = options.resolveFileUrl;
|
|
89
|
+
const entries = /* @__PURE__ */ new Map();
|
|
90
|
+
const pathVersions = /* @__PURE__ */ new Map();
|
|
91
|
+
const resolvers = /* @__PURE__ */ new Map();
|
|
92
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
93
|
+
const queue = [];
|
|
94
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
95
|
+
let active = 0;
|
|
96
|
+
let activeVideos = 0;
|
|
97
|
+
let disposed = false;
|
|
98
|
+
function notify() {
|
|
99
|
+
for (const listener of listeners) listener();
|
|
100
|
+
}
|
|
101
|
+
function managedAssetKey(item) {
|
|
102
|
+
const key = boardAssetKey(item);
|
|
103
|
+
if (!key) return null;
|
|
104
|
+
const source = keySource(key);
|
|
105
|
+
if (source?.media === "video" && options.loadVideoPreviews === false) return null;
|
|
106
|
+
if (source?.kind !== "file") return key;
|
|
107
|
+
const version = pathVersions.get(source.value) ?? 0;
|
|
108
|
+
return version > 0 ? `local:${version}:${key}` : key;
|
|
109
|
+
}
|
|
110
|
+
function clearRetry(entry) {
|
|
111
|
+
if (entry.retryTimer) {
|
|
112
|
+
clearTimeout(entry.retryTimer);
|
|
113
|
+
entry.retryTimer = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function ensureEntry(key) {
|
|
117
|
+
let entry = entries.get(key);
|
|
118
|
+
if (!entry) {
|
|
119
|
+
entry = {
|
|
120
|
+
url: null,
|
|
121
|
+
texture: null,
|
|
122
|
+
refs: 0,
|
|
123
|
+
loading: false,
|
|
124
|
+
error: false,
|
|
125
|
+
attempts: 0,
|
|
126
|
+
retryAt: 0,
|
|
127
|
+
retryTimer: null,
|
|
128
|
+
lastUsedAt: now()
|
|
129
|
+
};
|
|
130
|
+
entries.set(key, entry);
|
|
131
|
+
}
|
|
132
|
+
return entry;
|
|
133
|
+
}
|
|
134
|
+
function scheduleRetry(key, entry) {
|
|
135
|
+
clearRetry(entry);
|
|
136
|
+
const delay = Math.max(0, entry.retryAt - now());
|
|
137
|
+
entry.retryTimer = setTimeout(() => {
|
|
138
|
+
entry.retryTimer = null;
|
|
139
|
+
if (disposed || entries.get(key) !== entry) return;
|
|
140
|
+
if (entry.refs <= 0 || entry.texture || entry.loading) return;
|
|
141
|
+
const getUrl = resolvers.get(key);
|
|
142
|
+
if (!getUrl) return;
|
|
143
|
+
entry.error = false;
|
|
144
|
+
if (!queue.some((task) => task.key === key)) {
|
|
145
|
+
queue.push({
|
|
146
|
+
key,
|
|
147
|
+
getUrl
|
|
148
|
+
});
|
|
149
|
+
pump();
|
|
150
|
+
}
|
|
151
|
+
}, delay);
|
|
152
|
+
}
|
|
153
|
+
function evict(key, entry) {
|
|
154
|
+
clearRetry(entry);
|
|
155
|
+
resolvers.delete(key);
|
|
156
|
+
if (entry.url || entry.texture) releaseTexture(key, entry.url ?? "", entry.texture);
|
|
157
|
+
entries.delete(key);
|
|
158
|
+
}
|
|
159
|
+
function trim() {
|
|
160
|
+
const cooling = [];
|
|
161
|
+
for (const [key, entry] of entries) if (entry.refs <= 0 && entry.texture && !entry.loading) cooling.push({
|
|
162
|
+
key,
|
|
163
|
+
lastUsedAt: entry.lastUsedAt,
|
|
164
|
+
bytes: footprintOf(entry.texture)
|
|
165
|
+
});
|
|
166
|
+
const evictions = selectLruEvictions(cooling, budget);
|
|
167
|
+
for (const key of evictions) {
|
|
168
|
+
const entry = entries.get(key);
|
|
169
|
+
if (entry) evict(key, entry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function settle(key, entry, texture, error) {
|
|
173
|
+
entry.loading = false;
|
|
174
|
+
inflight.delete(key);
|
|
175
|
+
active -= 1;
|
|
176
|
+
if (keySource(key)?.media === "video") activeVideos -= 1;
|
|
177
|
+
if (entries.get(key) !== entry) {
|
|
178
|
+
if (texture) releaseTexture(key, entry.url ?? "", texture);
|
|
179
|
+
if (!disposed) pump();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (texture) {
|
|
183
|
+
entry.error = false;
|
|
184
|
+
entry.attempts = 0;
|
|
185
|
+
entry.texture = texture;
|
|
186
|
+
entry.lastUsedAt = now();
|
|
187
|
+
if (entry.refs <= 0) trim();
|
|
188
|
+
else notify();
|
|
189
|
+
} else {
|
|
190
|
+
entry.error = true;
|
|
191
|
+
entry.attempts += 1;
|
|
192
|
+
entry.retryAt = now() + Math.min(1e3 * 2 ** entry.attempts, MAX_RETRY_DELAY);
|
|
193
|
+
console.warn(`[board] failed to load preview for ${key}`, error);
|
|
194
|
+
if (entry.refs <= 0) evict(key, entry);
|
|
195
|
+
else {
|
|
196
|
+
scheduleRetry(key, entry);
|
|
197
|
+
notify();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!disposed) pump();
|
|
201
|
+
}
|
|
202
|
+
function pump() {
|
|
203
|
+
while (!disposed && active < concurrency && queue.length > 0) {
|
|
204
|
+
const taskIndex = queue.findIndex(({ key }) => {
|
|
205
|
+
return keySource(key)?.media !== "video" || activeVideos < videoConcurrency;
|
|
206
|
+
});
|
|
207
|
+
if (taskIndex < 0) return;
|
|
208
|
+
const [task] = queue.splice(taskIndex, 1);
|
|
209
|
+
if (!task) continue;
|
|
210
|
+
const entry = entries.get(task.key);
|
|
211
|
+
const source = keySource(task.key);
|
|
212
|
+
if (!entry || !source || entry.texture || entry.loading) continue;
|
|
213
|
+
entry.loading = true;
|
|
214
|
+
inflight.add(task.key);
|
|
215
|
+
active += 1;
|
|
216
|
+
if (source.media === "video") activeVideos += 1;
|
|
217
|
+
task.getUrl().then(async (url) => {
|
|
218
|
+
if (!url || disposed) return null;
|
|
219
|
+
entry.url = url;
|
|
220
|
+
const pendingKey = `${source.media}:${url}`;
|
|
221
|
+
const pending = pendingUnloads.get(pendingKey);
|
|
222
|
+
if (pending) await pending;
|
|
223
|
+
if (disposed) return null;
|
|
224
|
+
return loadTexture(url, source.media);
|
|
225
|
+
}).then((texture) => settle(task.key, entry, texture ?? null), (error) => settle(task.key, entry, null, error));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const manager = {
|
|
229
|
+
assetKey: managedAssetKey,
|
|
230
|
+
invalidatePath(path) {
|
|
231
|
+
if (disposed) return;
|
|
232
|
+
if (![...entries.keys()].some((key) => {
|
|
233
|
+
const source = keySource(key);
|
|
234
|
+
return source?.kind === "file" && source.value === path;
|
|
235
|
+
})) return;
|
|
236
|
+
pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
|
|
237
|
+
notify();
|
|
238
|
+
},
|
|
239
|
+
requestItem(item) {
|
|
240
|
+
if (disposed) return;
|
|
241
|
+
const key = managedAssetKey(item);
|
|
242
|
+
if (!key) return;
|
|
243
|
+
const source = keySource(key);
|
|
244
|
+
if (!source) return;
|
|
245
|
+
const entry = ensureEntry(key);
|
|
246
|
+
entry.lastUsedAt = now();
|
|
247
|
+
if (entry.texture || entry.loading) return;
|
|
248
|
+
if (entry.error && now() < entry.retryAt) return;
|
|
249
|
+
entry.error = false;
|
|
250
|
+
clearRetry(entry);
|
|
251
|
+
if (queue.some((queued) => queued.key === key)) return;
|
|
252
|
+
const getUrl = source.kind === "url" ? () => Promise.resolve(source.value) : () => resolveFileUrl(source.value);
|
|
253
|
+
resolvers.set(key, getUrl);
|
|
254
|
+
queue.push({
|
|
255
|
+
key,
|
|
256
|
+
getUrl
|
|
257
|
+
});
|
|
258
|
+
pump();
|
|
259
|
+
},
|
|
260
|
+
getTexture(key) {
|
|
261
|
+
const entry = entries.get(key);
|
|
262
|
+
if (!entry?.texture) return null;
|
|
263
|
+
entry.lastUsedAt = now();
|
|
264
|
+
return entry.texture;
|
|
265
|
+
},
|
|
266
|
+
getNaturalSize(key) {
|
|
267
|
+
const texture = manager.getTexture(key);
|
|
268
|
+
if (!texture) return null;
|
|
269
|
+
return videoTextureNaturalSize(texture) ?? {
|
|
270
|
+
width: texture.width,
|
|
271
|
+
height: texture.height
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
hasError(key) {
|
|
275
|
+
return entries.get(key)?.error ?? false;
|
|
276
|
+
},
|
|
277
|
+
acquire(key) {
|
|
278
|
+
const entry = ensureEntry(key);
|
|
279
|
+
entry.refs += 1;
|
|
280
|
+
entry.lastUsedAt = now();
|
|
281
|
+
},
|
|
282
|
+
release(key) {
|
|
283
|
+
const entry = entries.get(key);
|
|
284
|
+
if (!entry) return;
|
|
285
|
+
entry.refs -= 1;
|
|
286
|
+
if (entry.refs > 0) return;
|
|
287
|
+
entry.lastUsedAt = now();
|
|
288
|
+
clearRetry(entry);
|
|
289
|
+
if (entry.loading) return;
|
|
290
|
+
if (!entry.texture) evict(key, entry);
|
|
291
|
+
else trim();
|
|
292
|
+
},
|
|
293
|
+
async withTextures(items, use, loadOptions) {
|
|
294
|
+
const keys = /* @__PURE__ */ new Set();
|
|
295
|
+
if (!disposed) for (const item of items) {
|
|
296
|
+
const key = managedAssetKey(item);
|
|
297
|
+
if (key) keys.add(key);
|
|
298
|
+
}
|
|
299
|
+
const textures = /* @__PURE__ */ new Map();
|
|
300
|
+
if (keys.size === 0) return use(textures);
|
|
301
|
+
for (const key of keys) manager.acquire(key);
|
|
302
|
+
try {
|
|
303
|
+
for (const item of items) manager.requestItem(item);
|
|
304
|
+
const settled = () => [...keys].every((key) => {
|
|
305
|
+
const entry = entries.get(key);
|
|
306
|
+
return Boolean(entry?.texture) || Boolean(entry?.error);
|
|
307
|
+
});
|
|
308
|
+
if (!settled()) {
|
|
309
|
+
const timeoutMs = loadOptions?.timeoutMs ?? 15e3;
|
|
310
|
+
await new Promise((resolve) => {
|
|
311
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
312
|
+
const unsubscribe = manager.subscribe(() => {
|
|
313
|
+
if (settled()) finish();
|
|
314
|
+
});
|
|
315
|
+
function finish() {
|
|
316
|
+
clearTimeout(timer);
|
|
317
|
+
unsubscribe();
|
|
318
|
+
resolve();
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
for (const key of keys) {
|
|
323
|
+
const texture = entries.get(key)?.texture;
|
|
324
|
+
if (texture) textures.set(key, texture);
|
|
325
|
+
}
|
|
326
|
+
return await use(textures);
|
|
327
|
+
} finally {
|
|
328
|
+
for (const key of keys) manager.release(key);
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
subscribe(listener) {
|
|
332
|
+
listeners.add(listener);
|
|
333
|
+
return () => {
|
|
334
|
+
listeners.delete(listener);
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
destroy() {
|
|
338
|
+
disposed = true;
|
|
339
|
+
queue.length = 0;
|
|
340
|
+
listeners.clear();
|
|
341
|
+
pathVersions.clear();
|
|
342
|
+
resolvers.clear();
|
|
343
|
+
for (const [key, entry] of entries) evict(key, entry);
|
|
344
|
+
entries.clear();
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
return manager;
|
|
348
|
+
}
|
|
349
|
+
//#endregion
|
|
350
|
+
export { boardAssetKey, createBoardAssetManager };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_LRU_BUDGET, LruBudget, LruEntry, selectLruEvictions } from "./lru.js";
|
|
2
|
+
import { BoardImageTexturePool, boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
+
import { BoardAssetManager, BoardAssetManagerOptions, boardAssetKey, createBoardAssetManager } from "./assets.js";
|
|
4
|
+
import { toBoardPointerEvent } from "./pointer.js";
|
|
5
|
+
import { BoardLiveStroke, BoardScene, BoardSceneNode, SceneOverlayInput, SceneSyncInput, createBoardScene } from "./scene.js";
|
|
6
|
+
import { BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, createBoardItemViewLayer } from "./views.js";
|
|
7
|
+
import { BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, mountBoardStage } from "./stage.js";
|
|
8
|
+
export { BoardAssetManager, BoardAssetManagerOptions, BoardImageTexturePool, BoardItemView, BoardItemViewContext, BoardItemViewInstance, BoardItemViewLayer, BoardLiveStroke, BoardScene, BoardSceneNode, BoardStage, BoardStageOptions, BoardStagePointerPhase, BoardStageTheme, DEFAULT_LRU_BUDGET, LruBudget, LruEntry, SceneOverlayInput, SceneSyncInput, boardAssetKey, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardScene, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DEFAULT_LRU_BUDGET, selectLruEvictions } from "./lru.js";
|
|
2
|
+
import { boardImageTexturePool, createBoardImageTexturePool, loadBoardImageTexture, unloadBoardImageTexture } from "./texture-pool.js";
|
|
3
|
+
import { boardAssetKey, createBoardAssetManager } from "./assets.js";
|
|
4
|
+
import { toBoardPointerEvent } from "./pointer.js";
|
|
5
|
+
import { createBoardScene } from "./scene.js";
|
|
6
|
+
import { createBoardItemViewLayer } from "./views.js";
|
|
7
|
+
import { mountBoardStage } from "./stage.js";
|
|
8
|
+
export { DEFAULT_LRU_BUDGET, boardAssetKey, boardImageTexturePool, createBoardAssetManager, createBoardImageTexturePool, createBoardItemViewLayer, createBoardScene, loadBoardImageTexture, mountBoardStage, selectLruEvictions, toBoardPointerEvent, unloadBoardImageTexture };
|