@buoy-gg/js-top 7.0.29 → 7.0.31

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.
@@ -1,495 +0,0 @@
1
- /**
2
- * Origin registry — the "process table". An origin is the identity of a task
3
- * scheduling site.
4
- *
5
- * Two-stage identity keeps the hot path free of stack captures:
6
- * - Cheap key `${api}|${fnName}` — computed on every schedule, just string
7
- * concat + Map lookup.
8
- * - Once an origin gets hot (PROMOTE_MIN_CALLS schedules or
9
- * PROMOTE_MIN_TOTAL_MS booked), the next schedule captures ONE
10
- * `Error().stack` (throttled per-key and globally) and refines the key
11
- * with the scheduling site's caller frame. Hermes dev stacks reference
12
- * bundle offsets, not source files — the CALLER FUNCTION NAME is the
13
- * readable part, so labels read `setInterval ← startPolling`; raw frames
14
- * are kept on the origin for future desktop symbolication.
15
- */
16
-
17
- import {
18
- MAX_ORIGINS,
19
- PROMOTE_MIN_CALLS,
20
- PROMOTE_MIN_TOTAL_MS,
21
- WINDOW_BUCKETS,
22
- HISTORY_BUCKETS,
23
- BUCKET_MS,
24
- type JsTopRow,
25
- type JsTopOriginDetail,
26
- type OriginStackFrame,
27
- type TaskApi,
28
- } from "../types";
29
- import { bookAttributedRange, epochOf } from "./aggregator";
30
- import { buoyJsTopSymbolicateOrigin } from "./symbolicate";
31
-
32
- const STACK_CAPTURE_MIN_INTERVAL_PER_KEY_MS = 500;
33
- const STACK_CAPTURES_PER_SEC_GLOBAL = 20;
34
-
35
- export interface Origin {
36
- key: string;
37
- api: TaskApi;
38
- label: string;
39
- /** Caller function name from the scheduling stack, once refined. */
40
- caller?: string;
41
- file?: string;
42
- line?: number;
43
- frames?: OriginStackFrame[];
44
- calls: number;
45
- totalMs: number;
46
- maxMs: number;
47
- scheduleCount: number;
48
- lastSeenAt: number;
49
- promoted: boolean;
50
- /** On cheap origins: the refined key new work currently attributes to. */
51
- activeRefinedKey?: string;
52
- lastCaptureAt: number;
53
- /** Dev-only Metro symbolication attempted (one shot per origin). */
54
- symbolicated?: boolean;
55
- ring: Float64Array;
56
- ringStamps: Float64Array;
57
- }
58
-
59
- const origins = new Map<string, Origin>();
60
-
61
- /** Pinned bucket that absorbs evicted cold origins. */
62
- export const OTHER_KEY = "other";
63
-
64
- let globalCaptureWindowStart = 0;
65
- let globalCapturesInWindow = 0;
66
-
67
- function makeOrigin(key: string, api: TaskApi, label: string): Origin {
68
- return {
69
- key,
70
- api,
71
- label,
72
- calls: 0,
73
- totalMs: 0,
74
- maxMs: 0,
75
- scheduleCount: 0,
76
- lastSeenAt: 0,
77
- promoted: false,
78
- lastCaptureAt: 0,
79
- ring: new Float64Array(WINDOW_BUCKETS),
80
- ringStamps: new Float64Array(WINDOW_BUCKETS).fill(-1),
81
- };
82
- }
83
-
84
- function windowMsOf(o: Origin, nowT: number): number {
85
- const current = epochOf(nowT);
86
- let sum = 0;
87
- for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
88
- const i = e % WINDOW_BUCKETS;
89
- if (i >= 0 && o.ringStamps[i] === e) sum += o.ring[i];
90
- }
91
- return sum;
92
- }
93
-
94
- function evictColdest(nowT: number): void {
95
- let coldest: Origin | null = null;
96
- let coldestScore = Infinity;
97
- for (const o of origins.values()) {
98
- if (o.key === OTHER_KEY) continue;
99
- // Rank by recent cost, tiebreak by staleness.
100
- const score = windowMsOf(o, nowT) * 1e6 + o.lastSeenAt;
101
- if (score < coldestScore) {
102
- coldestScore = score;
103
- coldest = o;
104
- }
105
- }
106
- if (!coldest) return;
107
- const other = getOrCreate(OTHER_KEY, "other", "other (evicted origins)");
108
- other.calls += coldest.calls;
109
- other.totalMs += coldest.totalMs;
110
- other.maxMs = Math.max(other.maxMs, coldest.maxMs);
111
- other.lastSeenAt = Math.max(other.lastSeenAt, coldest.lastSeenAt);
112
- origins.delete(coldest.key);
113
- }
114
-
115
- function getOrCreate(key: string, api: TaskApi, label: string): Origin {
116
- let o = origins.get(key);
117
- if (!o) {
118
- o = makeOrigin(key, api, label);
119
- origins.set(key, o);
120
- }
121
- return o;
122
- }
123
-
124
- /**
125
- * Parse a Hermes stack into frames, skipping engine/wrapper frames. Engine
126
- * functions are deliberately named with a `buoyJsTop` prefix so they are
127
- * identifiable here even in bundle-offset stacks.
128
- */
129
- export function parseStack(stack: string): {
130
- caller?: string;
131
- frames: OriginStackFrame[];
132
- } {
133
- const frames: OriginStackFrame[] = [];
134
- let caller: string | undefined;
135
- const lines = stack.split("\n");
136
- for (const lineText of lines) {
137
- const m =
138
- /at\s+(.*?)\s+\((?:address at\s+)?(.*?):(\d+):(\d+)\)/.exec(lineText) ??
139
- /at\s+()(.*?):(\d+):(\d+)$/.exec(lineText.trim());
140
- if (!m) continue;
141
- const fnName = m[1] ?? "";
142
- if (
143
- fnName.includes("buoyJsTop") ||
144
- fnName === "Error" ||
145
- m[2]?.includes("/js-top/")
146
- ) {
147
- continue;
148
- }
149
- frames.push({
150
- file: m[2] ?? "",
151
- line: Number(m[3]),
152
- col: Number(m[4]),
153
- });
154
- // React splices internal frame names around effect callbacks
155
- // (react_stack_bottom_frame etc.) — their OFFSETS map to user code, so
156
- // keep the frame, but never present them as the "caller".
157
- const isReactInternal =
158
- /^(react_stack_bottom_frame|runWithFiberInDEV|commitHookEffectList|flushPassive|performWork|renderWithHooks|safelyCall)/.test(
159
- fnName,
160
- ) ||
161
- // Promise/polyfill machinery — technically the caller, never the answer.
162
- /^(then|catch|finally|resolve|reject|fulfilled|rejected|step|tryCallOne|tryCallTwo|tryCatch|invokeCallback|notify)$/.test(
163
- fnName,
164
- );
165
- if (!caller && fnName && fnName !== "anonymous" && !isReactInternal) {
166
- caller = fnName;
167
- }
168
- if (frames.length >= 3) break;
169
- }
170
- return { caller, frames };
171
- }
172
-
173
- function allowCapture(o: Origin, nowT: number): boolean {
174
- if (nowT - o.lastCaptureAt < STACK_CAPTURE_MIN_INTERVAL_PER_KEY_MS) {
175
- return false;
176
- }
177
- if (nowT - globalCaptureWindowStart > 1000) {
178
- globalCaptureWindowStart = nowT;
179
- globalCapturesInWindow = 0;
180
- }
181
- if (globalCapturesInWindow >= STACK_CAPTURES_PER_SEC_GLOBAL) return false;
182
- globalCapturesInWindow++;
183
- o.lastCaptureAt = nowT;
184
- return true;
185
- }
186
-
187
- function shortFile(file: string): string | undefined {
188
- if (!file) return undefined;
189
- // Strip scheme://host:port/ and query — keeps the bundle-relative tail.
190
- // Metro dev URLs carry params after "//&" instead of "?" — strip both.
191
- const noQuery = (file.split("?")[0] ?? file).replace(/\/\/&.*$/, "");
192
- const m = /^[a-z]+:\/\/[^/]+\/(.*)$/.exec(noQuery);
193
- const tail = m ? m[1] : noQuery;
194
- const parts = tail.split("/");
195
- return parts.slice(-2).join("/");
196
- }
197
-
198
- /**
199
- * Resolve the origin key for a schedule. Called from patched globals at
200
- * schedule time — this is the hot path; the common case is one Map get and
201
- * two counter bumps.
202
- */
203
- export function buoyJsTopOriginExists(key: string): boolean {
204
- return origins.has(key);
205
- }
206
-
207
- export function buoyJsTopScheduleOrigin(
208
- api: TaskApi,
209
- fn: { name?: string; displayName?: string } | ((...args: never[]) => unknown),
210
- nowT: number,
211
- /**
212
- * False when identity is resolved at FIRE time (callback scheduled while
213
- * the engine was off, or stats cleared since): a runtime stack is not a
214
- * scheduling site, so stack capture is skipped on that path.
215
- */
216
- allowStackCapture = true,
217
- ): string {
218
- const fnLike = fn as { name?: string; displayName?: string };
219
- const name =
220
- (typeof fnLike.displayName === "string" && fnLike.displayName) ||
221
- (typeof fnLike.name === "string" && fnLike.name) ||
222
- "anonymous";
223
- const cheapKey = `${api}|${name}`;
224
- let o = origins.get(cheapKey);
225
- if (!o) {
226
- if (origins.size >= MAX_ORIGINS) evictColdest(nowT);
227
- o = makeOrigin(
228
- cheapKey,
229
- api,
230
- name === "anonymous" ? apiLabel(api) : `${apiLabel(api)} ${name}`,
231
- );
232
- origins.set(cheapKey, o);
233
- }
234
- o.scheduleCount++;
235
- if (
236
- !o.promoted &&
237
- (o.scheduleCount >= PROMOTE_MIN_CALLS || o.totalMs >= PROMOTE_MIN_TOTAL_MS)
238
- ) {
239
- o.promoted = true;
240
- }
241
- // Intervals schedule exactly ONCE — this call is the only chance to see
242
- // their scheduling stack, so capture eagerly instead of waiting for
243
- // promotion (which is driven by schedule counts they'll never accrue).
244
- if (
245
- (o.promoted || api === "interval") &&
246
- allowStackCapture &&
247
- allowCapture(o, nowT)
248
- ) {
249
- const { caller, frames } = parseStack(new Error().stack ?? "");
250
- const top = frames[0];
251
- if (top || caller) {
252
- // Keys stay short + token-cheap: bundle URLs are stripped down to the
253
- // trailing path segment (full frames live on the origin record).
254
- const site = top ? `${shortFile(top.file) ?? "?"}:${top.line}` : "?";
255
- const refinedKey = `${cheapKey}@${caller ?? "?"}:${site}`;
256
- let refined = origins.get(refinedKey);
257
- if (!refined) {
258
- if (origins.size >= MAX_ORIGINS) evictColdest(nowT);
259
- // Label rule: the function name (when it exists) is the anchor and
260
- // is never discarded; a readable caller or site is appended.
261
- const base =
262
- name === "anonymous" ? apiLabel(api) : `${apiLabel(api)} ${name}`;
263
- refined = makeOrigin(
264
- refinedKey,
265
- api,
266
- caller
267
- ? `${base} ← ${caller}`
268
- : name === "anonymous"
269
- ? `${base} @ ${shortFile(top?.file ?? "") ?? "?"}`
270
- : base,
271
- );
272
- refined.caller = caller;
273
- refined.file = top ? shortFile(top.file) : undefined;
274
- refined.line = top?.line;
275
- refined.frames = frames;
276
- refined.promoted = true;
277
- // Migrate the cheap origin's accumulated stats into the refined one
278
- // and zero the cheap record — the table must never show the same
279
- // scheduling site as two rows (pre- and post-refinement).
280
- refined.calls = o.calls;
281
- refined.totalMs = o.totalMs;
282
- refined.maxMs = o.maxMs;
283
- refined.scheduleCount = o.scheduleCount;
284
- refined.lastSeenAt = o.lastSeenAt;
285
- refined.ring.set(o.ring);
286
- refined.ringStamps.set(o.ringStamps);
287
- o.calls = 0;
288
- o.totalMs = 0;
289
- o.maxMs = 0;
290
- o.ring.fill(0);
291
- o.ringStamps.fill(-1);
292
- origins.set(refinedKey, refined);
293
- // Upgrade the label to real source coordinates when Metro is around.
294
- buoyJsTopSymbolicateOrigin(refined, apiLabel(api));
295
- }
296
- o.activeRefinedKey = refinedKey;
297
- }
298
- }
299
- return o.activeRefinedKey ?? cheapKey;
300
- }
301
-
302
- function apiLabel(api: TaskApi): string {
303
- switch (api) {
304
- case "timeout":
305
- return "setTimeout";
306
- case "interval":
307
- return "setInterval";
308
- case "immediate":
309
- return "setImmediate";
310
- case "raf":
311
- return "requestAnimationFrame";
312
- case "microtask":
313
- return "queueMicrotask";
314
- case "then":
315
- return "Promise.then";
316
- case "bridge":
317
- return "bridge";
318
- default:
319
- return api;
320
- }
321
- }
322
-
323
- /** Book a completed call's exclusive time onto its origin + global buckets. */
324
- export function buoyJsTopBookExclusive(
325
- originKey: string,
326
- exclusiveMs: number,
327
- start: number,
328
- end: number,
329
- ): void {
330
- const o = origins.get(originKey);
331
- if (!o) return;
332
- o.calls++;
333
- o.totalMs += exclusiveMs;
334
- if (exclusiveMs > o.maxMs) o.maxMs = exclusiveMs;
335
- o.lastSeenAt = end;
336
- // Promotion by accumulated cost must ALSO happen at book time: an
337
- // interval never re-schedules, so the schedule-time check alone would
338
- // leave a 1000ms/run interval "not hot" forever.
339
- if (!o.promoted && o.totalMs >= PROMOTE_MIN_TOTAL_MS) o.promoted = true;
340
- // Per-origin ring, spread across the call's span like the global buckets.
341
- const span = end - start;
342
- const ratio = span > 0 ? Math.min(1, exclusiveMs / span) : 1;
343
- const clampedStart = Math.max(start, end - WINDOW_BUCKETS * BUCKET_MS);
344
- let e = epochOf(clampedStart);
345
- const lastEpoch = epochOf(end);
346
- let cursor = clampedStart;
347
- while (e <= lastEpoch) {
348
- const bucketEnd = (e + 1) * BUCKET_MS;
349
- const sliceEnd = Math.min(end, bucketEnd);
350
- const ms = sliceEnd - cursor;
351
- if (ms > 0) {
352
- const i = e % WINDOW_BUCKETS;
353
- if (i >= 0) {
354
- if (o.ringStamps[i] !== e) {
355
- o.ringStamps[i] = e;
356
- o.ring[i] = 0;
357
- }
358
- o.ring[i] += span > 0 ? ms * ratio : exclusiveMs;
359
- }
360
- }
361
- cursor = sliceEnd;
362
- e++;
363
- }
364
- bookAttributedRange(start, end, exclusiveMs);
365
- }
366
-
367
- export function labelForOrigin(key: string): string | null {
368
- return origins.get(key)?.label ?? null;
369
- }
370
-
371
- /** Build the ranked table rows (excludes the pinned unattributed row — the
372
- * controller assembles that from aggregator residuals). */
373
- export function buildRows(
374
- nowT: number,
375
- windowBusy: number,
376
- topN: number,
377
- ): JsTopRow[] {
378
- const rows: JsTopRow[] = [];
379
- for (const o of origins.values()) {
380
- // Cheap keys that have handed off to a refined key stop accumulating new
381
- // work but keep their pre-promotion totals; hide them once idle.
382
- const windowMs = windowMsOf(o, nowT);
383
- if (windowMs <= 0 && o.totalMs < 1) continue;
384
- rows.push({
385
- key: o.key,
386
- label: o.label,
387
- api: o.api,
388
- file: o.file,
389
- line: o.line,
390
- windowMs: Math.round(windowMs * 10) / 10,
391
- pctOfBusy:
392
- windowBusy > 0
393
- ? Math.min(100, Math.round((windowMs / windowBusy) * 1000) / 10)
394
- : 0,
395
- totalMs: Math.round(o.totalMs * 10) / 10,
396
- calls: o.calls,
397
- avgMs: o.calls > 0 ? Math.round((o.totalMs / o.calls) * 100) / 100 : 0,
398
- maxMs: Math.round(o.maxMs * 100) / 100,
399
- lastSeenAgoMs: o.lastSeenAt > 0 ? Math.max(0, nowT - o.lastSeenAt) : 0,
400
- // Filled in by the controller from the freeze-stats provider.
401
- freezes: 0,
402
- worstFreezeMs: 0,
403
- });
404
- }
405
- rows.sort((a, b) => b.windowMs - a.windowMs || b.totalMs - a.totalMs);
406
- // Noise gate: rows that can't matter (<1ms window, <0.5% busy, no freezes)
407
- // collapse into a single aggregate so the table stays a shortlist.
408
- const majors: JsTopRow[] = [];
409
- const minors: JsTopRow[] = [];
410
- for (const r of rows) {
411
- (r.windowMs >= 1 || r.pctOfBusy >= 0.5 || r.freezes > 0
412
- ? majors
413
- : minors
414
- ).push(r);
415
- }
416
- if (minors.length >= 2) {
417
- majors.push({
418
- key: "minor",
419
- label: `${minors.length} minor origins (≤1ms each)`,
420
- api: "other",
421
- windowMs:
422
- Math.round(minors.reduce((sum, r) => sum + r.windowMs, 0) * 10) / 10,
423
- pctOfBusy:
424
- Math.round(minors.reduce((sum, r) => sum + r.pctOfBusy, 0) * 10) / 10,
425
- totalMs:
426
- Math.round(minors.reduce((sum, r) => sum + r.totalMs, 0) * 10) / 10,
427
- calls: minors.reduce((sum, r) => sum + r.calls, 0),
428
- avgMs: 0,
429
- maxMs: Math.max(0, ...minors.map((r) => r.maxMs)),
430
- lastSeenAgoMs: Math.min(...minors.map((r) => r.lastSeenAgoMs)),
431
- freezes: 0,
432
- worstFreezeMs: 0,
433
- });
434
- return majors.slice(0, topN);
435
- }
436
- return rows.slice(0, topN);
437
- }
438
-
439
- /**
440
- * Full origin detail minus freeze stats (the controller merges those). The
441
- * bucket array is the origin's 5s activity trend, oldest→newest.
442
- */
443
- export function getOriginDetailBase(
444
- key: string,
445
- nowT: number,
446
- windowBusy: number,
447
- ): Omit<JsTopOriginDetail, "freezes" | "worstFreezeMs"> | null {
448
- const o = origins.get(key);
449
- if (!o) return null;
450
- const current = epochOf(nowT);
451
- const buckets: number[] = [];
452
- let windowMs = 0;
453
- for (let e = current - WINDOW_BUCKETS + 1; e <= current; e++) {
454
- const i = e % WINDOW_BUCKETS;
455
- const ms =
456
- i >= 0 && o.ringStamps[i] === e ? Math.round(o.ring[i] * 10) / 10 : 0;
457
- buckets.push(ms);
458
- windowMs += ms;
459
- }
460
- return {
461
- key: o.key,
462
- label: o.label,
463
- api: o.api,
464
- caller: o.caller,
465
- file: o.file,
466
- line: o.line,
467
- frames: o.frames,
468
- promoted: o.promoted,
469
- scheduleCount: o.scheduleCount,
470
- calls: o.calls,
471
- totalMs: Math.round(o.totalMs * 10) / 10,
472
- avgMs: o.calls > 0 ? Math.round((o.totalMs / o.calls) * 100) / 100 : 0,
473
- maxMs: Math.round(o.maxMs * 100) / 100,
474
- windowMs: Math.round(windowMs * 10) / 10,
475
- pctOfBusy:
476
- windowBusy > 0
477
- ? Math.min(100, Math.round((windowMs / windowBusy) * 1000) / 10)
478
- : 0,
479
- lastSeenAgoMs: o.lastSeenAt > 0 ? Math.max(0, nowT - o.lastSeenAt) : 0,
480
- buckets,
481
- };
482
- }
483
-
484
- export function resetOrigins(): void {
485
- origins.clear();
486
- }
487
-
488
- /** Test-only introspection. */
489
- export function debugGetOrigin(key: string): Origin | undefined {
490
- return origins.get(key);
491
- }
492
-
493
- export function originCount(): number {
494
- return origins.size;
495
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * Tier 2 patch: legacy-bridge call-ins. On Paper/legacy-bridge runtimes,
3
- * every native→JS call (touch events via RCTEventEmitter, timer flushes via
4
- * JSTimers.callTimers, module callbacks) enters through
5
- * `__fbBatchedBridge.callFunctionReturnFlushedQueue` /
6
- * `invokeCallbackAndReturnFlushedQueue`. Wrapping them attributes event and
7
- * native-callback cost by module.method.
8
- *
9
- * On New Architecture (bridgeless) these globals don't exist — install
10
- * reports `bridge: false` and the UI banners the coverage gap. Nested
11
- * wrapped work (e.g. our wrapped rAF callbacks running inside a
12
- * callTimers call-in) books exclusively via the frame stack, so this tier
13
- * only ever adds its own un-nested remainder.
14
- */
15
-
16
- import { buoyJsTopRunAs } from "../frameStack";
17
- import { g, isActive, PATCHED_TAG } from "../state";
18
- import type { JsTopTiers } from "../../types";
19
-
20
- export function installBridgePatch(): Partial<JsTopTiers> {
21
- const bridge = g.__fbBatchedBridge;
22
- if (!bridge) return { bridge: false };
23
-
24
- let patchedAny = false;
25
-
26
- if (
27
- typeof bridge.callFunctionReturnFlushedQueue === "function" &&
28
- !bridge.callFunctionReturnFlushedQueue[PATCHED_TAG]
29
- ) {
30
- const original = bridge.callFunctionReturnFlushedQueue.bind(bridge);
31
- const buoyJsTopCallFunction = function (
32
- module: string,
33
- method: string,
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- args: any[],
36
- ) {
37
- if (!isActive()) return original(module, method, args);
38
- return buoyJsTopRunAs("bridge", `${module}.${method}`, () =>
39
- original(module, method, args),
40
- );
41
- };
42
- buoyJsTopCallFunction[PATCHED_TAG] = true;
43
- bridge.callFunctionReturnFlushedQueue = buoyJsTopCallFunction;
44
- patchedAny = true;
45
- }
46
-
47
- if (
48
- typeof bridge.invokeCallbackAndReturnFlushedQueue === "function" &&
49
- !bridge.invokeCallbackAndReturnFlushedQueue[PATCHED_TAG]
50
- ) {
51
- const original = bridge.invokeCallbackAndReturnFlushedQueue.bind(bridge);
52
- // Callback ids are per-call and meaningless as identity — one shared
53
- // origin covers all native-module completion callbacks.
54
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
- const buoyJsTopInvokeCallback = function (cbID: number, args: any[]) {
56
- if (!isActive()) return original(cbID, args);
57
- return buoyJsTopRunAs("bridge", "nativeModuleCallback", () =>
58
- original(cbID, args),
59
- );
60
- };
61
- buoyJsTopInvokeCallback[PATCHED_TAG] = true;
62
- bridge.invokeCallbackAndReturnFlushedQueue = buoyJsTopInvokeCallback;
63
- patchedAny = true;
64
- }
65
-
66
- const alreadyPatched =
67
- bridge.callFunctionReturnFlushedQueue?.[PATCHED_TAG] === true;
68
- return { bridge: patchedAny || alreadyPatched };
69
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Tier 1 patch: queueMicrotask. Same install-once contract as timers.ts.
3
- * (Promise reactions are Tier 1.5 — see promises.ts.)
4
- */
5
-
6
- import { buoyJsTopWrapTask } from "../frameStack";
7
- import { captureOriginals, g, PATCHED_TAG } from "../state";
8
-
9
- export function installMicrotaskPatches(): void {
10
- const originals = captureOriginals();
11
- if (
12
- typeof g.queueMicrotask === "function" &&
13
- !g.queueMicrotask[PATCHED_TAG] &&
14
- originals.queueMicrotask
15
- ) {
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- const buoyJsTopQueueMicrotask = function (fn: any) {
18
- const cb = buoyJsTopWrapTask(fn, "microtask");
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- return (originals.queueMicrotask as any)(cb);
21
- };
22
- buoyJsTopQueueMicrotask[PATCHED_TAG] = true;
23
- g.queueMicrotask = buoyJsTopQueueMicrotask;
24
- }
25
- }
@@ -1,57 +0,0 @@
1
- /**
2
- * Tier 1.5 patch: Promise reactions via Promise.prototype.then/catch/finally.
3
- *
4
- * All three are patched: in Hermes, catch/finally delegate to then, and the
5
- * WRAPPED_TAG check in wrapTask makes the delegation harmless (a reaction
6
- * wrapped by our catch patch passes through our then patch untouched).
7
- *
8
- * Known, documented gap: native async/await resumption never goes through
9
- * `.then` — async-function bodies land in the "unattributed" row, not here.
10
- *
11
- * The wrapper is passthrough-first: non-function reactions
12
- * (`.then(undefined, fn)`), `this` binding, and return values are preserved
13
- * exactly; when the engine is inactive the original is called with the raw
14
- * arguments.
15
- */
16
-
17
- import { buoyJsTopWrapTask } from "../frameStack";
18
- import { g, PATCHED_TAG } from "../state";
19
- import type { JsTopTiers } from "../../types";
20
-
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- type AnyFn = (...args: any[]) => any;
23
-
24
- function patchMethod(
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- proto: any,
27
- name: "then" | "catch" | "finally",
28
- ): boolean {
29
- const original: AnyFn | undefined = proto[name];
30
- if (typeof original !== "function") return false;
31
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
32
- if ((original as any)[PATCHED_TAG]) return true;
33
-
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- const patched = function (this: any, ...reactions: any[]) {
36
- for (let i = 0; i < reactions.length; i++) {
37
- if (typeof reactions[i] === "function") {
38
- reactions[i] = buoyJsTopWrapTask(reactions[i], "then");
39
- }
40
- }
41
- return original.apply(this, reactions);
42
- };
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- (patched as any)[PATCHED_TAG] = true;
45
- Object.defineProperty(patched, "name", { value: name, configurable: true });
46
- proto[name] = patched;
47
- return true;
48
- }
49
-
50
- export function installPromisePatches(): Partial<JsTopTiers> {
51
- const proto = g.Promise?.prototype;
52
- if (!proto) return { promises: false };
53
- const ok = patchMethod(proto, "then");
54
- patchMethod(proto, "catch");
55
- patchMethod(proto, "finally");
56
- return { promises: ok };
57
- }