@sailfish-ai/recorder 1.12.5 → 1.12.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +8 -0
  2. package/dist/canvas/adapters.js +382 -0
  3. package/dist/canvas/adapters.js.br +0 -0
  4. package/dist/canvas/adapters.js.gz +0 -0
  5. package/dist/canvas/canvasManager.js +1949 -0
  6. package/dist/canvas/canvasManager.js.br +0 -0
  7. package/dist/canvas/canvasManager.js.gz +0 -0
  8. package/dist/canvas/canvasPrivacy.js +233 -0
  9. package/dist/canvas/canvasPrivacy.js.br +0 -0
  10. package/dist/canvas/canvasPrivacy.js.gz +0 -0
  11. package/dist/canvas/codec.js +314 -0
  12. package/dist/canvas/codec.js.br +0 -0
  13. package/dist/canvas/codec.js.gz +0 -0
  14. package/dist/canvas/deviceClass.js +67 -0
  15. package/dist/canvas/deviceClass.js.br +0 -0
  16. package/dist/canvas/deviceClass.js.gz +0 -0
  17. package/dist/canvas/encodeWorker.js +284 -0
  18. package/dist/canvas/encodeWorker.js.br +0 -0
  19. package/dist/canvas/encodeWorker.js.gz +0 -0
  20. package/dist/canvas/governor.js +79 -0
  21. package/dist/canvas/governor.js.br +0 -0
  22. package/dist/canvas/governor.js.gz +0 -0
  23. package/dist/canvas/keyframes.js +183 -0
  24. package/dist/canvas/keyframes.js.br +0 -0
  25. package/dist/canvas/keyframes.js.gz +0 -0
  26. package/dist/canvas/mediaRecorderCapture.js +257 -0
  27. package/dist/canvas/mediaRecorderCapture.js.br +0 -0
  28. package/dist/canvas/mediaRecorderCapture.js.gz +0 -0
  29. package/dist/canvas/objectLog.js +150 -0
  30. package/dist/canvas/objectLog.js.br +0 -0
  31. package/dist/canvas/objectLog.js.gz +0 -0
  32. package/dist/canvas/patch2d.js +1302 -0
  33. package/dist/canvas/patch2d.js.br +0 -0
  34. package/dist/canvas/patch2d.js.gz +0 -0
  35. package/dist/canvas/patchGL.js +371 -0
  36. package/dist/canvas/patchGL.js.br +0 -0
  37. package/dist/canvas/patchGL.js.gz +0 -0
  38. package/dist/canvas/patchWebGPU.js +361 -0
  39. package/dist/canvas/patchWebGPU.js.br +0 -0
  40. package/dist/canvas/patchWebGPU.js.gz +0 -0
  41. package/dist/canvas/piiDetect.js +155 -0
  42. package/dist/canvas/piiDetect.js.br +0 -0
  43. package/dist/canvas/piiDetect.js.gz +0 -0
  44. package/dist/canvas/protocol.js +122 -0
  45. package/dist/canvas/protocol.js.br +0 -0
  46. package/dist/canvas/protocol.js.gz +0 -0
  47. package/dist/canvas/videoCapture.js +252 -0
  48. package/dist/canvas/videoCapture.js.br +0 -0
  49. package/dist/canvas/videoCapture.js.gz +0 -0
  50. package/dist/canvas/workerRelay.js +99 -0
  51. package/dist/canvas/workerRelay.js.br +0 -0
  52. package/dist/canvas/workerRelay.js.gz +0 -0
  53. package/dist/canvas/workerShim.js +227 -0
  54. package/dist/canvas/workerShim.js.br +0 -0
  55. package/dist/canvas/workerShim.js.gz +0 -0
  56. package/dist/canvas-worker.cjs +74 -0
  57. package/dist/canvas-worker.cjs.br +0 -0
  58. package/dist/canvas-worker.cjs.gz +0 -0
  59. package/dist/canvas-worker.js +78 -0
  60. package/dist/canvas-worker.js.br +0 -0
  61. package/dist/canvas-worker.js.gz +0 -0
  62. package/dist/canvasFrameStore.js +198 -0
  63. package/dist/canvasFrameStore.js.br +0 -0
  64. package/dist/canvasFrameStore.js.gz +0 -0
  65. package/dist/chunks/canvasManager-B9uQ-PFS.js +1456 -0
  66. package/dist/chunks/canvasManager-B9uQ-PFS.js.br +0 -0
  67. package/dist/chunks/canvasManager-B9uQ-PFS.js.gz +0 -0
  68. package/dist/chunks/canvasManager-Ce7vazTt.js +1440 -0
  69. package/dist/chunks/canvasManager-Ce7vazTt.js.br +0 -0
  70. package/dist/chunks/canvasManager-Ce7vazTt.js.gz +0 -0
  71. package/dist/chunks/{chunkSerializer-pkpgT-FH.js → chunkSerializer-B-z7p0r-.js} +1 -1
  72. package/dist/chunks/chunkSerializer-B-z7p0r-.js.br +0 -0
  73. package/dist/chunks/chunkSerializer-B-z7p0r-.js.gz +0 -0
  74. package/dist/chunks/{chunkSerializer-Cd8y9tID.js → chunkSerializer-C1knjCQ5.js} +1 -1
  75. package/dist/chunks/chunkSerializer-C1knjCQ5.js.br +0 -0
  76. package/dist/chunks/chunkSerializer-C1knjCQ5.js.gz +0 -0
  77. package/dist/chunks/{index-CXvHbLRT.js → index-BMHQ-0NY.js} +1115 -724
  78. package/dist/chunks/index-BMHQ-0NY.js.br +0 -0
  79. package/dist/chunks/index-BMHQ-0NY.js.gz +0 -0
  80. package/dist/chunks/{index-Bvd8oTpV.js → index-qgM1hFXO.js} +1025 -655
  81. package/dist/chunks/index-qgM1hFXO.js.br +0 -0
  82. package/dist/chunks/index-qgM1hFXO.js.gz +0 -0
  83. package/dist/chunks/patch2d-9voowhJ_.js +816 -0
  84. package/dist/chunks/patch2d-9voowhJ_.js.br +0 -0
  85. package/dist/chunks/patch2d-9voowhJ_.js.gz +0 -0
  86. package/dist/chunks/patch2d-PQxcaE6r.js +886 -0
  87. package/dist/chunks/patch2d-PQxcaE6r.js.br +0 -0
  88. package/dist/chunks/patch2d-PQxcaE6r.js.gz +0 -0
  89. package/dist/graphql.js +12 -1
  90. package/dist/graphql.js.br +0 -0
  91. package/dist/graphql.js.gz +0 -0
  92. package/dist/inAppReportIssueModal/index.js +10 -1
  93. package/dist/inAppReportIssueModal/index.js.br +0 -0
  94. package/dist/inAppReportIssueModal/index.js.gz +0 -0
  95. package/dist/index.js +144 -2
  96. package/dist/index.js.br +0 -0
  97. package/dist/index.js.gz +0 -0
  98. package/dist/recorder.cjs +2 -2
  99. package/dist/recorder.cjs.br +0 -0
  100. package/dist/recorder.cjs.gz +0 -0
  101. package/dist/recorder.js +52 -41
  102. package/dist/recorder.js.br +0 -0
  103. package/dist/recorder.js.gz +0 -0
  104. package/dist/recorder.umd.cjs +4842 -2215
  105. package/dist/recorder.umd.cjs.br +0 -0
  106. package/dist/recorder.umd.cjs.gz +0 -0
  107. package/dist/recording.js +119 -10
  108. package/dist/recording.js.br +0 -0
  109. package/dist/recording.js.gz +0 -0
  110. package/dist/types/canvas/adapters.d.ts +71 -0
  111. package/dist/types/canvas/canvasManager.d.ts +273 -0
  112. package/dist/types/canvas/canvasPrivacy.d.ts +184 -0
  113. package/dist/types/canvas/codec.d.ts +27 -0
  114. package/dist/types/canvas/deviceClass.d.ts +40 -0
  115. package/dist/types/canvas/encodeWorker.d.ts +54 -0
  116. package/dist/types/canvas/governor.d.ts +44 -0
  117. package/dist/types/canvas/keyframes.d.ts +81 -0
  118. package/dist/types/canvas/mediaRecorderCapture.d.ts +91 -0
  119. package/dist/types/canvas/objectLog.d.ts +75 -0
  120. package/dist/types/canvas/patch2d.d.ts +339 -0
  121. package/dist/types/canvas/patchGL.d.ts +74 -0
  122. package/dist/types/canvas/patchWebGPU.d.ts +37 -0
  123. package/dist/types/canvas/piiDetect.d.ts +60 -0
  124. package/dist/types/canvas/protocol.d.ts +176 -0
  125. package/dist/types/canvas/videoCapture.d.ts +67 -0
  126. package/dist/types/canvas/workerRelay.d.ts +29 -0
  127. package/dist/types/canvas/workerShim.d.ts +10 -0
  128. package/dist/types/canvasFrameStore.d.ts +23 -0
  129. package/dist/types/graphql.d.ts +1 -1
  130. package/dist/types/index.d.ts +99 -1
  131. package/dist/types/types.d.ts +40 -0
  132. package/dist/types/websocket.d.ts +19 -0
  133. package/dist/websocket.js +210 -0
  134. package/dist/websocket.js.br +0 -0
  135. package/dist/websocket.js.gz +0 -0
  136. package/package.json +7 -1
  137. package/dist/chunks/chunkSerializer-Cd8y9tID.js.br +0 -0
  138. package/dist/chunks/chunkSerializer-Cd8y9tID.js.gz +0 -0
  139. package/dist/chunks/chunkSerializer-pkpgT-FH.js.br +0 -0
  140. package/dist/chunks/chunkSerializer-pkpgT-FH.js.gz +0 -0
  141. package/dist/chunks/index-Bvd8oTpV.js.br +0 -0
  142. package/dist/chunks/index-Bvd8oTpV.js.gz +0 -0
  143. package/dist/chunks/index-CXvHbLRT.js.br +0 -0
  144. package/dist/chunks/index-CXvHbLRT.js.gz +0 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * A41 — MediaRecorder fallback video capture (SDK half), for browsers that have
3
+ * NO WebCodecs (`VideoEncoder` / `VideoFrame`) but DO have `MediaRecorder` +
4
+ * `HTMLCanvasElement.captureStream` (notably Safari < 26). Without this, such a
5
+ * canvas would degrade to keyframe-only; here we capture continuous video via
6
+ * MediaRecorder instead.
7
+ *
8
+ * Wire contract (the player agent implements the exact mirror):
9
+ * - ONE `video-init` SFCV frame, header
10
+ * { kind:"video-init", codec:"mediarecorder", mimeType:<recorder.mimeType>, w, h }
11
+ * (no binary payload).
12
+ * - `video-chunk` SFCV frames, header { kind:"video-chunk", codec:"mediarecorder", seq }
13
+ * each carrying ONE `MediaRecorder.ondataavailable` Blob's bytes as the
14
+ * payload. The FIRST webm chunk holds the init segment, so ORDER MUST be
15
+ * preserved (seq is monotonic, assigned in dataavailable order).
16
+ *
17
+ * Differences from VideoCapture (WebCodecs):
18
+ * - No same-task readback hazard: MediaRecorder pulls frames off the
19
+ * captureStream track itself, so there is no afterRender pixel grab and no
20
+ * guardrail-#6 concern. The manager still calls afterRender for dirtiness
21
+ * bookkeeping, but it is a no-op here.
22
+ * - Chunk bytes are read from the Blob asynchronously (blob.arrayBuffer());
23
+ * emission is chained so seq order is preserved even if reads resolve out
24
+ * of order.
25
+ *
26
+ * SSR rule: nothing here touches window/document at module scope; capability
27
+ * detection and all DOM/Media access happen inside methods called from the
28
+ * manager (which itself only runs in the browser).
29
+ */
30
+ const TIMESLICE_MS = 1_000; // ~1s cadence so chunks flow steadily
31
+ const CAPTURE_FPS = 30;
32
+ /** webm mime candidates, preferred first. Safari < 26 generally supports
33
+ * `video/mp4` via MediaRecorder; we still probe webm first for player parity
34
+ * (the player decodes whatever mimeType we report). */
35
+ const MIME_CANDIDATES = [
36
+ "video/webm;codecs=vp9",
37
+ "video/webm;codecs=vp8",
38
+ "video/webm",
39
+ "video/mp4",
40
+ ];
41
+ /**
42
+ * Feature detection (contract): MediaRecorder fallback is usable ONLY when
43
+ * `MediaRecorder`, `HTMLCanvasElement.prototype.captureStream`, and
44
+ * `MediaRecorder.isTypeSupported` for at least one candidate mime are present.
45
+ * Callers prefer WebCodecs and reach here only when WebCodecs is ABSENT.
46
+ */
47
+ export function mediaRecorderSupported() {
48
+ if (typeof MediaRecorder === "undefined")
49
+ return false;
50
+ if (typeof HTMLCanvasElement === "undefined")
51
+ return false;
52
+ if (typeof HTMLCanvasElement.prototype.captureStream !== "function")
53
+ return false;
54
+ if (typeof MediaRecorder.isTypeSupported !== "function")
55
+ return false;
56
+ return pickMime() !== null;
57
+ }
58
+ /** First candidate mime MediaRecorder claims to support, or null. */
59
+ export function pickMime() {
60
+ if (typeof MediaRecorder === "undefined" || typeof MediaRecorder.isTypeSupported !== "function") {
61
+ return null;
62
+ }
63
+ for (const m of MIME_CANDIDATES) {
64
+ try {
65
+ if (MediaRecorder.isTypeSupported(m))
66
+ return m;
67
+ }
68
+ catch { /* probe next */ }
69
+ }
70
+ return null;
71
+ }
72
+ export class MediaRecorderCapture {
73
+ canvas;
74
+ out;
75
+ recorder = null;
76
+ stream = null;
77
+ stopped = false;
78
+ started = false;
79
+ seq = 0;
80
+ /** chain Blob -> bytes emission so seq order is preserved across async reads */
81
+ emitChain = Promise.resolve();
82
+ constructor(canvas, out) {
83
+ this.canvas = canvas;
84
+ this.out = out;
85
+ }
86
+ static supported() {
87
+ return mediaRecorderSupported();
88
+ }
89
+ /** Resolves true once the recorder started; false -> keyframes-only fallback. */
90
+ async start() {
91
+ if (this.stopped)
92
+ return false;
93
+ const w = this.canvas.width;
94
+ const h = this.canvas.height;
95
+ if (!w || !h) {
96
+ this.out.note("zero-size canvas — MediaRecorder capture unavailable");
97
+ return false;
98
+ }
99
+ const mime = pickMime();
100
+ if (!mime) {
101
+ this.out.note("no MediaRecorder-supported mime — keyframes only");
102
+ return false;
103
+ }
104
+ let stream;
105
+ try {
106
+ stream = this.canvas.captureStream(CAPTURE_FPS);
107
+ }
108
+ catch (e) {
109
+ this.out.note(`captureStream failed: ${e.message}`);
110
+ return false;
111
+ }
112
+ let recorder;
113
+ try {
114
+ recorder = new MediaRecorder(stream, { mimeType: mime });
115
+ }
116
+ catch (e) {
117
+ this.out.note(`MediaRecorder construct failed: ${e.message}`);
118
+ try {
119
+ stopStream(stream);
120
+ }
121
+ catch { /* best effort */ }
122
+ return false;
123
+ }
124
+ this.stream = stream;
125
+ this.recorder = recorder;
126
+ recorder.ondataavailable = (e) => this.onData(e.data);
127
+ recorder.onerror = (e) => {
128
+ this.out.note(`MediaRecorder error: ${String(e.error ?? e)}`);
129
+ };
130
+ try {
131
+ // timeslice => periodic dataavailable (~1s) so chunks stream live.
132
+ recorder.start(TIMESLICE_MS);
133
+ }
134
+ catch (e) {
135
+ this.out.note(`MediaRecorder start failed: ${e.message}`);
136
+ this.teardownStream();
137
+ return false;
138
+ }
139
+ this.started = true;
140
+ // The mimeType the recorder actually negotiated may differ from the
141
+ // requested one (e.g. codecs appended); report the recorder's own value so
142
+ // the player decodes against the truth.
143
+ this.out.onInit(recorder.mimeType || mime, w, h);
144
+ return true;
145
+ }
146
+ /** A non-empty Blob from dataavailable. Read bytes async, emit in seq order. */
147
+ onData(blob) {
148
+ if (!blob || blob.size === 0)
149
+ return;
150
+ const seq = this.seq++;
151
+ this.emitChain = this.emitChain.then(async () => {
152
+ try {
153
+ const buf = await blob.arrayBuffer();
154
+ if (buf.byteLength > 0)
155
+ this.out.onChunk(seq, buf);
156
+ }
157
+ catch (e) {
158
+ this.out.note(`chunk read failed: ${e.message}`);
159
+ }
160
+ });
161
+ }
162
+ /**
163
+ * The manager's microtask pump calls this after every render for dirtiness
164
+ * bookkeeping. MediaRecorder pulls frames off the captureStream track on its
165
+ * own, so this is intentionally a no-op (kept for VideoCapture parity). The
166
+ * `_t` mirrors VideoCapture.afterRender's signature.
167
+ */
168
+ afterRender(_t) { }
169
+ /** Interaction boost is a no-op: the captureStream fps is fixed and the
170
+ * recorder cannot be told to key on demand. Kept for VideoCapture parity. */
171
+ onInteraction(_tMs) { }
172
+ /**
173
+ * Non-terminal drain (visibilitychange:hidden): force the recorder to emit a
174
+ * chunk for everything buffered so far WITHOUT stopping, so capture resumes if
175
+ * the tab returns. Mirrors VideoCapture.flush(). No-op once stopped.
176
+ */
177
+ async flush() {
178
+ if (this.stopped || !this.recorder)
179
+ return;
180
+ try {
181
+ if (this.recorder.state === "recording")
182
+ this.recorder.requestData();
183
+ }
184
+ catch { /* best effort */ }
185
+ // let the synchronous-ish dataavailable enqueue, then settle the chain
186
+ await Promise.resolve();
187
+ await this.emitChain.catch(() => { });
188
+ }
189
+ /**
190
+ * Terminal stop: flush the final chunk (requestData then stop fire a last
191
+ * dataavailable), settle the emission chain so the final bytes reach onChunk,
192
+ * then release the recorder + stream tracks. Idempotent.
193
+ */
194
+ async stop() {
195
+ if (this.stopped)
196
+ return;
197
+ this.stopped = true;
198
+ const recorder = this.recorder;
199
+ if (recorder && this.started) {
200
+ const lastData = new Promise((resolve) => {
201
+ const done = () => resolve();
202
+ recorder.addEventListener("stop", done, { once: true });
203
+ // safety: never hang the manager's stop on a wedged recorder
204
+ setTimeout(done, 3_000);
205
+ });
206
+ try {
207
+ if (recorder.state !== "inactive") {
208
+ recorder.requestData(); // flush the tail timeslice
209
+ recorder.stop(); // fires a final dataavailable then 'stop'
210
+ }
211
+ }
212
+ catch { /* already inactive */ }
213
+ await lastData;
214
+ }
215
+ // settle any in-flight blob reads so their chunks are emitted before teardown
216
+ await this.emitChain.catch(() => { });
217
+ this.teardownStream();
218
+ }
219
+ /**
220
+ * Immediate teardown WITHOUT flushing (A22/A6/A10 parity): the canvas is gone
221
+ * or must not be recorded, so drop the recorder and stop the stream tracks
222
+ * without emitting the tail. Idempotent.
223
+ */
224
+ abort() {
225
+ if (this.stopped)
226
+ return;
227
+ this.stopped = true;
228
+ try {
229
+ if (this.recorder && this.recorder.state !== "inactive")
230
+ this.recorder.stop();
231
+ }
232
+ catch { /* best effort */ }
233
+ this.teardownStream();
234
+ }
235
+ teardownStream() {
236
+ if (this.recorder) {
237
+ this.recorder.ondataavailable = null;
238
+ this.recorder.onerror = null;
239
+ this.recorder = null;
240
+ }
241
+ if (this.stream) {
242
+ stopStream(this.stream);
243
+ this.stream = null;
244
+ }
245
+ }
246
+ }
247
+ function stopStream(stream) {
248
+ try {
249
+ for (const track of stream.getTracks()) {
250
+ try {
251
+ track.stop();
252
+ }
253
+ catch { /* best effort */ }
254
+ }
255
+ }
256
+ catch { /* getTracks unavailable */ }
257
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Cross-context canvas object logging — the global creation log for
3
+ * CanvasGradient / CanvasPattern / Path2D objects.
4
+ *
5
+ * Gradients, patterns, and Path2Ds are OPAQUE: their stops/segments cannot be
6
+ * read back after construction, so they can only be captured by observing the
7
+ * factory call live. They may also be created on one context (or no context —
8
+ * Konva builds gradients on a hidden dummy context) and used on another, so
9
+ * creation is logged GLOBALLY here, and each recorder lazily emits its own
10
+ * copy into whichever command stream first references the object
11
+ * (patch2d.ts serializeFactoryObject).
12
+ *
13
+ * ROOT-CAUSE GUARDRAIL (Phase 0.5 E2E, found on /2d-objects): app scene code
14
+ * typically constructs these objects ONCE at startup — often before the SDK's
15
+ * settings fetch + session start complete and startCanvasRecording() installs
16
+ * the full capture hooks. Anything created in that gap is permanently
17
+ * unserializable ({$:"unsupported"}). This module is therefore dependency-free
18
+ * and cheap so index.tsx can install it SYNCHRONOUSLY at startRecording()
19
+ * entry (Phase 1 critical interceptors) whenever canvas recording is opted
20
+ * in — long before any network roundtrip.
21
+ *
22
+ * SSR rule (veritas/jsts-frontend/CLAUDE.md): no window/document/navigator or
23
+ * canvas globals at module scope; all installation happens inside the
24
+ * exported install functions, each guarded with typeof checks.
25
+ */
26
+ /** factory-created object -> its construction + method-call history */
27
+ export const objectLogs = new WeakMap();
28
+ /**
29
+ * A26 — per-object recorded-calls cap. Gradients/patterns/Path2Ds are normally
30
+ * built with a handful of stops/segments at construction; a few apps, though,
31
+ * reuse one long-lived object and keep appending to it for the whole session
32
+ * (e.g. a Path2D that accumulates points), growing `calls[]` without bound and
33
+ * leaking memory. We cap the recorded history: the first MAX_OBJECT_CALLS calls
34
+ * are kept (covering every realistic construction), and anything beyond is
35
+ * dropped with a one-time marker. Replay of the common case is unaffected.
36
+ */
37
+ export const MAX_OBJECT_CALLS = 4_096;
38
+ /** Append a method call to an object's log, honoring the per-object cap. */
39
+ export function pushObjectCall(log, method, args) {
40
+ if (!log)
41
+ return;
42
+ if (log.calls.length >= MAX_OBJECT_CALLS) {
43
+ log.callsCapped = true;
44
+ return;
45
+ }
46
+ log.calls.push({ method, args });
47
+ }
48
+ export const OBJECT_FACTORIES = [
49
+ "createLinearGradient",
50
+ "createRadialGradient",
51
+ "createConicGradient",
52
+ "createPattern",
53
+ ];
54
+ export const PATH_OPS = [
55
+ "addPath", "arc", "arcTo", "bezierCurveTo", "closePath", "ellipse",
56
+ "lineTo", "moveTo", "quadraticCurveTo", "rect", "roundRect",
57
+ ];
58
+ let nativePath2D = null;
59
+ /** The unwrapped Path2D constructor (set once Path2D logging installs). */
60
+ export function getNativePath2D() {
61
+ return nativePath2D;
62
+ }
63
+ /** prototypes whose factory methods are already wrapped */
64
+ const patchedFactoryProtos = new WeakSet();
65
+ let sharedLoggingInstalled = false;
66
+ /**
67
+ * Wrap the gradient/pattern factory methods of one 2D-context prototype
68
+ * (CanvasRenderingContext2D on the main thread, OffscreenCanvasRendering-
69
+ * Context2D inside workers) so every created object lands in `objectLogs`.
70
+ * Idempotent per prototype.
71
+ */
72
+ export function installFactoryLoggingOnProto(protoObj) {
73
+ if (patchedFactoryProtos.has(protoObj))
74
+ return;
75
+ patchedFactoryProtos.add(protoObj);
76
+ const proto = protoObj;
77
+ for (const name of OBJECT_FACTORIES) {
78
+ const desc = Object.getOwnPropertyDescriptor(protoObj, name);
79
+ if (!desc || typeof desc.value !== "function")
80
+ continue;
81
+ const orig = desc.value;
82
+ proto[name] = function (...args) {
83
+ const result = orig.apply(this, args);
84
+ // log unconditionally — the object may be used on a DIFFERENT
85
+ // context than the one that created it (Konva's dummy context).
86
+ if (result && typeof result === "object") {
87
+ objectLogs.set(result, { factory: name, args, calls: [] });
88
+ }
89
+ return result;
90
+ };
91
+ }
92
+ }
93
+ /**
94
+ * Wrap the realm-global object types: CanvasGradient.addColorStop appends to
95
+ * the creating factory's log; Path2D construction + segment ops are logged
96
+ * per instance (replayed via __create '__path2d' + __objcall when a path is
97
+ * first used on a recorded context). Idempotent.
98
+ */
99
+ export function installSharedObjectLogging() {
100
+ if (sharedLoggingInstalled)
101
+ return;
102
+ sharedLoggingInstalled = true;
103
+ // gradient stops: append to the object's global log (cross-context safe)
104
+ if (typeof CanvasGradient !== "undefined") {
105
+ const orig = CanvasGradient.prototype.addColorStop;
106
+ CanvasGradient.prototype.addColorStop = function (...args) {
107
+ pushObjectCall(objectLogs.get(this), "addColorStop", args);
108
+ return orig.apply(this, args);
109
+ };
110
+ }
111
+ if (typeof Path2D !== "undefined" && !nativePath2D) {
112
+ nativePath2D = Path2D;
113
+ for (const m of PATH_OPS) {
114
+ const orig = Path2D.prototype[m];
115
+ if (typeof orig !== "function")
116
+ continue;
117
+ Path2D.prototype[m] = function (...args) {
118
+ pushObjectCall(objectLogs.get(this), m, args);
119
+ return orig.apply(this, args);
120
+ };
121
+ }
122
+ const native = nativePath2D;
123
+ const Wrapped = function (arg) {
124
+ const instance = arg === undefined ? new native() : new native(arg);
125
+ objectLogs.set(instance, {
126
+ factory: "__path2d",
127
+ args: [typeof arg === "string" ? arg : null],
128
+ calls: arg instanceof native ? [...(objectLogs.get(arg)?.calls ?? [])] : [],
129
+ });
130
+ return instance;
131
+ };
132
+ Wrapped.prototype = native.prototype;
133
+ globalThis.Path2D = Wrapped;
134
+ }
135
+ }
136
+ /**
137
+ * Early hook for the SDK entry path: arm all object logging on the main
138
+ * thread BEFORE app scene code constructs its gradients/patterns/paths.
139
+ * Safe to call multiple times; no-ops outside a browser realm.
140
+ */
141
+ export function installEarlyObjectLogging() {
142
+ if (typeof CanvasRenderingContext2D !== "undefined") {
143
+ installFactoryLoggingOnProto(CanvasRenderingContext2D.prototype);
144
+ }
145
+ if (typeof OffscreenCanvasRenderingContext2D !== "undefined") {
146
+ // main-thread OffscreenCanvas 2D contexts share the same object types
147
+ installFactoryLoggingOnProto(OffscreenCanvasRenderingContext2D.prototype);
148
+ }
149
+ installSharedObjectLogging();
150
+ }
Binary file
Binary file