@neta-art/cohub 12.4.0 → 12.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/dist/board/model/media-playback.d.ts +1 -0
- package/dist/board/replica/index.d.ts +3 -2
- package/dist/board/replica/index.js +15 -4
- package/dist/board/replica/indexeddb.js +14 -85
- package/dist/board/stage/assets.d.ts +3 -2
- package/dist/board/stage/assets.js +53 -21
- package/dist/board/stage/scene.js +11 -4
- package/dist/chunks/app-realtime.js +1 -1
- package/dist/chunks/environment.d.ts +88 -8
- package/dist/chunks/environment.js +4 -3
- package/dist/chunks/http-error.js +28 -5
- package/dist/chunks/http.d.ts +91 -15
- package/dist/chunks/http.js +696 -35
- 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 +33 -4
- package/dist/indexeddb.js +79 -0
- package/dist/protocol/dist/board.d.ts +19 -1
- package/dist/protocol/dist/index.d.ts +2 -2
- package/dist/voice-input.js +1 -1
- package/dist/websocket.js +37 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,14 @@ await session.messages.send({
|
|
|
95
95
|
});
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
## Space files
|
|
99
|
+
|
|
100
|
+
`space.files.read(path)` and `space.files.resolveUrl(path)` share requests: reads
|
|
101
|
+
made in the same task become one batched read, and concurrent reads of a file
|
|
102
|
+
share one request. In browsers with realtime, resolved URLs are remembered per
|
|
103
|
+
user across reloads, confirmed in the background and dropped when the file
|
|
104
|
+
changes; call `clearFileUrlCache()` on sign-out.
|
|
105
|
+
|
|
98
106
|
## Boards
|
|
99
107
|
|
|
100
108
|
A Board is one document of `board` settings, `items` and `animations`. Bind
|
|
@@ -197,16 +205,14 @@ editor.addMedia("https://cdn.example.com/media?id=42", worldPoint(400, 0), {
|
|
|
197
205
|
snapshot: { title: "Launch", naturalWidth: 1920, naturalHeight: 1080 },
|
|
198
206
|
});
|
|
199
207
|
|
|
200
|
-
// Media URLs load directly;
|
|
201
|
-
// A published copy can resolve
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
resolvePlaybackUrl: (path: string) => space.files.resolveUrl(path, { purpose: "playback" }),
|
|
205
|
-
};
|
|
208
|
+
// Media URLs load directly; Space paths resolve through the Space's file URLs,
|
|
209
|
+
// which reload pictures when their file changes. A published copy can resolve
|
|
210
|
+
// paths against captured artifacts instead.
|
|
211
|
+
const source = space.boards.assetSource();
|
|
206
212
|
const stage = mountBoardStage(element, {
|
|
207
213
|
editor,
|
|
208
214
|
source,
|
|
209
|
-
assets: createBoardAssetManager(
|
|
215
|
+
assets: createBoardAssetManager(source),
|
|
210
216
|
views: [{
|
|
211
217
|
type: "acme.order",
|
|
212
218
|
// Mount anything: a React root, a Vue app, plain DOM.
|
|
@@ -245,9 +251,10 @@ Pictures your own items show go through `itemAsset`. They share the built-in
|
|
|
245
251
|
loader: CDN variants sized to the screen, loads ordered by where the view is
|
|
246
252
|
heading, prefetch, compact copies when panned away, device budgets, one cache
|
|
247
253
|
and exports. `src` is a Space path, resolved like an image item's, or a URL;
|
|
248
|
-
`fit: "cover"` sizes pictures cropped to fill their box by the short edge.
|
|
249
|
-
|
|
250
|
-
when
|
|
254
|
+
`fit: "cover"` sizes pictures cropped to fill their box by the short edge. When
|
|
255
|
+
the data it reads changes for some items, call `invalidateItemAssets(ids)` and
|
|
256
|
+
`stage.invalidate({ items: ids })` (omit `ids` when everything changed); call
|
|
257
|
+
`invalidatePath` when a file changes:
|
|
251
258
|
|
|
252
259
|
```ts
|
|
253
260
|
const assets = createBoardAssetManager({
|
|
@@ -6,6 +6,7 @@ type BoardAssetUrlOptions = {
|
|
|
6
6
|
type BoardAssetSource = {
|
|
7
7
|
resolveFileUrl: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
|
|
8
8
|
resolvePlaybackUrl?: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
|
|
9
|
+
subscribe?: (listener: (path: string) => void) => () => void;
|
|
9
10
|
};
|
|
10
11
|
declare function resolveBoardMediaUrl(src: string, source: BoardAssetSource, purpose?: "preview" | "playback"): Promise<string | null>;
|
|
11
12
|
type BoardPlayableMedia = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BoardDelta, BoardDocument, BoardPatch } from "../../protocol/dist/board-model.js";
|
|
2
|
-
import { BoardApplyResult, BoardPlaybackSnapshot, BoardReadResult } from "../../protocol/dist/board.js";
|
|
2
|
+
import { BoardApplyResult, BoardPlaybackSnapshot, BoardReadInput, BoardReadResult } from "../../protocol/dist/board.js";
|
|
3
3
|
import "../../protocol/dist/index.js";
|
|
4
4
|
import { BoardReplicaDatabaseOptions, BoardReplicaScope, clearBoardReplicaStorage, createIndexedDbBoardReplicaStorage } from "./indexeddb.js";
|
|
5
5
|
//#region src/board/replica/index.d.ts
|
|
@@ -14,7 +14,8 @@ type BoardChangedPayload = {
|
|
|
14
14
|
after?: BoardDelta;
|
|
15
15
|
};
|
|
16
16
|
type BoardRemote = {
|
|
17
|
-
|
|
17
|
+
/** `only: []` reads metadata only; remotes may return the full Board. */
|
|
18
|
+
get(read?: Pick<BoardReadInput, "only">): Promise<BoardReadResult>;
|
|
18
19
|
apply(patch: BoardPatch, options: {
|
|
19
20
|
mutationId: string;
|
|
20
21
|
clientId?: string;
|
|
@@ -57,7 +57,7 @@ function createBoardReplica(options) {
|
|
|
57
57
|
return remote.subscribe({
|
|
58
58
|
changed: (event) => receive(event.payload),
|
|
59
59
|
playback: (event) => receivePlayback(event.payload.playback),
|
|
60
|
-
resync: () => void
|
|
60
|
+
resync: () => void resync()
|
|
61
61
|
});
|
|
62
62
|
} catch {
|
|
63
63
|
return null;
|
|
@@ -220,7 +220,7 @@ function createBoardReplica(options) {
|
|
|
220
220
|
}
|
|
221
221
|
emit();
|
|
222
222
|
}
|
|
223
|
-
function retry() {
|
|
223
|
+
function retry(read = true) {
|
|
224
224
|
if (disposed) return Promise.resolve();
|
|
225
225
|
if (retryTimer) clearTimeout(retryTimer);
|
|
226
226
|
retryTimer = null;
|
|
@@ -228,7 +228,18 @@ function createBoardReplica(options) {
|
|
|
228
228
|
readError = null;
|
|
229
229
|
writeError = null;
|
|
230
230
|
emit();
|
|
231
|
-
return refresh().then(flush);
|
|
231
|
+
return (read ? refresh() : Promise.resolve()).then(flush);
|
|
232
|
+
}
|
|
233
|
+
async function resync() {
|
|
234
|
+
await refreshing;
|
|
235
|
+
if (disposed) return;
|
|
236
|
+
if (!loaded || readError) return retry();
|
|
237
|
+
const revision = playbackRevision;
|
|
238
|
+
const head = await remote.get({ only: [] }).catch(() => null);
|
|
239
|
+
if (disposed) return;
|
|
240
|
+
if (!head || head.version > version) return retry();
|
|
241
|
+
if (revision === playbackRevision) playback = head.playback;
|
|
242
|
+
return retry(false);
|
|
232
243
|
}
|
|
233
244
|
function onOnline() {
|
|
234
245
|
if (pending.length || readError || writeError) retry();
|
|
@@ -276,7 +287,7 @@ function createBoardReplica(options) {
|
|
|
276
287
|
receive,
|
|
277
288
|
receivePlayback,
|
|
278
289
|
refresh,
|
|
279
|
-
retry,
|
|
290
|
+
retry: () => retry(),
|
|
280
291
|
dispose() {
|
|
281
292
|
disposed = true;
|
|
282
293
|
globalThis.removeEventListener?.("online", onOnline);
|
|
@@ -1,102 +1,31 @@
|
|
|
1
|
+
import { deleteByIndex, indexedDb } from "../../indexeddb.js";
|
|
1
2
|
//#region src/board/replica/indexeddb.ts
|
|
2
3
|
const DATABASE = "cohub-board";
|
|
3
4
|
const VERSION = 1;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const opening = new Promise((resolve, reject) => {
|
|
10
|
-
const factory = globalThis.indexedDB;
|
|
11
|
-
if (!factory) {
|
|
12
|
-
reject(/* @__PURE__ */ new Error("IndexedDB is unavailable."));
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
let late = false;
|
|
16
|
-
const timer = setTimeout(() => {
|
|
17
|
-
late = true;
|
|
18
|
-
reject(/* @__PURE__ */ new Error("IndexedDB did not open in time."));
|
|
19
|
-
}, OPEN_TIMEOUT_MS);
|
|
20
|
-
const request = factory.open(name, VERSION);
|
|
21
|
-
request.onupgradeneeded = () => {
|
|
22
|
-
const db = request.result;
|
|
23
|
-
db.createObjectStore("documents", { keyPath: "board" }).createIndex("user", "userKey");
|
|
24
|
-
const pending = db.createObjectStore("pending", { keyPath: ["board", "mutationId"] });
|
|
25
|
-
pending.createIndex("board", "board");
|
|
26
|
-
pending.createIndex("user", "userKey");
|
|
27
|
-
};
|
|
28
|
-
request.onsuccess = () => {
|
|
29
|
-
clearTimeout(timer);
|
|
30
|
-
const db = request.result;
|
|
31
|
-
if (late) {
|
|
32
|
-
db.close();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
const forget = () => {
|
|
36
|
-
if (connections.get(name) === opening) connections.delete(name);
|
|
37
|
-
};
|
|
38
|
-
db.onversionchange = () => {
|
|
39
|
-
db.close();
|
|
40
|
-
forget();
|
|
41
|
-
};
|
|
42
|
-
db.onclose = forget;
|
|
43
|
-
resolve(db);
|
|
44
|
-
};
|
|
45
|
-
request.onerror = () => {
|
|
46
|
-
clearTimeout(timer);
|
|
47
|
-
reject(request.error ?? /* @__PURE__ */ new Error("IndexedDB failed to open."));
|
|
48
|
-
};
|
|
49
|
-
});
|
|
50
|
-
connections.set(name, opening);
|
|
51
|
-
opening.catch(() => {
|
|
52
|
-
if (connections.get(name) === opening) connections.delete(name);
|
|
53
|
-
});
|
|
54
|
-
return opening;
|
|
55
|
-
}
|
|
56
|
-
async function transact(name, stores, mode, work, retry = true) {
|
|
57
|
-
const db = await open(name);
|
|
58
|
-
let tx;
|
|
59
|
-
try {
|
|
60
|
-
tx = db.transaction(stores, mode);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (!retry) throw error;
|
|
63
|
-
db.close();
|
|
64
|
-
connections.delete(name);
|
|
65
|
-
return transact(name, stores, mode, work, false);
|
|
66
|
-
}
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const result = work(tx);
|
|
69
|
-
tx.oncomplete = () => resolve(result());
|
|
70
|
-
tx.onerror = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction failed."));
|
|
71
|
-
tx.onabort = () => reject(tx.error ?? /* @__PURE__ */ new Error("IndexedDB transaction aborted."));
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
function deleteByIndex(store, index, key) {
|
|
75
|
-
const request = store.index(index).openKeyCursor(IDBKeyRange.only(key));
|
|
76
|
-
request.onsuccess = () => {
|
|
77
|
-
const cursor = request.result;
|
|
78
|
-
if (!cursor) return;
|
|
79
|
-
store.delete(cursor.primaryKey);
|
|
80
|
-
cursor.continue();
|
|
81
|
-
};
|
|
5
|
+
function upgrade(db) {
|
|
6
|
+
db.createObjectStore("documents", { keyPath: "board" }).createIndex("user", "userKey");
|
|
7
|
+
const pending = db.createObjectStore("pending", { keyPath: ["board", "mutationId"] });
|
|
8
|
+
pending.createIndex("board", "board");
|
|
9
|
+
pending.createIndex("user", "userKey");
|
|
82
10
|
}
|
|
11
|
+
const database = (name) => indexedDb(name, VERSION, upgrade);
|
|
83
12
|
let lastOrder = 0;
|
|
84
13
|
function createIndexedDbBoardReplicaStorage(scope, options = {}) {
|
|
85
|
-
const
|
|
14
|
+
const { transact } = database(options.database ?? DATABASE);
|
|
86
15
|
const board = JSON.stringify([
|
|
87
16
|
scope.userKey,
|
|
88
17
|
scope.spaceId,
|
|
89
18
|
scope.boardId
|
|
90
19
|
]);
|
|
91
20
|
return {
|
|
92
|
-
listPending: () => transact(
|
|
21
|
+
listPending: () => transact(["pending"], "readonly", (tx) => {
|
|
93
22
|
const request = tx.objectStore("pending").index("board").getAll(IDBKeyRange.only(board));
|
|
94
23
|
return () => request.result.sort((a, b) => a.order - b.order).map(({ mutationId, patch }) => ({
|
|
95
24
|
mutationId,
|
|
96
25
|
patch
|
|
97
26
|
}));
|
|
98
27
|
}),
|
|
99
|
-
putPending: ({ mutationId, patch }) => transact(
|
|
28
|
+
putPending: ({ mutationId, patch }) => transact(["pending"], "readwrite", (tx) => {
|
|
100
29
|
lastOrder = Math.max(Date.now(), lastOrder + 1);
|
|
101
30
|
const record = {
|
|
102
31
|
board,
|
|
@@ -108,11 +37,11 @@ function createIndexedDbBoardReplicaStorage(scope, options = {}) {
|
|
|
108
37
|
tx.objectStore("pending").put(record);
|
|
109
38
|
return () => void 0;
|
|
110
39
|
}),
|
|
111
|
-
deletePending: (mutationId) => transact(
|
|
40
|
+
deletePending: (mutationId) => transact(["pending"], "readwrite", (tx) => {
|
|
112
41
|
tx.objectStore("pending").delete([board, mutationId]);
|
|
113
42
|
return () => void 0;
|
|
114
43
|
}),
|
|
115
|
-
readDocument: () => transact(
|
|
44
|
+
readDocument: () => transact(["documents"], "readonly", (tx) => {
|
|
116
45
|
const request = tx.objectStore("documents").get(board);
|
|
117
46
|
return () => {
|
|
118
47
|
const record = request.result;
|
|
@@ -122,7 +51,7 @@ function createIndexedDbBoardReplicaStorage(scope, options = {}) {
|
|
|
122
51
|
} : null;
|
|
123
52
|
};
|
|
124
53
|
}),
|
|
125
|
-
writeDocument: (version, document) => transact(
|
|
54
|
+
writeDocument: (version, document) => transact(["documents"], "readwrite", (tx) => {
|
|
126
55
|
const store = tx.objectStore("documents");
|
|
127
56
|
const request = store.get(board);
|
|
128
57
|
request.onsuccess = () => {
|
|
@@ -142,7 +71,7 @@ function createIndexedDbBoardReplicaStorage(scope, options = {}) {
|
|
|
142
71
|
}
|
|
143
72
|
async function clearBoardReplicaStorage(options = {}) {
|
|
144
73
|
const { userKey } = options;
|
|
145
|
-
await
|
|
74
|
+
await database(options.database ?? DATABASE).transact(["documents", "pending"], "readwrite", (tx) => {
|
|
146
75
|
for (const name of ["documents", "pending"]) {
|
|
147
76
|
const store = tx.objectStore(name);
|
|
148
77
|
if (userKey === void 0) store.clear();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ModelFormat } from "../../protocol/dist/fs/mime.js";
|
|
2
2
|
import { BoardSceneItem } from "../model/scene.js";
|
|
3
3
|
import { BoardAssetMedia, BoardAssetRef } from "../model/asset-key.js";
|
|
4
|
-
import { BoardAssetUrlOptions } from "../model/media-playback.js";
|
|
4
|
+
import { BoardAssetSource, BoardAssetUrlOptions } from "../model/media-playback.js";
|
|
5
5
|
import "../model/index.js";
|
|
6
6
|
import { LruBudget } from "./lru.js";
|
|
7
7
|
import { BoardImageTexturePool } from "./texture-pool.js";
|
|
@@ -25,7 +25,7 @@ type BoardModelPreviews = {
|
|
|
25
25
|
type BoardAssetManager = {
|
|
26
26
|
assetKey: (item: BoardSceneItem) => string | null;
|
|
27
27
|
invalidatePath: (path: string) => void;
|
|
28
|
-
invalidateItemAssets: () => void;
|
|
28
|
+
invalidateItemAssets: (ids?: Iterable<string>) => void;
|
|
29
29
|
requestItem: (item: BoardSceneItem, edge?: number) => void;
|
|
30
30
|
/** World point used to prioritize queued loads. */
|
|
31
31
|
focus: (point: {
|
|
@@ -61,6 +61,7 @@ type BoardAssetChange = "texture" | "tone";
|
|
|
61
61
|
type BoardAssetManagerOptions = {
|
|
62
62
|
/** Resolve a Space file path to a URL a texture can load from; null when unavailable. */
|
|
63
63
|
resolveFileUrl: (path: string, options?: BoardAssetUrlOptions) => Promise<string | null>;
|
|
64
|
+
subscribe?: BoardAssetSource["subscribe"];
|
|
64
65
|
/** undefined keeps the built-in rule, null shows none; cached until `invalidateItemAssets`. */
|
|
65
66
|
itemAsset?: (item: BoardSceneItem) => BoardAssetRef | null | undefined;
|
|
66
67
|
concurrency?: number;
|
|
@@ -80,6 +80,7 @@ function createBoardAssetManager(options) {
|
|
|
80
80
|
}
|
|
81
81
|
const resolveFileUrl = options.resolveFileUrl;
|
|
82
82
|
const entries = /* @__PURE__ */ new Map();
|
|
83
|
+
const keysByPath = /* @__PURE__ */ new Map();
|
|
83
84
|
const pathVersions = /* @__PURE__ */ new Map();
|
|
84
85
|
const starters = /* @__PURE__ */ new Map();
|
|
85
86
|
const failedModels = /* @__PURE__ */ new Set();
|
|
@@ -92,6 +93,7 @@ function createBoardAssetManager(options) {
|
|
|
92
93
|
model: 0
|
|
93
94
|
};
|
|
94
95
|
let disposed = false;
|
|
96
|
+
let stopSource = null;
|
|
95
97
|
let focus = null;
|
|
96
98
|
let trimTimer = null;
|
|
97
99
|
let trimSince = 0;
|
|
@@ -131,16 +133,33 @@ function createBoardAssetManager(options) {
|
|
|
131
133
|
cancelTone = toneQueue.size > 0 ? scheduleBoardIdle(readNextTone, 0) : null;
|
|
132
134
|
}
|
|
133
135
|
const keys = /* @__PURE__ */ new WeakMap();
|
|
136
|
+
const itemEpochs = /* @__PURE__ */ new Map();
|
|
134
137
|
let keyEpoch = 0;
|
|
135
138
|
function managedAssetKey(item) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
const itemEpoch = itemEpochs.get(item.id) ?? 0;
|
|
140
|
+
let record = keys.get(item);
|
|
141
|
+
if (record?.epoch !== keyEpoch || record.itemEpoch !== itemEpoch) {
|
|
142
|
+
const base = resolveAssetKey(item);
|
|
143
|
+
const source = base ? boardAssetKeySource(base) : null;
|
|
144
|
+
const path = source?.kind === "file" ? source.value : null;
|
|
145
|
+
record = {
|
|
146
|
+
epoch: keyEpoch,
|
|
147
|
+
itemEpoch,
|
|
148
|
+
base,
|
|
149
|
+
path,
|
|
150
|
+
version: 0,
|
|
151
|
+
key: base
|
|
152
|
+
};
|
|
153
|
+
keys.set(item, record);
|
|
154
|
+
}
|
|
155
|
+
if (record.path !== null) {
|
|
156
|
+
const version = pathVersions.get(record.path) ?? 0;
|
|
157
|
+
if (version !== record.version) {
|
|
158
|
+
record.version = version;
|
|
159
|
+
record.key = `local:${version}:${record.base}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return record.key;
|
|
144
163
|
}
|
|
145
164
|
function resolveAssetKey(item) {
|
|
146
165
|
const asset = options.itemAsset?.(item);
|
|
@@ -149,9 +168,7 @@ function createBoardAssetManager(options) {
|
|
|
149
168
|
const source = boardAssetKeySource(key);
|
|
150
169
|
if (source?.media === "video" && options.loadVideoPreviews === false) return null;
|
|
151
170
|
if (source?.media === "model" && !modelPreviewable(item)) return null;
|
|
152
|
-
|
|
153
|
-
const version = pathVersions.get(source.value) ?? 0;
|
|
154
|
-
return version > 0 ? `local:${version}:${key}` : key;
|
|
171
|
+
return key;
|
|
155
172
|
}
|
|
156
173
|
function clearRetry(entry) {
|
|
157
174
|
if (entry.retryTimer) {
|
|
@@ -163,9 +180,11 @@ function createBoardAssetManager(options) {
|
|
|
163
180
|
let entry = entries.get(key);
|
|
164
181
|
if (!entry) {
|
|
165
182
|
const source = boardAssetKeySource(key);
|
|
183
|
+
const path = source?.kind === "file" ? source.value : null;
|
|
166
184
|
entry = {
|
|
167
185
|
media: source?.media ?? "image",
|
|
168
186
|
cover: source?.fit === "cover",
|
|
187
|
+
path,
|
|
169
188
|
source: null,
|
|
170
189
|
url: null,
|
|
171
190
|
texture: null,
|
|
@@ -182,6 +201,11 @@ function createBoardAssetManager(options) {
|
|
|
182
201
|
lastUsedAt: now()
|
|
183
202
|
};
|
|
184
203
|
entries.set(key, entry);
|
|
204
|
+
if (path !== null) {
|
|
205
|
+
const keyed = keysByPath.get(path);
|
|
206
|
+
if (keyed) keyed.add(key);
|
|
207
|
+
else keysByPath.set(path, /* @__PURE__ */ new Set([key]));
|
|
208
|
+
}
|
|
185
209
|
}
|
|
186
210
|
return entry;
|
|
187
211
|
}
|
|
@@ -203,6 +227,10 @@ function createBoardAssetManager(options) {
|
|
|
203
227
|
starters.delete(key);
|
|
204
228
|
if (entry.url || entry.texture) releaseTexture(entry.media, entry.url ?? "", entry.texture);
|
|
205
229
|
entries.delete(key);
|
|
230
|
+
if (entry.path === null) return;
|
|
231
|
+
const keyed = keysByPath.get(entry.path);
|
|
232
|
+
keyed?.delete(key);
|
|
233
|
+
if (keyed?.size === 0) keysByPath.delete(entry.path);
|
|
206
234
|
}
|
|
207
235
|
function scheduleTrim() {
|
|
208
236
|
const at = now();
|
|
@@ -464,18 +492,19 @@ function createBoardAssetManager(options) {
|
|
|
464
492
|
const manager = {
|
|
465
493
|
assetKey: managedAssetKey,
|
|
466
494
|
invalidatePath(path) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const source = boardAssetKeySource(key);
|
|
470
|
-
return source?.kind === "file" && source.value === path;
|
|
471
|
-
});
|
|
472
|
-
if (stale.length === 0) return;
|
|
495
|
+
const stale = keysByPath.get(path);
|
|
496
|
+
if (disposed || !stale) return;
|
|
473
497
|
pathVersions.set(path, (pathVersions.get(path) ?? 0) + 1);
|
|
474
|
-
|
|
475
|
-
for (const key of stale) notify(key);
|
|
498
|
+
for (const key of [...stale]) notify(key);
|
|
476
499
|
},
|
|
477
|
-
invalidateItemAssets() {
|
|
478
|
-
if (options.itemAsset)
|
|
500
|
+
invalidateItemAssets(ids) {
|
|
501
|
+
if (!options.itemAsset) return;
|
|
502
|
+
if (!ids) {
|
|
503
|
+
keyEpoch += 1;
|
|
504
|
+
itemEpochs.clear();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
for (const id of ids) itemEpochs.set(id, (itemEpochs.get(id) ?? 0) + 1);
|
|
479
508
|
},
|
|
480
509
|
requestItem(item, edge = Number.POSITIVE_INFINITY) {
|
|
481
510
|
if (disposed) return;
|
|
@@ -659,6 +688,7 @@ function createBoardAssetManager(options) {
|
|
|
659
688
|
},
|
|
660
689
|
destroy() {
|
|
661
690
|
disposed = true;
|
|
691
|
+
stopSource?.();
|
|
662
692
|
if (trimTimer !== null) clearTimeout(trimTimer);
|
|
663
693
|
trimTimer = null;
|
|
664
694
|
cancelTone?.();
|
|
@@ -669,6 +699,7 @@ function createBoardAssetManager(options) {
|
|
|
669
699
|
unwarmable.clear();
|
|
670
700
|
listeners.clear();
|
|
671
701
|
pathVersions.clear();
|
|
702
|
+
itemEpochs.clear();
|
|
672
703
|
starters.clear();
|
|
673
704
|
failedModels.clear();
|
|
674
705
|
for (const [key, entry] of entries) evict(key, entry);
|
|
@@ -676,6 +707,7 @@ function createBoardAssetManager(options) {
|
|
|
676
707
|
for (const release of [...retiring]) release();
|
|
677
708
|
}
|
|
678
709
|
};
|
|
710
|
+
stopSource = options.subscribe?.((path) => manager.invalidatePath(path)) ?? null;
|
|
679
711
|
return manager;
|
|
680
712
|
}
|
|
681
713
|
//#endregion
|
|
@@ -14,6 +14,7 @@ const Z_LIVE_STROKE = Number.MAX_SAFE_INTEGER - 1;
|
|
|
14
14
|
const Z_OVERLAY = Number.MAX_SAFE_INTEGER;
|
|
15
15
|
const POOL_LIMIT_PER_RENDERER = 48;
|
|
16
16
|
const EMPTY_IDS = /* @__PURE__ */ new Set();
|
|
17
|
+
const NO_PLATES = /* @__PURE__ */ new Map();
|
|
17
18
|
function idSetSignature(ids) {
|
|
18
19
|
let hash = ids.size;
|
|
19
20
|
for (const id of ids) {
|
|
@@ -257,13 +258,19 @@ function createBoardScene(options) {
|
|
|
257
258
|
let paintedPlan = null;
|
|
258
259
|
let paintedSig = "";
|
|
259
260
|
let platesPending = false;
|
|
260
|
-
let covered =
|
|
261
|
+
let covered = NO_PLATES;
|
|
261
262
|
/**
|
|
262
263
|
* Repaint the plates whose cards changed, within a per-frame budget. A card
|
|
263
264
|
* is covered once a plate draws it; until then it stays live if it already is.
|
|
264
265
|
*/
|
|
265
266
|
function paintPlates(input, current) {
|
|
266
267
|
const { context, globalSig, scene } = input;
|
|
268
|
+
for (const id of input.staleIds ?? EMPTY_IDS) {
|
|
269
|
+
const plate = covered.get(id);
|
|
270
|
+
if (!plate) continue;
|
|
271
|
+
plate.sig = "";
|
|
272
|
+
paintedPlan = null;
|
|
273
|
+
}
|
|
267
274
|
if (current === paintedPlan && globalSig === paintedSig) return false;
|
|
268
275
|
const start = performance.now();
|
|
269
276
|
const allowance = budget.allow("plate");
|
|
@@ -303,8 +310,8 @@ function createBoardScene(options) {
|
|
|
303
310
|
plate.graphics.destroy();
|
|
304
311
|
changed = true;
|
|
305
312
|
}
|
|
306
|
-
const next = /* @__PURE__ */ new
|
|
307
|
-
for (const plate of plates.values()) for (const item of plate.items) next.
|
|
313
|
+
const next = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const plate of plates.values()) for (const item of plate.items) next.set(item.id, plate);
|
|
308
315
|
covered = next;
|
|
309
316
|
budget.spend("plate", painted, paintMs);
|
|
310
317
|
platesPending = pending;
|
|
@@ -328,7 +335,7 @@ function createBoardScene(options) {
|
|
|
328
335
|
paintedPlan = null;
|
|
329
336
|
paintedSig = "";
|
|
330
337
|
platesPending = false;
|
|
331
|
-
covered =
|
|
338
|
+
covered = NO_PLATES;
|
|
332
339
|
plan = null;
|
|
333
340
|
planInputs = [];
|
|
334
341
|
nearIds = EMPTY_IDS;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as readRtcTransportStats, j as waitForIceGathering } from "./http.js";
|
|
2
2
|
import { c as HttpError, p as REALTIME_ROOM_EVENT_NAME_PATTERN } from "./http-error.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
//#region \0rolldown/runtime.js
|