@tldraw/editor 5.5.0-canary.db5d20befa38 → 5.5.0-canary.ddcb7ca958fa
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/dist-cjs/index.d.ts +14 -11
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/editor/managers/EditorManager.js +11 -2
- package/dist-cjs/lib/editor/managers/EditorManager.js.map +2 -2
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js +46 -60
- package/dist-cjs/lib/editor/managers/PerformanceManager/PerformanceManager.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +14 -11
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/editor/managers/EditorManager.mjs +11 -2
- package/dist-esm/lib/editor/managers/EditorManager.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs +46 -60
- package/dist-esm/lib/editor/managers/PerformanceManager/PerformanceManager.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/package.json +7 -7
- package/src/lib/editor/managers/EditorManager.test.ts +32 -1
- package/src/lib/editor/managers/EditorManager.ts +15 -3
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.test.ts +79 -0
- package/src/lib/editor/managers/PerformanceManager/PerformanceManager.ts +51 -78
- package/src/version.ts +3 -3
package/dist-cjs/index.d.ts
CHANGED
|
@@ -4579,6 +4579,8 @@ export declare const EditorContext: React_3.Context<Editor | null>;
|
|
|
4579
4579
|
* - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`
|
|
4580
4580
|
* - DOM listener: `this.register(() => el.removeEventListener(...))`
|
|
4581
4581
|
* - Other resource (cache, index, child manager): `this.register(() => x.dispose())`
|
|
4582
|
+
* - Resource held only part of the time (a listener attached while subscribers exist):
|
|
4583
|
+
* `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`
|
|
4582
4584
|
*
|
|
4583
4585
|
* For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped
|
|
4584
4586
|
* and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a
|
|
@@ -4598,11 +4600,18 @@ export declare abstract class EditorManager {
|
|
|
4598
4600
|
* make a resource "see itself out". Returns the same function for convenience.
|
|
4599
4601
|
*/
|
|
4600
4602
|
protected register(dispose: () => void): () => void;
|
|
4603
|
+
/**
|
|
4604
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
4605
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
4606
|
+
* torn down twice.
|
|
4607
|
+
*/
|
|
4608
|
+
protected unregister(dispose: () => void): void;
|
|
4601
4609
|
/**
|
|
4602
4610
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
4603
|
-
* is removed automatically on `dispose()`.
|
|
4611
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
4612
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
4604
4613
|
*/
|
|
4605
|
-
protected addEditorEvent<E extends keyof TLEventMap>(event: E, fn: (...args: TLEventMap[E]) => void): void;
|
|
4614
|
+
protected addEditorEvent<E extends keyof TLEventMap>(event: E, fn: (...args: TLEventMap[E]) => void): () => void;
|
|
4606
4615
|
/* Excluded from this release type: dispose */
|
|
4607
4616
|
}
|
|
4608
4617
|
|
|
@@ -5990,17 +5999,11 @@ export declare class PerformanceApiAdapter {
|
|
|
5990
5999
|
*
|
|
5991
6000
|
* @public
|
|
5992
6001
|
*/
|
|
5993
|
-
export declare class PerformanceManager {
|
|
6002
|
+
export declare class PerformanceManager extends EditorManager {
|
|
5994
6003
|
/* Excluded from this release type: emitter */
|
|
5995
|
-
private editor;
|
|
5996
6004
|
private activeInteraction;
|
|
5997
6005
|
private activeCamera;
|
|
5998
|
-
private
|
|
5999
|
-
private shapeCreatedCleanup;
|
|
6000
|
-
private shapeEditedCleanup;
|
|
6001
|
-
private shapeDeletedCleanup;
|
|
6002
|
-
private loafObserver;
|
|
6003
|
-
constructor(editor: Editor);
|
|
6006
|
+
private readonly lazyCleanups;
|
|
6004
6007
|
/**
|
|
6005
6008
|
* Subscribe to a performance event. Returns an unsubscribe function.
|
|
6006
6009
|
*
|
|
@@ -6034,11 +6037,11 @@ export declare class PerformanceManager {
|
|
|
6034
6037
|
private _onShapesEdited;
|
|
6035
6038
|
private _onShapesDeleted;
|
|
6036
6039
|
private _startLoafObserver;
|
|
6037
|
-
private _stopLoafObserver;
|
|
6038
6040
|
private _needsFrameListener;
|
|
6039
6041
|
private _needsLoafObserver;
|
|
6040
6042
|
private _maybeAttachLazyListeners;
|
|
6041
6043
|
private _maybeDetachLazyListeners;
|
|
6044
|
+
private _detachLazy;
|
|
6042
6045
|
}
|
|
6043
6046
|
|
|
6044
6047
|
/**
|
package/dist-cjs/index.js
CHANGED
|
@@ -392,7 +392,7 @@ var import_uniq = require("./lib/utils/uniq");
|
|
|
392
392
|
var import_defaultThemes2 = require("./lib/editor/managers/ThemeManager/defaultThemes");
|
|
393
393
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
394
394
|
"@tldraw/editor",
|
|
395
|
-
"5.5.0-canary.
|
|
395
|
+
"5.5.0-canary.ddcb7ca958fa",
|
|
396
396
|
"cjs"
|
|
397
397
|
);
|
|
398
398
|
//# sourceMappingURL=index.js.map
|
|
@@ -35,13 +35,22 @@ class EditorManager {
|
|
|
35
35
|
this.disposables.add(dispose);
|
|
36
36
|
return dispose;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
40
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
41
|
+
* torn down twice.
|
|
42
|
+
*/
|
|
43
|
+
unregister(dispose) {
|
|
44
|
+
if (this.disposables.delete(dispose)) dispose();
|
|
45
|
+
}
|
|
38
46
|
/**
|
|
39
47
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
40
|
-
* is removed automatically on `dispose()`.
|
|
48
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
49
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
41
50
|
*/
|
|
42
51
|
addEditorEvent(event, fn) {
|
|
43
52
|
this.editor.on(event, fn);
|
|
44
|
-
this.register(() => this.editor.off(event, fn));
|
|
53
|
+
return this.register(() => this.editor.off(event, fn));
|
|
45
54
|
}
|
|
46
55
|
/** @internal */
|
|
47
56
|
dispose() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/editor/managers/EditorManager.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Editor } from '../Editor'\nimport type { TLEventMap } from '../types/emit-types'\n\n/**\n * Base class for editor-attached managers that need to clean up after themselves.\n *\n * Extending `EditorManager` gives a manager one consistent teardown contract: register\n * anything that needs undoing, and it runs automatically on `dispose()`. This keeps\n * subscription lifecycle symmetric \u2014 whatever sets a thing up is what tears it down \u2014 so\n * it can't be forgotten (the failure mode behind the strict-mode camera bug, #8892).\n *\n * Register cleanup by what you're creating:\n *\n * - Editor bus event (`tick`, `frame`, `change`, \u2026): {@link EditorManager.addEditorEvent}\n * - Store side effect (`editor.sideEffects.register\u2026`): `this.register(disposer)`\n * - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`\n * - DOM listener: `this.register(() => el.removeEventListener(...))`\n * - Other resource (cache, index, child manager): `this.register(() => x.dispose())`\n *\n * For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped\n * and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a\n * manager, use `editor.disposables`.\n *\n * If a manager needs ordered teardown (e.g. pause a loop before cancelling it), override\n * `dispose()` and call `super.dispose()` last \u2014 see `TickManager`.\n *\n * @public\n */\nexport abstract class EditorManager {\n\tprotected readonly disposables = new Set<() => void>()\n\n\tconstructor(protected readonly editor: Editor) {}\n\n\t/**\n\t * Register a teardown function to run on `dispose()`. The universal way for a manager to\n\t * make a resource \"see itself out\". Returns the same function for convenience.\n\t */\n\tprotected register(dispose: () => void): () => void {\n\t\tthis.disposables.add(dispose)\n\t\treturn dispose\n\t}\n\n\t/**\n\t * Subscribe to an editor bus event and register the matching unsubscribe, so the listener\n\t * is removed automatically on `dispose()`.\n\t */\n\tprotected addEditorEvent<E extends keyof TLEventMap>(\n\t\tevent: E,\n\t\tfn: (...args: TLEventMap[E]) => void\n\t): void {\n\t\tthis.editor.on(event, fn as any)\n\t\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
4
|
+
"sourcesContent": ["import type { Editor } from '../Editor'\nimport type { TLEventMap } from '../types/emit-types'\n\n/**\n * Base class for editor-attached managers that need to clean up after themselves.\n *\n * Extending `EditorManager` gives a manager one consistent teardown contract: register\n * anything that needs undoing, and it runs automatically on `dispose()`. This keeps\n * subscription lifecycle symmetric \u2014 whatever sets a thing up is what tears it down \u2014 so\n * it can't be forgotten (the failure mode behind the strict-mode camera bug, #8892).\n *\n * Register cleanup by what you're creating:\n *\n * - Editor bus event (`tick`, `frame`, `change`, \u2026): {@link EditorManager.addEditorEvent}\n * - Store side effect (`editor.sideEffects.register\u2026`): `this.register(disposer)`\n * - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`\n * - DOM listener: `this.register(() => el.removeEventListener(...))`\n * - Other resource (cache, index, child manager): `this.register(() => x.dispose())`\n * - Resource held only part of the time (a listener attached while subscribers exist):\n * `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`\n *\n * For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped\n * and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a\n * manager, use `editor.disposables`.\n *\n * If a manager needs ordered teardown (e.g. pause a loop before cancelling it), override\n * `dispose()` and call `super.dispose()` last \u2014 see `TickManager`.\n *\n * @public\n */\nexport abstract class EditorManager {\n\tprotected readonly disposables = new Set<() => void>()\n\n\tconstructor(protected readonly editor: Editor) {}\n\n\t/**\n\t * Register a teardown function to run on `dispose()`. The universal way for a manager to\n\t * make a resource \"see itself out\". Returns the same function for convenience.\n\t */\n\tprotected register(dispose: () => void): () => void {\n\t\tthis.disposables.add(dispose)\n\t\treturn dispose\n\t}\n\n\t/**\n\t * Run a registered teardown now instead of at `dispose()`. A teardown that is no longer\n\t * registered (already run by `dispose()`, or never registered) is skipped, so nothing is\n\t * torn down twice.\n\t */\n\tprotected unregister(dispose: () => void): void {\n\t\tif (this.disposables.delete(dispose)) dispose()\n\t}\n\n\t/**\n\t * Subscribe to an editor bus event and register the matching unsubscribe, so the listener\n\t * is removed automatically on `dispose()`. Returns that unsubscribe, for passing to\n\t * {@link EditorManager.unregister} to detach it before `dispose()`.\n\t */\n\tprotected addEditorEvent<E extends keyof TLEventMap>(\n\t\tevent: E,\n\t\tfn: (...args: TLEventMap[E]) => void\n\t): () => void {\n\t\tthis.editor.on(event, fn as any)\n\t\treturn this.register(() => this.editor.off(event, fn as any))\n\t}\n\n\t/** @internal */\n\tdispose(): void {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8BO,MAAe,cAAc;AAAA,EAGnC,YAA+B,QAAgB;AAAhB;AAAA,EAAiB;AAAA,EAAjB;AAAA,EAFZ,cAAc,oBAAI,IAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ3C,SAAS,SAAiC;AACnD,SAAK,YAAY,IAAI,OAAO;AAC5B,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,WAAW,SAA2B;AAC/C,QAAI,KAAK,YAAY,OAAO,OAAO,EAAG,SAAQ;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,eACT,OACA,IACa;AACb,SAAK,OAAO,GAAG,OAAO,EAAS;AAC/B,WAAO,KAAK,SAAS,MAAM,KAAK,OAAO,IAAI,OAAO,EAAS,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,UAAgB;AACf,SAAK,YAAY,QAAQ,CAAC,MAAM,EAAE,CAAC;AACnC,SAAK,YAAY,MAAM;AAAA,EACxB;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -41,6 +41,7 @@ __export(PerformanceManager_exports, {
|
|
|
41
41
|
module.exports = __toCommonJS(PerformanceManager_exports);
|
|
42
42
|
var import_utils = require("@tldraw/utils");
|
|
43
43
|
var import_eventemitter3 = __toESM(require("eventemitter3"), 1);
|
|
44
|
+
var import_EditorManager = require("../EditorManager");
|
|
44
45
|
function percentile(sorted, p) {
|
|
45
46
|
const idx = Math.ceil(p * sorted.length) - 1;
|
|
46
47
|
return sorted[Math.max(0, idx)];
|
|
@@ -72,24 +73,15 @@ function toLoafEntry(entry) {
|
|
|
72
73
|
}))
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
|
-
class PerformanceManager {
|
|
76
|
+
class PerformanceManager extends import_EditorManager.EditorManager {
|
|
76
77
|
/** @internal */
|
|
77
78
|
emitter = new import_eventemitter3.default();
|
|
78
|
-
editor;
|
|
79
79
|
// Active interaction tracking
|
|
80
80
|
activeInteraction = null;
|
|
81
81
|
// Active camera tracking
|
|
82
82
|
activeCamera = null;
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
shapeCreatedCleanup = null;
|
|
86
|
-
shapeEditedCleanup = null;
|
|
87
|
-
shapeDeletedCleanup = null;
|
|
88
|
-
// LoAF observer
|
|
89
|
-
loafObserver = null;
|
|
90
|
-
constructor(editor) {
|
|
91
|
-
this.editor = editor;
|
|
92
|
-
}
|
|
83
|
+
// Cleanups for whatever is currently attached, keyed so a detach unregisters only its own
|
|
84
|
+
lazyCleanups = /* @__PURE__ */ new Map();
|
|
93
85
|
/**
|
|
94
86
|
* Subscribe to a performance event. Returns an unsubscribe function.
|
|
95
87
|
*
|
|
@@ -135,16 +127,9 @@ class PerformanceManager {
|
|
|
135
127
|
if (this.activeCamera?.timeout) this.editor.timers.clearTimeout(this.activeCamera.timeout);
|
|
136
128
|
this.activeInteraction = null;
|
|
137
129
|
this.activeCamera = null;
|
|
138
|
-
this.
|
|
139
|
-
this.frameCleanup = null;
|
|
140
|
-
this.shapeCreatedCleanup?.();
|
|
141
|
-
this.shapeCreatedCleanup = null;
|
|
142
|
-
this.shapeEditedCleanup?.();
|
|
143
|
-
this.shapeEditedCleanup = null;
|
|
144
|
-
this.shapeDeletedCleanup?.();
|
|
145
|
-
this.shapeDeletedCleanup = null;
|
|
146
|
-
this._stopLoafObserver();
|
|
130
|
+
this.lazyCleanups.clear();
|
|
147
131
|
this.emitter.removeAllListeners();
|
|
132
|
+
super.dispose();
|
|
148
133
|
}
|
|
149
134
|
// --- Internal notification methods ---
|
|
150
135
|
/** @internal */
|
|
@@ -370,7 +355,7 @@ class PerformanceManager {
|
|
|
370
355
|
} catch {
|
|
371
356
|
return;
|
|
372
357
|
}
|
|
373
|
-
|
|
358
|
+
const observer = new PerformanceObserver((list) => {
|
|
374
359
|
const isInteractionActive = this.activeInteraction !== null;
|
|
375
360
|
const isCameraActive = this.activeCamera !== null;
|
|
376
361
|
if (!isInteractionActive && !isCameraActive) return;
|
|
@@ -385,13 +370,11 @@ class PerformanceManager {
|
|
|
385
370
|
}
|
|
386
371
|
}
|
|
387
372
|
});
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
this.loafObserver = null;
|
|
394
|
-
}
|
|
373
|
+
observer.observe({ type: "long-animation-frame", buffered: false });
|
|
374
|
+
this.lazyCleanups.set(
|
|
375
|
+
"loaf",
|
|
376
|
+
this.register(() => observer.disconnect())
|
|
377
|
+
);
|
|
395
378
|
}
|
|
396
379
|
// --- Lazy listener management ---
|
|
397
380
|
_needsFrameListener() {
|
|
@@ -401,55 +384,58 @@ class PerformanceManager {
|
|
|
401
384
|
return this.emitter.listenerCount("interaction-end") > 0 || this.emitter.listenerCount("camera-end") > 0;
|
|
402
385
|
}
|
|
403
386
|
_maybeAttachLazyListeners(event) {
|
|
404
|
-
if (!this.
|
|
387
|
+
if (!this.lazyCleanups.has("frame") && (event === "frame" || event === "interaction-start" || event === "interaction-end" || event === "camera-start" || event === "camera-end")) {
|
|
405
388
|
if (this._needsFrameListener()) {
|
|
406
|
-
this.
|
|
407
|
-
this.frameCleanup = () => this.editor.off("frame", this._onFrame);
|
|
389
|
+
this.lazyCleanups.set("frame", this.addEditorEvent("frame", this._onFrame));
|
|
408
390
|
}
|
|
409
391
|
}
|
|
410
|
-
if (!this.
|
|
392
|
+
if (!this.lazyCleanups.has("loaf") && (event === "interaction-end" || event === "camera-end")) {
|
|
411
393
|
if (this._needsLoafObserver()) {
|
|
412
394
|
this._startLoafObserver();
|
|
413
395
|
}
|
|
414
396
|
}
|
|
415
|
-
if (!this.
|
|
416
|
-
this.
|
|
417
|
-
|
|
397
|
+
if (!this.lazyCleanups.has("shapes-created") && event === "shapes-created") {
|
|
398
|
+
this.lazyCleanups.set(
|
|
399
|
+
"shapes-created",
|
|
400
|
+
this.addEditorEvent("created-shapes", this._onShapesCreated)
|
|
401
|
+
);
|
|
418
402
|
}
|
|
419
|
-
if (!this.
|
|
420
|
-
this.
|
|
421
|
-
|
|
403
|
+
if (!this.lazyCleanups.has("shapes-updated") && event === "shapes-updated") {
|
|
404
|
+
this.lazyCleanups.set(
|
|
405
|
+
"shapes-updated",
|
|
406
|
+
this.addEditorEvent("edited-shapes", this._onShapesEdited)
|
|
407
|
+
);
|
|
422
408
|
}
|
|
423
|
-
if (!this.
|
|
424
|
-
this.
|
|
425
|
-
|
|
409
|
+
if (!this.lazyCleanups.has("shapes-deleted") && event === "shapes-deleted") {
|
|
410
|
+
this.lazyCleanups.set(
|
|
411
|
+
"shapes-deleted",
|
|
412
|
+
this.addEditorEvent("deleted-shapes", this._onShapesDeleted)
|
|
413
|
+
);
|
|
426
414
|
}
|
|
427
415
|
}
|
|
428
416
|
_maybeDetachLazyListeners(event) {
|
|
429
|
-
if (
|
|
430
|
-
|
|
431
|
-
this.frameCleanup();
|
|
432
|
-
this.frameCleanup = null;
|
|
433
|
-
}
|
|
417
|
+
if ((event === "frame" || event === "interaction-start" || event === "interaction-end" || event === "camera-start" || event === "camera-end") && !this._needsFrameListener()) {
|
|
418
|
+
this._detachLazy("frame");
|
|
434
419
|
}
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
this._stopLoafObserver();
|
|
438
|
-
}
|
|
420
|
+
if ((event === "interaction-end" || event === "camera-end") && !this._needsLoafObserver()) {
|
|
421
|
+
this._detachLazy("loaf");
|
|
439
422
|
}
|
|
440
|
-
if (
|
|
441
|
-
this.
|
|
442
|
-
this.shapeCreatedCleanup = null;
|
|
423
|
+
if (event === "shapes-created" && this.emitter.listenerCount("shapes-created") === 0) {
|
|
424
|
+
this._detachLazy("shapes-created");
|
|
443
425
|
}
|
|
444
|
-
if (
|
|
445
|
-
this.
|
|
446
|
-
this.shapeEditedCleanup = null;
|
|
426
|
+
if (event === "shapes-updated" && this.emitter.listenerCount("shapes-updated") === 0) {
|
|
427
|
+
this._detachLazy("shapes-updated");
|
|
447
428
|
}
|
|
448
|
-
if (
|
|
449
|
-
this.
|
|
450
|
-
this.shapeDeletedCleanup = null;
|
|
429
|
+
if (event === "shapes-deleted" && this.emitter.listenerCount("shapes-deleted") === 0) {
|
|
430
|
+
this._detachLazy("shapes-deleted");
|
|
451
431
|
}
|
|
452
432
|
}
|
|
433
|
+
_detachLazy(key) {
|
|
434
|
+
const cleanup = this.lazyCleanups.get(key);
|
|
435
|
+
if (!cleanup) return;
|
|
436
|
+
this.unregister(cleanup);
|
|
437
|
+
this.lazyCleanups.delete(key);
|
|
438
|
+
}
|
|
453
439
|
}
|
|
454
440
|
__decorateClass([
|
|
455
441
|
import_utils.bind
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/lib/editor/managers/PerformanceManager/PerformanceManager.ts"],
|
|
4
|
-
"sourcesContent": ["import type { TLRecord, TLShapeId } from '@tldraw/tlschema'\nimport { bind } from '@tldraw/utils'\nimport EventEmitter from 'eventemitter3'\nimport type { Editor } from '../../Editor'\nimport type {\n\tTLCameraEndPerfEvent,\n\tTLCameraStartPerfEvent,\n\tTLFramePerfEvent,\n\tTLInteractionEndPerfEvent,\n\tTLInteractionStartPerfEvent,\n\tTLPerfEventMap,\n\tTLPerfLongAnimationFrame,\n\tTLShapeOperationPerfEvent,\n\tTLUndoRedoPerfEvent,\n} from './perf-types'\n\nfunction percentile(sorted: number[], p: number): number {\n\tconst idx = Math.ceil(p * sorted.length) - 1\n\treturn sorted[Math.max(0, idx)]\n}\n\nfunction computeFrameTimeStats(frameTimes: number[]) {\n\tif (frameTimes.length === 0) return { avg: 0, median: 0, p95: 0, p99: 0, min: 0, max: 0 }\n\tconst sorted = [...frameTimes].sort((a, b) => a - b)\n\tconst sum = sorted.reduce((a, b) => a + b, 0)\n\treturn {\n\t\tavg: sum / sorted.length,\n\t\tmedian: percentile(sorted, 0.5),\n\t\tp95: percentile(sorted, 0.95),\n\t\tp99: percentile(sorted, 0.99),\n\t\tmin: sorted[0],\n\t\tmax: sorted[sorted.length - 1],\n\t}\n}\n\nfunction toLoafEntry(entry: PerformanceEntry): TLPerfLongAnimationFrame | null {\n\t// LoAF entries have these properties but TypeScript doesn't know about them yet\n\tconst e = entry as PerformanceEntry & {\n\t\tblockingDuration?: number\n\t\tscripts?: ReadonlyArray<{\n\t\t\tsourceURL?: string\n\t\t\tinvoker?: string\n\t\t\tduration?: number\n\t\t}>\n\t}\n\tif (typeof e.duration !== 'number') return null\n\treturn {\n\t\tstartTime: e.startTime,\n\t\tduration: e.duration,\n\t\tblockingDuration: e.blockingDuration ?? 0,\n\t\tscripts: (e.scripts ?? []).map((s) => ({\n\t\t\tsourceURL: s.sourceURL ?? '',\n\t\t\tinvoker: s.invoker ?? '',\n\t\t\tduration: s.duration ?? 0,\n\t\t})),\n\t}\n}\n\n/**\n * Manages performance event subscriptions for the editor. Available as `editor.performance`.\n *\n * Listeners are lazy \u2014 internal editor hooks (frame, shape events) are only attached while\n * at least one subscriber exists, so there is zero overhead when unused.\n *\n * @example\n * ```ts\n * const unsub = editor.performance.on('interaction-end', (event) => {\n * console.log(`${event.name}: ${event.fps.toFixed(1)} fps, p95=${event.p95FrameTime.toFixed(1)}ms`)\n * })\n * ```\n *\n * @public\n */\nexport class PerformanceManager {\n\t/** @internal */\n\treadonly emitter = new EventEmitter<TLPerfEventMap>()\n\n\tprivate editor: Editor\n\n\t// Active interaction tracking\n\tprivate activeInteraction: {\n\t\tname: string\n\t\tpath: string\n\t\tstartTime: number\n\t\tframeTimes: number[]\n\t\tselectedShapeTypes: Record<string, number>\n\t\tloafEntries: TLPerfLongAnimationFrame[]\n\t} | null = null\n\n\t// Active camera tracking\n\tprivate activeCamera: {\n\t\ttype: 'panning' | 'zooming'\n\t\tstartTime: number\n\t\tframeTimes: number[]\n\t\ttimeout: number | null\n\t\tloafEntries: TLPerfLongAnimationFrame[]\n\t} | null = null\n\n\t// Lazy listener cleanup functions\n\tprivate frameCleanup: (() => void) | null = null\n\tprivate shapeCreatedCleanup: (() => void) | null = null\n\tprivate shapeEditedCleanup: (() => void) | null = null\n\tprivate shapeDeletedCleanup: (() => void) | null = null\n\n\t// LoAF observer\n\tprivate loafObserver: PerformanceObserver | null = null\n\n\tconstructor(editor: Editor) {\n\t\tthis.editor = editor\n\t}\n\n\t/**\n\t * Subscribe to a performance event. Returns an unsubscribe function.\n\t *\n\t * @example\n\t * ```ts\n\t * const unsub = editor.performance.on('interaction-end', (event) => {\n\t * sendToAnalytics({ name: event.name, fps: event.fps, p95: event.p95FrameTime })\n\t * })\n\t * // later: unsub()\n\t * ```\n\t *\n\t * @public\n\t */\n\ton<K extends keyof TLPerfEventMap>(\n\t\tevent: K,\n\t\tfn: (...args: TLPerfEventMap[K]) => void\n\t): () => void {\n\t\tthis.emitter.on(event, fn as any)\n\t\tthis._maybeAttachLazyListeners(event)\n\t\treturn () => {\n\t\t\tthis.emitter.off(event, fn as any)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to a performance event once. The listener is removed after the first invocation.\n\t * Returns an unsubscribe function for early removal.\n\t *\n\t * @public\n\t */\n\tonce<K extends keyof TLPerfEventMap>(\n\t\tevent: K,\n\t\tfn: (...args: TLPerfEventMap[K]) => void\n\t): () => void {\n\t\tconst wrapped = (...args: TLPerfEventMap[K]) => {\n\t\t\t;(fn as any)(...args)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t\tthis.emitter.once(event, wrapped as any)\n\t\tthis._maybeAttachLazyListeners(event)\n\t\treturn () => {\n\t\t\tthis.emitter.off(event, wrapped as any)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t}\n\n\t/** @internal */\n\tdispose() {\n\t\tif (this.activeCamera?.timeout) this.editor.timers.clearTimeout(this.activeCamera.timeout)\n\t\tthis.activeInteraction = null\n\t\tthis.activeCamera = null\n\t\tthis.frameCleanup?.()\n\t\tthis.frameCleanup = null\n\t\tthis.shapeCreatedCleanup?.()\n\t\tthis.shapeCreatedCleanup = null\n\t\tthis.shapeEditedCleanup?.()\n\t\tthis.shapeEditedCleanup = null\n\t\tthis.shapeDeletedCleanup?.()\n\t\tthis.shapeDeletedCleanup = null\n\t\tthis._stopLoafObserver()\n\t\tthis.emitter.removeAllListeners()\n\t}\n\n\t// --- Internal notification methods ---\n\n\t/** @internal */\n\t_notifyInteractionStart(name: string, path: string) {\n\t\tif (\n\t\t\tthis.emitter.listenerCount('interaction-start') === 0 &&\n\t\t\tthis.emitter.listenerCount('interaction-end') === 0\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tif (this.activeInteraction) {\n\t\t\tconsole.warn(\n\t\t\t\t`[tldraw] New interaction '${name}' started while '${this.activeInteraction.name}' was still active`\n\t\t\t)\n\t\t}\n\n\t\t// Capture selected shape types at start\n\t\tconst selectedShapeTypes: Record<string, number> = {}\n\t\tfor (const shape of this.editor.getSelectedShapes()) {\n\t\t\tselectedShapeTypes[shape.type] = (selectedShapeTypes[shape.type] || 0) + 1\n\t\t}\n\n\t\tthis.activeInteraction = {\n\t\t\tname,\n\t\t\tpath,\n\t\t\tstartTime: performance.now(),\n\t\t\tframeTimes: [],\n\t\t\tselectedShapeTypes,\n\t\t\tloafEntries: [],\n\t\t}\n\n\t\tconst event: TLInteractionStartPerfEvent = {\n\t\t\tname,\n\t\t\tpath,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('interaction-start', event)\n\t}\n\n\t/** @internal */\n\t_notifyInteractionEnd() {\n\t\tconst interaction = this.activeInteraction\n\t\tif (!interaction) return\n\t\tthis.activeInteraction = null\n\n\t\tif (this.emitter.listenerCount('interaction-end') === 0) return\n\n\t\tconst duration = performance.now() - interaction.startTime\n\t\tconst stats = computeFrameTimeStats(interaction.frameTimes)\n\n\t\tconst event: TLInteractionEndPerfEvent = {\n\t\t\tname: interaction.name,\n\t\t\tpath: interaction.path,\n\t\t\tduration,\n\t\t\tfps:\n\t\t\t\tinteraction.frameTimes.length > 0 ? (interaction.frameTimes.length / duration) * 1000 : 0,\n\t\t\tframeCount: interaction.frameTimes.length,\n\t\t\tavgFrameTime: stats.avg,\n\t\t\tmedianFrameTime: stats.median,\n\t\t\tp95FrameTime: stats.p95,\n\t\t\tp99FrameTime: stats.p99,\n\t\t\tminFrameTime: stats.min,\n\t\t\tmaxFrameTime: stats.max,\n\t\t\tframeTimes: interaction.frameTimes,\n\t\t\tshapeCount: this.editor.getCurrentPageShapeIds().size,\n\t\t\tselectedShapeTypes: interaction.selectedShapeTypes,\n\t\t\tlongAnimationFrames: interaction.loafEntries.length > 0 ? interaction.loafEntries : undefined,\n\t\t\tzoomLevel: this.editor.getCamera().z,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('interaction-end', event)\n\t}\n\n\t/** @internal */\n\t_notifyCameraOperation(type: 'panning' | 'zooming') {\n\t\tif (\n\t\t\tthis.emitter.listenerCount('camera-start') === 0 &&\n\t\t\tthis.emitter.listenerCount('camera-end') === 0\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tif (this.activeCamera) {\n\t\t\t// Extend existing camera session\n\t\t\tif (this.activeCamera.timeout) {\n\t\t\t\tthis.editor.timers.clearTimeout(this.activeCamera.timeout)\n\t\t\t}\n\t\t\t// If type changed, end old and start new\n\t\t\tif (this.activeCamera.type !== type) {\n\t\t\t\tthis._endCameraSession()\n\t\t\t\tthis._startCameraSession(type)\n\t\t\t} else {\n\t\t\t\t// Reset timeout\n\t\t\t\tthis.activeCamera.timeout = this.editor.timers.setTimeout(\n\t\t\t\t\t() => this._endCameraSession(),\n\t\t\t\t\t50\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tthis._startCameraSession(type)\n\t\t}\n\t}\n\n\t/** @internal */\n\t_notifyUndoRedo(type: 'undo' | 'redo', undoDepth: number, redoDepth: number) {\n\t\tif (this.emitter.listenerCount(type) === 0) return\n\n\t\tconst event: TLUndoRedoPerfEvent = {\n\t\t\ttype,\n\t\t\tundoDepth,\n\t\t\tredoDepth,\n\t\t}\n\t\tthis.emitter.emit(type, event)\n\t}\n\n\t// --- Private helpers ---\n\n\tprivate _startCameraSession(type: 'panning' | 'zooming') {\n\t\tthis.activeCamera = {\n\t\t\ttype,\n\t\t\tstartTime: performance.now(),\n\t\t\tframeTimes: [],\n\t\t\ttimeout: this.editor.timers.setTimeout(() => this._endCameraSession(), 50),\n\t\t\tloafEntries: [],\n\t\t}\n\n\t\tif (this.emitter.listenerCount('camera-start') > 0) {\n\t\t\tconst event: TLCameraStartPerfEvent = {\n\t\t\t\ttype,\n\t\t\t\ttimestamp: performance.now(),\n\t\t\t}\n\t\t\tthis.emitter.emit('camera-start', event)\n\t\t}\n\t}\n\n\tprivate _endCameraSession() {\n\t\tconst camera = this.activeCamera\n\t\tif (!camera) return\n\t\tthis.activeCamera = null\n\t\tif (camera.timeout) this.editor.timers.clearTimeout(camera.timeout)\n\n\t\tif (this.emitter.listenerCount('camera-end') === 0) return\n\n\t\tconst duration = performance.now() - camera.startTime\n\t\tconst stats = computeFrameTimeStats(camera.frameTimes)\n\t\tconst viewportBounds = this.editor.getViewportScreenBounds()\n\t\tconst totalShapes = this.editor.getCurrentPageShapeIds().size\n\t\tconst culledShapeCount = this.editor.getCulledShapes().size\n\n\t\tconst event: TLCameraEndPerfEvent = {\n\t\t\ttype: camera.type,\n\t\t\tduration,\n\t\t\tfps: camera.frameTimes.length > 0 ? (camera.frameTimes.length / duration) * 1000 : 0,\n\t\t\tframeCount: camera.frameTimes.length,\n\t\t\tavgFrameTime: stats.avg,\n\t\t\tmedianFrameTime: stats.median,\n\t\t\tp95FrameTime: stats.p95,\n\t\t\tp99FrameTime: stats.p99,\n\t\t\tminFrameTime: stats.min,\n\t\t\tmaxFrameTime: stats.max,\n\t\t\tframeTimes: camera.frameTimes,\n\t\t\tshapeCount: totalShapes,\n\t\t\tviewportWidth: viewportBounds.w,\n\t\t\tviewportHeight: viewportBounds.h,\n\t\t\tlongAnimationFrames: camera.loafEntries.length > 0 ? camera.loafEntries : undefined,\n\t\t\tvisibleShapeCount: totalShapes - culledShapeCount,\n\t\t\tculledShapeCount,\n\t\t\tzoomLevel: this.editor.getCamera().z,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('camera-end', event)\n\t}\n\n\t@bind\n\tprivate _onFrame(elapsed: number) {\n\t\t// Record frame time for active interaction/camera\n\t\tif (this.activeInteraction) {\n\t\t\tthis.activeInteraction.frameTimes.push(elapsed)\n\t\t}\n\t\tif (this.activeCamera) {\n\t\t\tthis.activeCamera.frameTimes.push(elapsed)\n\t\t}\n\n\t\t// Emit standalone frame event if listeners exist\n\t\tif (this.emitter.listenerCount('frame') > 0) {\n\t\t\tconst totalShapes = this.editor.getCurrentPageShapeIds().size\n\t\t\tconst culledShapes = this.editor.getCulledShapes()\n\t\t\tconst culledCount = culledShapes.size\n\t\t\tconst event: TLFramePerfEvent = {\n\t\t\t\telapsed,\n\t\t\t\tshapeCount: totalShapes,\n\t\t\t\tculledShapeCount: culledCount,\n\t\t\t\tvisibleShapeCount: totalShapes - culledCount,\n\t\t\t}\n\t\t\tthis.emitter.emit('frame', event)\n\t\t}\n\t}\n\n\t@bind\n\tprivate _onShapesCreated(records: TLRecord[]) {\n\t\tif (this.emitter.listenerCount('shapes-created') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const record of records) {\n\t\t\tif (record.typeName === 'shape') {\n\t\t\t\tshapeTypes[record.type] = (shapeTypes[record.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst count = Object.values(shapeTypes).reduce((a, b) => a + b, 0)\n\t\tif (count === 0) return\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'create',\n\t\t\tcount,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-created', event)\n\t}\n\n\t@bind\n\tprivate _onShapesEdited(records: TLRecord[]) {\n\t\tif (this.emitter.listenerCount('shapes-updated') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const record of records) {\n\t\t\tif (record.typeName === 'shape') {\n\t\t\t\tshapeTypes[record.type] = (shapeTypes[record.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst count = Object.values(shapeTypes).reduce((a, b) => a + b, 0)\n\t\tif (count === 0) return\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'update',\n\t\t\tcount,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-updated', event)\n\t}\n\n\t@bind\n\tprivate _onShapesDeleted(ids: TLShapeId[]) {\n\t\tif (this.emitter.listenerCount('shapes-deleted') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const id of ids) {\n\t\t\t// Works because 'deleted-shapes' fires before store.remove() in Editor.deleteShapes\n\t\t\tconst shape = this.editor.getShape(id)\n\t\t\tif (shape) {\n\t\t\t\tshapeTypes[shape.type] = (shapeTypes[shape.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'delete',\n\t\t\tcount: ids.length,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-deleted', event)\n\t}\n\n\t// --- LoAF observer ---\n\n\tprivate _startLoafObserver() {\n\t\tif (typeof PerformanceObserver === 'undefined') return\n\n\t\ttry {\n\t\t\tconst supported = PerformanceObserver.supportedEntryTypes\n\t\t\tif (!supported?.includes('long-animation-frame')) return\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\n\t\tthis.loafObserver = new PerformanceObserver((list) => {\n\t\t\tconst isInteractionActive = this.activeInteraction !== null\n\t\t\tconst isCameraActive = this.activeCamera !== null\n\n\t\t\tif (!isInteractionActive && !isCameraActive) return\n\n\t\t\tfor (const entry of list.getEntries()) {\n\t\t\t\tconst loaf = toLoafEntry(entry)\n\t\t\t\tif (!loaf) continue\n\n\t\t\t\tif (isInteractionActive) {\n\t\t\t\t\tthis.activeInteraction!.loafEntries.push(loaf)\n\t\t\t\t}\n\t\t\t\tif (isCameraActive) {\n\t\t\t\t\tthis.activeCamera!.loafEntries.push(loaf)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tthis.loafObserver.observe({ type: 'long-animation-frame', buffered: false })\n\t}\n\n\tprivate _stopLoafObserver() {\n\t\tif (this.loafObserver) {\n\t\t\tthis.loafObserver.disconnect()\n\t\t\tthis.loafObserver = null\n\t\t}\n\t}\n\n\t// --- Lazy listener management ---\n\n\tprivate _needsFrameListener(): boolean {\n\t\treturn (\n\t\t\tthis.emitter.listenerCount('frame') > 0 ||\n\t\t\tthis.emitter.listenerCount('interaction-start') > 0 ||\n\t\t\tthis.emitter.listenerCount('interaction-end') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-start') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-end') > 0\n\t\t)\n\t}\n\n\tprivate _needsLoafObserver(): boolean {\n\t\treturn (\n\t\t\tthis.emitter.listenerCount('interaction-end') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-end') > 0\n\t\t)\n\t}\n\n\tprivate _maybeAttachLazyListeners(event: keyof TLPerfEventMap) {\n\t\t// Frame listener needed for frame event + interaction/camera frame time tracking\n\t\tif (\n\t\t\t!this.frameCleanup &&\n\t\t\t(event === 'frame' ||\n\t\t\t\tevent === 'interaction-start' ||\n\t\t\t\tevent === 'interaction-end' ||\n\t\t\t\tevent === 'camera-start' ||\n\t\t\t\tevent === 'camera-end')\n\t\t) {\n\t\t\tif (this._needsFrameListener()) {\n\t\t\t\tthis.editor.on('frame', this._onFrame)\n\t\t\t\tthis.frameCleanup = () => this.editor.off('frame', this._onFrame)\n\t\t\t}\n\t\t}\n\n\t\t// LoAF observer needed when interaction-end or camera-end listeners exist\n\t\tif (!this.loafObserver && (event === 'interaction-end' || event === 'camera-end')) {\n\t\t\tif (this._needsLoafObserver()) {\n\t\t\t\tthis._startLoafObserver()\n\t\t\t}\n\t\t}\n\n\t\tif (!this.shapeCreatedCleanup && event === 'shapes-created') {\n\t\t\tthis.editor.on('created-shapes', this._onShapesCreated)\n\t\t\tthis.shapeCreatedCleanup = () => this.editor.off('created-shapes', this._onShapesCreated)\n\t\t}\n\n\t\tif (!this.shapeEditedCleanup && event === 'shapes-updated') {\n\t\t\tthis.editor.on('edited-shapes', this._onShapesEdited)\n\t\t\tthis.shapeEditedCleanup = () => this.editor.off('edited-shapes', this._onShapesEdited)\n\t\t}\n\n\t\tif (!this.shapeDeletedCleanup && event === 'shapes-deleted') {\n\t\t\tthis.editor.on('deleted-shapes', this._onShapesDeleted)\n\t\t\tthis.shapeDeletedCleanup = () => this.editor.off('deleted-shapes', this._onShapesDeleted)\n\t\t}\n\t}\n\n\tprivate _maybeDetachLazyListeners(event: keyof TLPerfEventMap) {\n\t\tif (\n\t\t\tthis.frameCleanup &&\n\t\t\t(event === 'frame' ||\n\t\t\t\tevent === 'interaction-start' ||\n\t\t\t\tevent === 'interaction-end' ||\n\t\t\t\tevent === 'camera-start' ||\n\t\t\t\tevent === 'camera-end')\n\t\t) {\n\t\t\tif (!this._needsFrameListener()) {\n\t\t\t\tthis.frameCleanup()\n\t\t\t\tthis.frameCleanup = null\n\t\t\t}\n\t\t}\n\n\t\t// Stop LoAF observer when no longer needed\n\t\tif (this.loafObserver && (event === 'interaction-end' || event === 'camera-end')) {\n\t\t\tif (!this._needsLoafObserver()) {\n\t\t\t\tthis._stopLoafObserver()\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\tthis.shapeCreatedCleanup &&\n\t\t\tevent === 'shapes-created' &&\n\t\t\tthis.emitter.listenerCount('shapes-created') === 0\n\t\t) {\n\t\t\tthis.shapeCreatedCleanup()\n\t\t\tthis.shapeCreatedCleanup = null\n\t\t}\n\n\t\tif (\n\t\t\tthis.shapeEditedCleanup &&\n\t\t\tevent === 'shapes-updated' &&\n\t\t\tthis.emitter.listenerCount('shapes-updated') === 0\n\t\t) {\n\t\t\tthis.shapeEditedCleanup()\n\t\t\tthis.shapeEditedCleanup = null\n\t\t}\n\n\t\tif (\n\t\t\tthis.shapeDeletedCleanup &&\n\t\t\tevent === 'shapes-deleted' &&\n\t\t\tthis.emitter.listenerCount('shapes-deleted') === 0\n\t\t) {\n\t\t\tthis.shapeDeletedCleanup()\n\t\t\tthis.shapeDeletedCleanup = null\n\t\t}\n\t}\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,mBAAqB;AACrB,2BAAyB;
|
|
4
|
+
"sourcesContent": ["import type { TLRecord, TLShapeId } from '@tldraw/tlschema'\nimport { bind } from '@tldraw/utils'\nimport EventEmitter from 'eventemitter3'\nimport { EditorManager } from '../EditorManager'\nimport type {\n\tTLCameraEndPerfEvent,\n\tTLCameraStartPerfEvent,\n\tTLFramePerfEvent,\n\tTLInteractionEndPerfEvent,\n\tTLInteractionStartPerfEvent,\n\tTLPerfEventMap,\n\tTLPerfLongAnimationFrame,\n\tTLShapeOperationPerfEvent,\n\tTLUndoRedoPerfEvent,\n} from './perf-types'\n\n// The editor listeners and observer that are attached only while a subscriber needs them\ntype LazyListener = 'frame' | 'loaf' | 'shapes-created' | 'shapes-updated' | 'shapes-deleted'\n\nfunction percentile(sorted: number[], p: number): number {\n\tconst idx = Math.ceil(p * sorted.length) - 1\n\treturn sorted[Math.max(0, idx)]\n}\n\nfunction computeFrameTimeStats(frameTimes: number[]) {\n\tif (frameTimes.length === 0) return { avg: 0, median: 0, p95: 0, p99: 0, min: 0, max: 0 }\n\tconst sorted = [...frameTimes].sort((a, b) => a - b)\n\tconst sum = sorted.reduce((a, b) => a + b, 0)\n\treturn {\n\t\tavg: sum / sorted.length,\n\t\tmedian: percentile(sorted, 0.5),\n\t\tp95: percentile(sorted, 0.95),\n\t\tp99: percentile(sorted, 0.99),\n\t\tmin: sorted[0],\n\t\tmax: sorted[sorted.length - 1],\n\t}\n}\n\nfunction toLoafEntry(entry: PerformanceEntry): TLPerfLongAnimationFrame | null {\n\t// LoAF entries have these properties but TypeScript doesn't know about them yet\n\tconst e = entry as PerformanceEntry & {\n\t\tblockingDuration?: number\n\t\tscripts?: ReadonlyArray<{\n\t\t\tsourceURL?: string\n\t\t\tinvoker?: string\n\t\t\tduration?: number\n\t\t}>\n\t}\n\tif (typeof e.duration !== 'number') return null\n\treturn {\n\t\tstartTime: e.startTime,\n\t\tduration: e.duration,\n\t\tblockingDuration: e.blockingDuration ?? 0,\n\t\tscripts: (e.scripts ?? []).map((s) => ({\n\t\t\tsourceURL: s.sourceURL ?? '',\n\t\t\tinvoker: s.invoker ?? '',\n\t\t\tduration: s.duration ?? 0,\n\t\t})),\n\t}\n}\n\n/**\n * Manages performance event subscriptions for the editor. Available as `editor.performance`.\n *\n * Listeners are lazy \u2014 internal editor hooks (frame, shape events) are only attached while\n * at least one subscriber exists, so there is zero overhead when unused.\n *\n * @example\n * ```ts\n * const unsub = editor.performance.on('interaction-end', (event) => {\n * console.log(`${event.name}: ${event.fps.toFixed(1)} fps, p95=${event.p95FrameTime.toFixed(1)}ms`)\n * })\n * ```\n *\n * @public\n */\nexport class PerformanceManager extends EditorManager {\n\t/** @internal */\n\treadonly emitter = new EventEmitter<TLPerfEventMap>()\n\n\t// Active interaction tracking\n\tprivate activeInteraction: {\n\t\tname: string\n\t\tpath: string\n\t\tstartTime: number\n\t\tframeTimes: number[]\n\t\tselectedShapeTypes: Record<string, number>\n\t\tloafEntries: TLPerfLongAnimationFrame[]\n\t} | null = null\n\n\t// Active camera tracking\n\tprivate activeCamera: {\n\t\ttype: 'panning' | 'zooming'\n\t\tstartTime: number\n\t\tframeTimes: number[]\n\t\ttimeout: number | null\n\t\tloafEntries: TLPerfLongAnimationFrame[]\n\t} | null = null\n\n\t// Cleanups for whatever is currently attached, keyed so a detach unregisters only its own\n\tprivate readonly lazyCleanups = new Map<LazyListener, () => void>()\n\n\t/**\n\t * Subscribe to a performance event. Returns an unsubscribe function.\n\t *\n\t * @example\n\t * ```ts\n\t * const unsub = editor.performance.on('interaction-end', (event) => {\n\t * sendToAnalytics({ name: event.name, fps: event.fps, p95: event.p95FrameTime })\n\t * })\n\t * // later: unsub()\n\t * ```\n\t *\n\t * @public\n\t */\n\ton<K extends keyof TLPerfEventMap>(\n\t\tevent: K,\n\t\tfn: (...args: TLPerfEventMap[K]) => void\n\t): () => void {\n\t\tthis.emitter.on(event, fn as any)\n\t\tthis._maybeAttachLazyListeners(event)\n\t\treturn () => {\n\t\t\tthis.emitter.off(event, fn as any)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to a performance event once. The listener is removed after the first invocation.\n\t * Returns an unsubscribe function for early removal.\n\t *\n\t * @public\n\t */\n\tonce<K extends keyof TLPerfEventMap>(\n\t\tevent: K,\n\t\tfn: (...args: TLPerfEventMap[K]) => void\n\t): () => void {\n\t\tconst wrapped = (...args: TLPerfEventMap[K]) => {\n\t\t\t;(fn as any)(...args)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t\tthis.emitter.once(event, wrapped as any)\n\t\tthis._maybeAttachLazyListeners(event)\n\t\treturn () => {\n\t\t\tthis.emitter.off(event, wrapped as any)\n\t\t\tthis._maybeDetachLazyListeners(event)\n\t\t}\n\t}\n\n\t/** @internal */\n\tdispose() {\n\t\tif (this.activeCamera?.timeout) this.editor.timers.clearTimeout(this.activeCamera.timeout)\n\t\tthis.activeInteraction = null\n\t\tthis.activeCamera = null\n\t\tthis.lazyCleanups.clear()\n\t\tthis.emitter.removeAllListeners()\n\t\tsuper.dispose()\n\t}\n\n\t// --- Internal notification methods ---\n\n\t/** @internal */\n\t_notifyInteractionStart(name: string, path: string) {\n\t\tif (\n\t\t\tthis.emitter.listenerCount('interaction-start') === 0 &&\n\t\t\tthis.emitter.listenerCount('interaction-end') === 0\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tif (this.activeInteraction) {\n\t\t\tconsole.warn(\n\t\t\t\t`[tldraw] New interaction '${name}' started while '${this.activeInteraction.name}' was still active`\n\t\t\t)\n\t\t}\n\n\t\t// Capture selected shape types at start\n\t\tconst selectedShapeTypes: Record<string, number> = {}\n\t\tfor (const shape of this.editor.getSelectedShapes()) {\n\t\t\tselectedShapeTypes[shape.type] = (selectedShapeTypes[shape.type] || 0) + 1\n\t\t}\n\n\t\tthis.activeInteraction = {\n\t\t\tname,\n\t\t\tpath,\n\t\t\tstartTime: performance.now(),\n\t\t\tframeTimes: [],\n\t\t\tselectedShapeTypes,\n\t\t\tloafEntries: [],\n\t\t}\n\n\t\tconst event: TLInteractionStartPerfEvent = {\n\t\t\tname,\n\t\t\tpath,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('interaction-start', event)\n\t}\n\n\t/** @internal */\n\t_notifyInteractionEnd() {\n\t\tconst interaction = this.activeInteraction\n\t\tif (!interaction) return\n\t\tthis.activeInteraction = null\n\n\t\tif (this.emitter.listenerCount('interaction-end') === 0) return\n\n\t\tconst duration = performance.now() - interaction.startTime\n\t\tconst stats = computeFrameTimeStats(interaction.frameTimes)\n\n\t\tconst event: TLInteractionEndPerfEvent = {\n\t\t\tname: interaction.name,\n\t\t\tpath: interaction.path,\n\t\t\tduration,\n\t\t\tfps:\n\t\t\t\tinteraction.frameTimes.length > 0 ? (interaction.frameTimes.length / duration) * 1000 : 0,\n\t\t\tframeCount: interaction.frameTimes.length,\n\t\t\tavgFrameTime: stats.avg,\n\t\t\tmedianFrameTime: stats.median,\n\t\t\tp95FrameTime: stats.p95,\n\t\t\tp99FrameTime: stats.p99,\n\t\t\tminFrameTime: stats.min,\n\t\t\tmaxFrameTime: stats.max,\n\t\t\tframeTimes: interaction.frameTimes,\n\t\t\tshapeCount: this.editor.getCurrentPageShapeIds().size,\n\t\t\tselectedShapeTypes: interaction.selectedShapeTypes,\n\t\t\tlongAnimationFrames: interaction.loafEntries.length > 0 ? interaction.loafEntries : undefined,\n\t\t\tzoomLevel: this.editor.getCamera().z,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('interaction-end', event)\n\t}\n\n\t/** @internal */\n\t_notifyCameraOperation(type: 'panning' | 'zooming') {\n\t\tif (\n\t\t\tthis.emitter.listenerCount('camera-start') === 0 &&\n\t\t\tthis.emitter.listenerCount('camera-end') === 0\n\t\t) {\n\t\t\treturn\n\t\t}\n\n\t\tif (this.activeCamera) {\n\t\t\t// Extend existing camera session\n\t\t\tif (this.activeCamera.timeout) {\n\t\t\t\tthis.editor.timers.clearTimeout(this.activeCamera.timeout)\n\t\t\t}\n\t\t\t// If type changed, end old and start new\n\t\t\tif (this.activeCamera.type !== type) {\n\t\t\t\tthis._endCameraSession()\n\t\t\t\tthis._startCameraSession(type)\n\t\t\t} else {\n\t\t\t\t// Reset timeout\n\t\t\t\tthis.activeCamera.timeout = this.editor.timers.setTimeout(\n\t\t\t\t\t() => this._endCameraSession(),\n\t\t\t\t\t50\n\t\t\t\t)\n\t\t\t}\n\t\t} else {\n\t\t\tthis._startCameraSession(type)\n\t\t}\n\t}\n\n\t/** @internal */\n\t_notifyUndoRedo(type: 'undo' | 'redo', undoDepth: number, redoDepth: number) {\n\t\tif (this.emitter.listenerCount(type) === 0) return\n\n\t\tconst event: TLUndoRedoPerfEvent = {\n\t\t\ttype,\n\t\t\tundoDepth,\n\t\t\tredoDepth,\n\t\t}\n\t\tthis.emitter.emit(type, event)\n\t}\n\n\t// --- Private helpers ---\n\n\tprivate _startCameraSession(type: 'panning' | 'zooming') {\n\t\tthis.activeCamera = {\n\t\t\ttype,\n\t\t\tstartTime: performance.now(),\n\t\t\tframeTimes: [],\n\t\t\ttimeout: this.editor.timers.setTimeout(() => this._endCameraSession(), 50),\n\t\t\tloafEntries: [],\n\t\t}\n\n\t\tif (this.emitter.listenerCount('camera-start') > 0) {\n\t\t\tconst event: TLCameraStartPerfEvent = {\n\t\t\t\ttype,\n\t\t\t\ttimestamp: performance.now(),\n\t\t\t}\n\t\t\tthis.emitter.emit('camera-start', event)\n\t\t}\n\t}\n\n\tprivate _endCameraSession() {\n\t\tconst camera = this.activeCamera\n\t\tif (!camera) return\n\t\tthis.activeCamera = null\n\t\tif (camera.timeout) this.editor.timers.clearTimeout(camera.timeout)\n\n\t\tif (this.emitter.listenerCount('camera-end') === 0) return\n\n\t\tconst duration = performance.now() - camera.startTime\n\t\tconst stats = computeFrameTimeStats(camera.frameTimes)\n\t\tconst viewportBounds = this.editor.getViewportScreenBounds()\n\t\tconst totalShapes = this.editor.getCurrentPageShapeIds().size\n\t\tconst culledShapeCount = this.editor.getCulledShapes().size\n\n\t\tconst event: TLCameraEndPerfEvent = {\n\t\t\ttype: camera.type,\n\t\t\tduration,\n\t\t\tfps: camera.frameTimes.length > 0 ? (camera.frameTimes.length / duration) * 1000 : 0,\n\t\t\tframeCount: camera.frameTimes.length,\n\t\t\tavgFrameTime: stats.avg,\n\t\t\tmedianFrameTime: stats.median,\n\t\t\tp95FrameTime: stats.p95,\n\t\t\tp99FrameTime: stats.p99,\n\t\t\tminFrameTime: stats.min,\n\t\t\tmaxFrameTime: stats.max,\n\t\t\tframeTimes: camera.frameTimes,\n\t\t\tshapeCount: totalShapes,\n\t\t\tviewportWidth: viewportBounds.w,\n\t\t\tviewportHeight: viewportBounds.h,\n\t\t\tlongAnimationFrames: camera.loafEntries.length > 0 ? camera.loafEntries : undefined,\n\t\t\tvisibleShapeCount: totalShapes - culledShapeCount,\n\t\t\tculledShapeCount,\n\t\t\tzoomLevel: this.editor.getCamera().z,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('camera-end', event)\n\t}\n\n\t@bind\n\tprivate _onFrame(elapsed: number) {\n\t\t// Record frame time for active interaction/camera\n\t\tif (this.activeInteraction) {\n\t\t\tthis.activeInteraction.frameTimes.push(elapsed)\n\t\t}\n\t\tif (this.activeCamera) {\n\t\t\tthis.activeCamera.frameTimes.push(elapsed)\n\t\t}\n\n\t\t// Emit standalone frame event if listeners exist\n\t\tif (this.emitter.listenerCount('frame') > 0) {\n\t\t\tconst totalShapes = this.editor.getCurrentPageShapeIds().size\n\t\t\tconst culledShapes = this.editor.getCulledShapes()\n\t\t\tconst culledCount = culledShapes.size\n\t\t\tconst event: TLFramePerfEvent = {\n\t\t\t\telapsed,\n\t\t\t\tshapeCount: totalShapes,\n\t\t\t\tculledShapeCount: culledCount,\n\t\t\t\tvisibleShapeCount: totalShapes - culledCount,\n\t\t\t}\n\t\t\tthis.emitter.emit('frame', event)\n\t\t}\n\t}\n\n\t@bind\n\tprivate _onShapesCreated(records: TLRecord[]) {\n\t\tif (this.emitter.listenerCount('shapes-created') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const record of records) {\n\t\t\tif (record.typeName === 'shape') {\n\t\t\t\tshapeTypes[record.type] = (shapeTypes[record.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst count = Object.values(shapeTypes).reduce((a, b) => a + b, 0)\n\t\tif (count === 0) return\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'create',\n\t\t\tcount,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-created', event)\n\t}\n\n\t@bind\n\tprivate _onShapesEdited(records: TLRecord[]) {\n\t\tif (this.emitter.listenerCount('shapes-updated') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const record of records) {\n\t\t\tif (record.typeName === 'shape') {\n\t\t\t\tshapeTypes[record.type] = (shapeTypes[record.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst count = Object.values(shapeTypes).reduce((a, b) => a + b, 0)\n\t\tif (count === 0) return\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'update',\n\t\t\tcount,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-updated', event)\n\t}\n\n\t@bind\n\tprivate _onShapesDeleted(ids: TLShapeId[]) {\n\t\tif (this.emitter.listenerCount('shapes-deleted') === 0) return\n\t\tconst shapeTypes: Record<string, number> = {}\n\t\tfor (const id of ids) {\n\t\t\t// Works because 'deleted-shapes' fires before store.remove() in Editor.deleteShapes\n\t\t\tconst shape = this.editor.getShape(id)\n\t\t\tif (shape) {\n\t\t\t\tshapeTypes[shape.type] = (shapeTypes[shape.type] || 0) + 1\n\t\t\t}\n\t\t}\n\t\tconst event: TLShapeOperationPerfEvent = {\n\t\t\toperation: 'delete',\n\t\t\tcount: ids.length,\n\t\t\tshapeTypes,\n\t\t\ttimestamp: performance.now(),\n\t\t}\n\t\tthis.emitter.emit('shapes-deleted', event)\n\t}\n\n\t// --- LoAF observer ---\n\n\tprivate _startLoafObserver() {\n\t\tif (typeof PerformanceObserver === 'undefined') return\n\n\t\ttry {\n\t\t\tconst supported = PerformanceObserver.supportedEntryTypes\n\t\t\tif (!supported?.includes('long-animation-frame')) return\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\n\t\tconst observer = new PerformanceObserver((list) => {\n\t\t\tconst isInteractionActive = this.activeInteraction !== null\n\t\t\tconst isCameraActive = this.activeCamera !== null\n\n\t\t\tif (!isInteractionActive && !isCameraActive) return\n\n\t\t\tfor (const entry of list.getEntries()) {\n\t\t\t\tconst loaf = toLoafEntry(entry)\n\t\t\t\tif (!loaf) continue\n\n\t\t\t\tif (isInteractionActive) {\n\t\t\t\t\tthis.activeInteraction!.loafEntries.push(loaf)\n\t\t\t\t}\n\t\t\t\tif (isCameraActive) {\n\t\t\t\t\tthis.activeCamera!.loafEntries.push(loaf)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tobserver.observe({ type: 'long-animation-frame', buffered: false })\n\t\tthis.lazyCleanups.set(\n\t\t\t'loaf',\n\t\t\tthis.register(() => observer.disconnect())\n\t\t)\n\t}\n\n\t// --- Lazy listener management ---\n\n\tprivate _needsFrameListener(): boolean {\n\t\treturn (\n\t\t\tthis.emitter.listenerCount('frame') > 0 ||\n\t\t\tthis.emitter.listenerCount('interaction-start') > 0 ||\n\t\t\tthis.emitter.listenerCount('interaction-end') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-start') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-end') > 0\n\t\t)\n\t}\n\n\tprivate _needsLoafObserver(): boolean {\n\t\treturn (\n\t\t\tthis.emitter.listenerCount('interaction-end') > 0 ||\n\t\t\tthis.emitter.listenerCount('camera-end') > 0\n\t\t)\n\t}\n\n\tprivate _maybeAttachLazyListeners(event: keyof TLPerfEventMap) {\n\t\t// Frame listener needed for frame event + interaction/camera frame time tracking\n\t\tif (\n\t\t\t!this.lazyCleanups.has('frame') &&\n\t\t\t(event === 'frame' ||\n\t\t\t\tevent === 'interaction-start' ||\n\t\t\t\tevent === 'interaction-end' ||\n\t\t\t\tevent === 'camera-start' ||\n\t\t\t\tevent === 'camera-end')\n\t\t) {\n\t\t\tif (this._needsFrameListener()) {\n\t\t\t\tthis.lazyCleanups.set('frame', this.addEditorEvent('frame', this._onFrame))\n\t\t\t}\n\t\t}\n\n\t\t// LoAF observer needed when interaction-end or camera-end listeners exist\n\t\tif (!this.lazyCleanups.has('loaf') && (event === 'interaction-end' || event === 'camera-end')) {\n\t\t\tif (this._needsLoafObserver()) {\n\t\t\t\tthis._startLoafObserver()\n\t\t\t}\n\t\t}\n\n\t\tif (!this.lazyCleanups.has('shapes-created') && event === 'shapes-created') {\n\t\t\tthis.lazyCleanups.set(\n\t\t\t\t'shapes-created',\n\t\t\t\tthis.addEditorEvent('created-shapes', this._onShapesCreated)\n\t\t\t)\n\t\t}\n\n\t\tif (!this.lazyCleanups.has('shapes-updated') && event === 'shapes-updated') {\n\t\t\tthis.lazyCleanups.set(\n\t\t\t\t'shapes-updated',\n\t\t\t\tthis.addEditorEvent('edited-shapes', this._onShapesEdited)\n\t\t\t)\n\t\t}\n\n\t\tif (!this.lazyCleanups.has('shapes-deleted') && event === 'shapes-deleted') {\n\t\t\tthis.lazyCleanups.set(\n\t\t\t\t'shapes-deleted',\n\t\t\t\tthis.addEditorEvent('deleted-shapes', this._onShapesDeleted)\n\t\t\t)\n\t\t}\n\t}\n\n\tprivate _maybeDetachLazyListeners(event: keyof TLPerfEventMap) {\n\t\tif (\n\t\t\t(event === 'frame' ||\n\t\t\t\tevent === 'interaction-start' ||\n\t\t\t\tevent === 'interaction-end' ||\n\t\t\t\tevent === 'camera-start' ||\n\t\t\t\tevent === 'camera-end') &&\n\t\t\t!this._needsFrameListener()\n\t\t) {\n\t\t\tthis._detachLazy('frame')\n\t\t}\n\n\t\t// Stop LoAF observer when no longer needed\n\t\tif ((event === 'interaction-end' || event === 'camera-end') && !this._needsLoafObserver()) {\n\t\t\tthis._detachLazy('loaf')\n\t\t}\n\n\t\tif (event === 'shapes-created' && this.emitter.listenerCount('shapes-created') === 0) {\n\t\t\tthis._detachLazy('shapes-created')\n\t\t}\n\n\t\tif (event === 'shapes-updated' && this.emitter.listenerCount('shapes-updated') === 0) {\n\t\t\tthis._detachLazy('shapes-updated')\n\t\t}\n\n\t\tif (event === 'shapes-deleted' && this.emitter.listenerCount('shapes-deleted') === 0) {\n\t\t\tthis._detachLazy('shapes-deleted')\n\t\t}\n\t}\n\n\tprivate _detachLazy(key: LazyListener) {\n\t\tconst cleanup = this.lazyCleanups.get(key)\n\t\tif (!cleanup) return\n\t\tthis.unregister(cleanup)\n\t\tthis.lazyCleanups.delete(key)\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["EventEmitter"]
|
|
7
7
|
}
|
package/dist-cjs/version.js
CHANGED
|
@@ -22,10 +22,10 @@ __export(version_exports, {
|
|
|
22
22
|
version: () => version
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(version_exports);
|
|
25
|
-
const version = "5.5.0-canary.
|
|
25
|
+
const version = "5.5.0-canary.ddcb7ca958fa";
|
|
26
26
|
const publishDates = {
|
|
27
27
|
major: "2026-05-06T16:28:18.473Z",
|
|
28
|
-
minor: "2026-09-
|
|
29
|
-
patch: "2026-09-
|
|
28
|
+
minor: "2026-09-23T12:53:21.553Z",
|
|
29
|
+
patch: "2026-09-23T12:53:21.553Z"
|
|
30
30
|
};
|
|
31
31
|
//# sourceMappingURL=version.js.map
|
package/dist-cjs/version.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/version.ts"],
|
|
4
|
-
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.5.0-canary.
|
|
4
|
+
"sourcesContent": ["// This file is automatically generated by internal/scripts/refresh-assets.ts.\n// Do not edit manually. Or do, I'm a comment, not a cop.\n\nexport const version = '5.5.0-canary.ddcb7ca958fa'\nexport const publishDates = {\n\tmajor: '2026-05-06T16:28:18.473Z',\n\tminor: '2026-09-23T12:53:21.553Z',\n\tpatch: '2026-09-23T12:53:21.553Z',\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAGO,MAAM,UAAU;AAChB,MAAM,eAAe;AAAA,EAC3B,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AACR;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -4579,6 +4579,8 @@ export declare const EditorContext: React_3.Context<Editor | null>;
|
|
|
4579
4579
|
* - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`
|
|
4580
4580
|
* - DOM listener: `this.register(() => el.removeEventListener(...))`
|
|
4581
4581
|
* - Other resource (cache, index, child manager): `this.register(() => x.dispose())`
|
|
4582
|
+
* - Resource held only part of the time (a listener attached while subscribers exist):
|
|
4583
|
+
* `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`
|
|
4582
4584
|
*
|
|
4583
4585
|
* For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped
|
|
4584
4586
|
* and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a
|
|
@@ -4598,11 +4600,18 @@ export declare abstract class EditorManager {
|
|
|
4598
4600
|
* make a resource "see itself out". Returns the same function for convenience.
|
|
4599
4601
|
*/
|
|
4600
4602
|
protected register(dispose: () => void): () => void;
|
|
4603
|
+
/**
|
|
4604
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
4605
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
4606
|
+
* torn down twice.
|
|
4607
|
+
*/
|
|
4608
|
+
protected unregister(dispose: () => void): void;
|
|
4601
4609
|
/**
|
|
4602
4610
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
4603
|
-
* is removed automatically on `dispose()`.
|
|
4611
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
4612
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
4604
4613
|
*/
|
|
4605
|
-
protected addEditorEvent<E extends keyof TLEventMap>(event: E, fn: (...args: TLEventMap[E]) => void): void;
|
|
4614
|
+
protected addEditorEvent<E extends keyof TLEventMap>(event: E, fn: (...args: TLEventMap[E]) => void): () => void;
|
|
4606
4615
|
/* Excluded from this release type: dispose */
|
|
4607
4616
|
}
|
|
4608
4617
|
|
|
@@ -5990,17 +5999,11 @@ export declare class PerformanceApiAdapter {
|
|
|
5990
5999
|
*
|
|
5991
6000
|
* @public
|
|
5992
6001
|
*/
|
|
5993
|
-
export declare class PerformanceManager {
|
|
6002
|
+
export declare class PerformanceManager extends EditorManager {
|
|
5994
6003
|
/* Excluded from this release type: emitter */
|
|
5995
|
-
private editor;
|
|
5996
6004
|
private activeInteraction;
|
|
5997
6005
|
private activeCamera;
|
|
5998
|
-
private
|
|
5999
|
-
private shapeCreatedCleanup;
|
|
6000
|
-
private shapeEditedCleanup;
|
|
6001
|
-
private shapeDeletedCleanup;
|
|
6002
|
-
private loafObserver;
|
|
6003
|
-
constructor(editor: Editor);
|
|
6006
|
+
private readonly lazyCleanups;
|
|
6004
6007
|
/**
|
|
6005
6008
|
* Subscribe to a performance event. Returns an unsubscribe function.
|
|
6006
6009
|
*
|
|
@@ -6034,11 +6037,11 @@ export declare class PerformanceManager {
|
|
|
6034
6037
|
private _onShapesEdited;
|
|
6035
6038
|
private _onShapesDeleted;
|
|
6036
6039
|
private _startLoafObserver;
|
|
6037
|
-
private _stopLoafObserver;
|
|
6038
6040
|
private _needsFrameListener;
|
|
6039
6041
|
private _needsLoafObserver;
|
|
6040
6042
|
private _maybeAttachLazyListeners;
|
|
6041
6043
|
private _maybeDetachLazyListeners;
|
|
6044
|
+
private _detachLazy;
|
|
6042
6045
|
}
|
|
6043
6046
|
|
|
6044
6047
|
/**
|
package/dist-esm/index.mjs
CHANGED
|
@@ -312,7 +312,7 @@ import { LocalIndexedDb, Table } from "./lib/utils/sync/LocalIndexedDb.mjs";
|
|
|
312
312
|
import { uniq } from "./lib/utils/uniq.mjs";
|
|
313
313
|
registerTldrawLibraryVersion(
|
|
314
314
|
"@tldraw/editor",
|
|
315
|
-
"5.5.0-canary.
|
|
315
|
+
"5.5.0-canary.ddcb7ca958fa",
|
|
316
316
|
"esm"
|
|
317
317
|
);
|
|
318
318
|
import { getColorValue } from "./lib/editor/managers/ThemeManager/defaultThemes.mjs";
|
|
@@ -12,13 +12,22 @@ class EditorManager {
|
|
|
12
12
|
this.disposables.add(dispose);
|
|
13
13
|
return dispose;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Run a registered teardown now instead of at `dispose()`. A teardown that is no longer
|
|
17
|
+
* registered (already run by `dispose()`, or never registered) is skipped, so nothing is
|
|
18
|
+
* torn down twice.
|
|
19
|
+
*/
|
|
20
|
+
unregister(dispose) {
|
|
21
|
+
if (this.disposables.delete(dispose)) dispose();
|
|
22
|
+
}
|
|
15
23
|
/**
|
|
16
24
|
* Subscribe to an editor bus event and register the matching unsubscribe, so the listener
|
|
17
|
-
* is removed automatically on `dispose()`.
|
|
25
|
+
* is removed automatically on `dispose()`. Returns that unsubscribe, for passing to
|
|
26
|
+
* {@link EditorManager.unregister} to detach it before `dispose()`.
|
|
18
27
|
*/
|
|
19
28
|
addEditorEvent(event, fn) {
|
|
20
29
|
this.editor.on(event, fn);
|
|
21
|
-
this.register(() => this.editor.off(event, fn));
|
|
30
|
+
return this.register(() => this.editor.off(event, fn));
|
|
22
31
|
}
|
|
23
32
|
/** @internal */
|
|
24
33
|
dispose() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/lib/editor/managers/EditorManager.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Editor } from '../Editor'\nimport type { TLEventMap } from '../types/emit-types'\n\n/**\n * Base class for editor-attached managers that need to clean up after themselves.\n *\n * Extending `EditorManager` gives a manager one consistent teardown contract: register\n * anything that needs undoing, and it runs automatically on `dispose()`. This keeps\n * subscription lifecycle symmetric \u2014 whatever sets a thing up is what tears it down \u2014 so\n * it can't be forgotten (the failure mode behind the strict-mode camera bug, #8892).\n *\n * Register cleanup by what you're creating:\n *\n * - Editor bus event (`tick`, `frame`, `change`, \u2026): {@link EditorManager.addEditorEvent}\n * - Store side effect (`editor.sideEffects.register\u2026`): `this.register(disposer)`\n * - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`\n * - DOM listener: `this.register(() => el.removeEventListener(...))`\n * - Other resource (cache, index, child manager): `this.register(() => x.dispose())`\n *\n * For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped\n * and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a\n * manager, use `editor.disposables`.\n *\n * If a manager needs ordered teardown (e.g. pause a loop before cancelling it), override\n * `dispose()` and call `super.dispose()` last \u2014 see `TickManager`.\n *\n * @public\n */\nexport abstract class EditorManager {\n\tprotected readonly disposables = new Set<() => void>()\n\n\tconstructor(protected readonly editor: Editor) {}\n\n\t/**\n\t * Register a teardown function to run on `dispose()`. The universal way for a manager to\n\t * make a resource \"see itself out\". Returns the same function for convenience.\n\t */\n\tprotected register(dispose: () => void): () => void {\n\t\tthis.disposables.add(dispose)\n\t\treturn dispose\n\t}\n\n\t/**\n\t * Subscribe to an editor bus event and register the matching unsubscribe, so the listener\n\t * is removed automatically on `dispose()`.\n\t */\n\tprotected addEditorEvent<E extends keyof TLEventMap>(\n\t\tevent: E,\n\t\tfn: (...args: TLEventMap[E]) => void\n\t): void {\n\t\tthis.editor.on(event, fn as any)\n\t\
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Editor } from '../Editor'\nimport type { TLEventMap } from '../types/emit-types'\n\n/**\n * Base class for editor-attached managers that need to clean up after themselves.\n *\n * Extending `EditorManager` gives a manager one consistent teardown contract: register\n * anything that needs undoing, and it runs automatically on `dispose()`. This keeps\n * subscription lifecycle symmetric \u2014 whatever sets a thing up is what tears it down \u2014 so\n * it can't be forgotten (the failure mode behind the strict-mode camera bug, #8892).\n *\n * Register cleanup by what you're creating:\n *\n * - Editor bus event (`tick`, `frame`, `change`, \u2026): {@link EditorManager.addEditorEvent}\n * - Store side effect (`editor.sideEffects.register\u2026`): `this.register(disposer)`\n * - Reaction (`react(...)` from `@tldraw/state`): `this.register(react(...))`\n * - DOM listener: `this.register(() => el.removeEventListener(...))`\n * - Other resource (cache, index, child manager): `this.register(() => x.dispose())`\n * - Resource held only part of the time (a listener attached while subscribers exist):\n * `this.register(...)` on attach and `this.unregister(...)` on detach, see `PerformanceManager`\n *\n * For timeouts, intervals, and animation frames prefer `editor.timers` (context-grouped\n * and auto-disposed) over raw `setTimeout`. For cleanup on the editor itself rather than a\n * manager, use `editor.disposables`.\n *\n * If a manager needs ordered teardown (e.g. pause a loop before cancelling it), override\n * `dispose()` and call `super.dispose()` last \u2014 see `TickManager`.\n *\n * @public\n */\nexport abstract class EditorManager {\n\tprotected readonly disposables = new Set<() => void>()\n\n\tconstructor(protected readonly editor: Editor) {}\n\n\t/**\n\t * Register a teardown function to run on `dispose()`. The universal way for a manager to\n\t * make a resource \"see itself out\". Returns the same function for convenience.\n\t */\n\tprotected register(dispose: () => void): () => void {\n\t\tthis.disposables.add(dispose)\n\t\treturn dispose\n\t}\n\n\t/**\n\t * Run a registered teardown now instead of at `dispose()`. A teardown that is no longer\n\t * registered (already run by `dispose()`, or never registered) is skipped, so nothing is\n\t * torn down twice.\n\t */\n\tprotected unregister(dispose: () => void): void {\n\t\tif (this.disposables.delete(dispose)) dispose()\n\t}\n\n\t/**\n\t * Subscribe to an editor bus event and register the matching unsubscribe, so the listener\n\t * is removed automatically on `dispose()`. Returns that unsubscribe, for passing to\n\t * {@link EditorManager.unregister} to detach it before `dispose()`.\n\t */\n\tprotected addEditorEvent<E extends keyof TLEventMap>(\n\t\tevent: E,\n\t\tfn: (...args: TLEventMap[E]) => void\n\t): () => void {\n\t\tthis.editor.on(event, fn as any)\n\t\treturn this.register(() => this.editor.off(event, fn as any))\n\t}\n\n\t/** @internal */\n\tdispose(): void {\n\t\tthis.disposables.forEach((d) => d())\n\t\tthis.disposables.clear()\n\t}\n}\n"],
|
|
5
|
+
"mappings": "AA8BO,MAAe,cAAc;AAAA,EAGnC,YAA+B,QAAgB;AAAhB;AAAA,EAAiB;AAAA,EAAjB;AAAA,EAFZ,cAAc,oBAAI,IAAgB;AAAA;AAAA;AAAA;AAAA;AAAA,EAQ3C,SAAS,SAAiC;AACnD,SAAK,YAAY,IAAI,OAAO;AAC5B,WAAO;AAAA,EACR;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,WAAW,SAA2B;AAC/C,QAAI,KAAK,YAAY,OAAO,OAAO,EAAG,SAAQ;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOU,eACT,OACA,IACa;AACb,SAAK,OAAO,GAAG,OAAO,EAAS;AAC/B,WAAO,KAAK,SAAS,MAAM,KAAK,OAAO,IAAI,OAAO,EAAS,CAAC;AAAA,EAC7D;AAAA;AAAA,EAGA,UAAgB;AACf,SAAK,YAAY,QAAQ,CAAC,MAAM,EAAE,CAAC;AACnC,SAAK,YAAY,MAAM;AAAA,EACxB;AACD;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|