nuxt-devtools-observatory 0.1.17 → 0.1.19

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/module.mjs CHANGED
@@ -5,17 +5,19 @@ import _generate from '@babel/generator';
5
5
  import * as t from '@babel/types';
6
6
 
7
7
  function extractScriptBlock(code) {
8
- const openTagRE = /<script(\s[^>]*)?>/i;
9
- const openMatch = openTagRE.exec(code);
10
- if (!openMatch) {
11
- return null;
12
- }
13
- const start = openMatch.index + openMatch[0].length;
14
- const end = code.indexOf("<\/script>", start);
15
- if (end === -1) {
8
+ try {
9
+ const { parse } = require("@vue/compiler-sfc");
10
+ const { descriptor } = parse(code, { ignoreEmpty: false });
11
+ const block = descriptor.scriptSetup ?? descriptor.script ?? null;
12
+ if (!block) {
13
+ return null;
14
+ }
15
+ const start = block.loc.start.offset;
16
+ const end = block.loc.end.offset;
17
+ return { content: block.content, start, end };
18
+ } catch {
16
19
  return null;
17
20
  }
18
- return { content: code.slice(start, end), start, end };
19
21
  }
20
22
 
21
23
  const traverse$2 = _traverse.default ?? _traverse;
@@ -90,9 +92,7 @@ function fetchInstrumentPlugin() {
90
92
  handlerArg = getExpr(args[1]);
91
93
  optsArg = getExpr(args[2]) ?? t.objectExpression([]);
92
94
  } else {
93
- keyArg = getExpr(args[0]) ?? t.stringLiteral("");
94
- optsArg = getExpr(args[1]) ?? t.objectExpression([]);
95
- handlerArg = void 0;
95
+ return;
96
96
  }
97
97
  } else {
98
98
  keyArg = getExpr(args[0]) ?? t.stringLiteral("");
@@ -121,39 +121,41 @@ function fetchInstrumentPlugin() {
121
121
  t.objectProperty(t.identifier("originalFn"), t.stringLiteral(originalName))
122
122
  ]);
123
123
  if ((originalName === "useAsyncData" || originalName === "useLazyAsyncData") && handlerArg) {
124
- if (handlerArg) {
125
- const wrappedHandler = t.arrowFunctionExpression(
126
- [t.restElement(t.identifier("args"))],
127
- t.conditionalExpression(
128
- t.logicalExpression(
129
- "&&",
130
- t.memberExpression(t.identifier("process"), t.identifier("dev")),
131
- t.memberExpression(t.identifier("process"), t.identifier("client"))
132
- ),
133
- t.callExpression(
134
- t.callExpression(t.identifier("__devFetchHandler"), [handlerArg, keyArg ?? t.stringLiteral(key), meta]),
135
- [t.spreadElement(t.identifier("args"))]
136
- ),
137
- t.callExpression(handlerArg, [t.spreadElement(t.identifier("args"))])
138
- )
139
- );
140
- wrappedHandler.__observatoryTransformed = true;
141
- needsFetchHandlerHelper = true;
142
- if (keyArg) {
143
- const newCall = t.callExpression(t.identifier(originalName), [
144
- keyArg,
145
- wrappedHandler,
146
- optsArg ?? t.objectExpression([])
147
- ]);
148
- newCall.__observatoryTransformed = true;
149
- path.replaceWith(newCall);
150
- } else {
151
- const newCall = t.callExpression(t.identifier(originalName), [wrappedHandler]);
152
- newCall.__observatoryTransformed = true;
153
- path.replaceWith(newCall);
154
- }
155
- modified = true;
124
+ const wrappedHandler = t.arrowFunctionExpression(
125
+ [t.restElement(t.identifier("args"))],
126
+ t.conditionalExpression(
127
+ t.logicalExpression(
128
+ "&&",
129
+ t.memberExpression(t.identifier("process"), t.identifier("dev")),
130
+ t.memberExpression(t.identifier("process"), t.identifier("client"))
131
+ ),
132
+ t.callExpression(
133
+ t.callExpression(t.identifier("__devFetchHandler"), [
134
+ handlerArg,
135
+ keyArg ?? t.stringLiteral(key),
136
+ meta
137
+ ]),
138
+ [t.spreadElement(t.identifier("args"))]
139
+ ),
140
+ t.callExpression(handlerArg, [t.spreadElement(t.identifier("args"))])
141
+ )
142
+ );
143
+ wrappedHandler.__observatoryTransformed = true;
144
+ needsFetchHandlerHelper = true;
145
+ if (keyArg) {
146
+ const newCall = t.callExpression(t.identifier(originalName), [
147
+ keyArg,
148
+ wrappedHandler,
149
+ optsArg ?? t.objectExpression([])
150
+ ]);
151
+ newCall.__observatoryTransformed = true;
152
+ path.replaceWith(newCall);
153
+ } else {
154
+ const newCall = t.callExpression(t.identifier(originalName), [wrappedHandler]);
155
+ newCall.__observatoryTransformed = true;
156
+ path.replaceWith(newCall);
156
157
  }
158
+ modified = true;
157
159
  } else {
158
160
  const newCall = t.callExpression(t.identifier("__devFetchCall"), [
159
161
  t.identifier(originalName),
@@ -244,8 +246,9 @@ function provideInjectPlugin() {
244
246
  }
245
247
  const args = path.node.arguments;
246
248
  const loc = path.node.loc;
249
+ const fileName = id.split(/[\\/]/).pop() || id;
247
250
  const meta = t.objectExpression([
248
- t.objectProperty(t.identifier("file"), t.stringLiteral(id.split("/").pop() ?? id)),
251
+ t.objectProperty(t.identifier("file"), t.stringLiteral(fileName)),
249
252
  t.objectProperty(t.identifier("line"), t.numericLiteral(loc?.start.line ?? 0))
250
253
  ]);
251
254
  if (name === "provide") {
@@ -416,6 +419,11 @@ function _obsMergeHook(original, fn) {
416
419
  return function(el) { fn(el); if (original) original(el) }
417
420
  }
418
421
 
422
+ // Monotonically increasing counter used to make transition IDs unique even
423
+ // when multiple transitions fire within the same performance.now() millisecond
424
+ // (e.g. rapid-toggle stress tests, or simultaneous enter + leave on a swap).
425
+ let _obsSeq = 0
426
+
419
427
  const _ObservedTransition = _obsDefineComponent({
420
428
  name: 'Transition',
421
429
  inheritAttrs: false,
@@ -442,7 +450,7 @@ const _ObservedTransition = _obsDefineComponent({
442
450
  const hookedAttrs = Object.assign({}, attrs, {
443
451
  onBeforeEnter: _obsMergeHook(attrs.onBeforeEnter, function() {
444
452
  const t = performance.now()
445
- const id = transitionName + '::enter::' + t
453
+ const id = transitionName + '::enter::' + t + '::' + (++_obsSeq)
446
454
  enterEntryId = id
447
455
  r.register({ id, transitionName, parentComponent, direction: 'enter', phase: 'entering', startTime: t, cancelled: false, appear: isAppear, mode })
448
456
  }),
@@ -454,7 +462,7 @@ const _ObservedTransition = _obsDefineComponent({
454
462
  }),
455
463
  onBeforeLeave: _obsMergeHook(attrs.onBeforeLeave, function() {
456
464
  const t = performance.now()
457
- const id = transitionName + '::leave::' + t
465
+ const id = transitionName + '::leave::' + t + '::' + (++_obsSeq)
458
466
  leaveEntryId = id
459
467
  r.register({ id, transitionName, parentComponent, direction: 'leave', phase: 'leaving', startTime: t, cancelled: false, appear: false, mode })
460
468
  }),
@@ -516,7 +524,14 @@ const module$1 = defineNuxtModule({
516
524
  composableTracker: true,
517
525
  renderHeatmap: true,
518
526
  transitionTracker: true,
519
- heatmapThreshold: 5
527
+ heatmapThresholdCount: process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3,
528
+ heatmapThresholdTime: process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600,
529
+ maxFetchEntries: process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200,
530
+ maxPayloadBytes: process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4,
531
+ maxTransitions: process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500,
532
+ maxComposableHistory: process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50,
533
+ maxComposableEntries: process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300,
534
+ maxRenderTimeline: process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100
520
535
  },
521
536
  setup(options, nuxt) {
522
537
  if (!nuxt.options.dev) {
@@ -526,6 +541,21 @@ const module$1 = defineNuxtModule({
526
541
  process.env.LAUNCH_EDITOR = "code";
527
542
  }
528
543
  const resolver = createResolver(import.meta.url);
544
+ const resolved = {
545
+ fetchDashboard: options.fetchDashboard ?? (process.env.OBSERVATORY_FETCH_DASHBOARD ? process.env.OBSERVATORY_FETCH_DASHBOARD === "true" : true),
546
+ provideInjectGraph: options.provideInjectGraph ?? (process.env.OBSERVATORY_PROVIDE_INJECT_GRAPH ? process.env.OBSERVATORY_PROVIDE_INJECT_GRAPH === "true" : true),
547
+ composableTracker: options.composableTracker ?? (process.env.OBSERVATORY_COMPOSABLE_TRACKER ? process.env.OBSERVATORY_COMPOSABLE_TRACKER === "true" : true),
548
+ renderHeatmap: options.renderHeatmap ?? (process.env.OBSERVATORY_RENDER_HEATMAP ? process.env.OBSERVATORY_RENDER_HEATMAP === "true" : true),
549
+ transitionTracker: options.transitionTracker ?? (process.env.OBSERVATORY_TRANSITION_TRACKER ? process.env.OBSERVATORY_TRANSITION_TRACKER === "true" : true),
550
+ heatmapThresholdCount: options.heatmapThresholdCount ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_COUNT) : 3),
551
+ heatmapThresholdTime: options.heatmapThresholdTime ?? (process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME ? Number(process.env.OBSERVATORY_HEATMAP_THRESHOLD_TIME) : 1600),
552
+ maxFetchEntries: options.maxFetchEntries ?? (process.env.OBSERVATORY_MAX_FETCH_ENTRIES ? Number(process.env.OBSERVATORY_MAX_FETCH_ENTRIES) : 200),
553
+ maxPayloadBytes: options.maxPayloadBytes ?? (process.env.OBSERVATORY_MAX_PAYLOAD_BYTES ? Number(process.env.OBSERVATORY_MAX_PAYLOAD_BYTES) : 1e4),
554
+ maxTransitions: options.maxTransitions ?? (process.env.OBSERVATORY_MAX_TRANSITIONS ? Number(process.env.OBSERVATORY_MAX_TRANSITIONS) : 500),
555
+ maxComposableHistory: options.maxComposableHistory ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50),
556
+ maxComposableEntries: options.maxComposableEntries ?? (process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300),
557
+ maxRenderTimeline: options.maxRenderTimeline ?? (process.env.OBSERVATORY_MAX_RENDER_TIMELINE ? Number(process.env.OBSERVATORY_MAX_RENDER_TIMELINE) : 100)
558
+ };
529
559
  nuxt.hook("vite:extendConfig", (config) => {
530
560
  const alias = config.resolve?.alias;
531
561
  const aliases = Array.isArray(alias) ? {} : alias ?? {};
@@ -536,33 +566,37 @@ const module$1 = defineNuxtModule({
536
566
  aliases["nuxt-devtools-observatory/runtime/fetch-registry"] = resolver.resolve("./runtime/composables/fetch-registry");
537
567
  config.resolve = { ...config.resolve, alias: aliases };
538
568
  });
539
- if (options.fetchDashboard) {
569
+ if (resolved.fetchDashboard) {
540
570
  addVitePlugin(fetchInstrumentPlugin());
541
571
  }
542
- if (options.provideInjectGraph) {
572
+ if (resolved.provideInjectGraph) {
543
573
  addVitePlugin(provideInjectPlugin());
544
574
  }
545
- if (options.composableTracker) {
575
+ if (resolved.composableTracker) {
546
576
  addVitePlugin(composableTrackerPlugin());
547
577
  }
548
- if (options.transitionTracker) {
578
+ if (resolved.transitionTracker) {
549
579
  addVitePlugin(transitionTrackerPlugin());
550
580
  }
551
- if (options.fetchDashboard || options.provideInjectGraph || options.composableTracker || options.renderHeatmap || options.transitionTracker) {
581
+ if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
552
582
  addPlugin(resolver.resolve("./runtime/plugin"));
553
583
  }
554
- if (options.fetchDashboard) {
584
+ if (resolved.fetchDashboard) {
555
585
  addServerPlugin(resolver.resolve("./runtime/nitro/fetch-capture"));
556
586
  }
557
587
  const CLIENT_PORT = 4949;
558
588
  const clientOrigin = `http://localhost:${CLIENT_PORT}`;
589
+ let innerServer = null;
559
590
  nuxt.hook("vite:serverCreated", async (_viteServer, env) => {
560
591
  if (!env.isClient) {
561
592
  return;
562
593
  }
594
+ if (innerServer) {
595
+ return;
596
+ }
563
597
  const { createServer } = await import('vite');
564
598
  const { default: vue } = await import('@vitejs/plugin-vue');
565
- const inner = await createServer({
599
+ innerServer = await createServer({
566
600
  root: resolver.resolve("../client"),
567
601
  base: "/",
568
602
  server: { port: CLIENT_PORT, strictPort: true, cors: true },
@@ -571,60 +605,45 @@ const module$1 = defineNuxtModule({
571
605
  plugins: [vue()],
572
606
  logLevel: "warn"
573
607
  });
574
- await inner.listen();
575
- nuxt.hook("close", () => inner.close());
608
+ await innerServer.listen();
609
+ nuxt.hook("close", () => {
610
+ innerServer?.close();
611
+ innerServer = null;
612
+ });
576
613
  });
577
614
  const base = clientOrigin;
578
- nuxt.hook("devtools:customTabs", (tabs) => {
579
- if (options.fetchDashboard) {
580
- tabs.push({
581
- name: "observatory-fetch",
582
- title: "useFetch",
583
- icon: "carbon:radio-button",
584
- view: { type: "iframe", src: `${base}/fetch` }
585
- });
586
- }
587
- if (options.provideInjectGraph) {
588
- tabs.push({
589
- name: "observatory-provide",
590
- title: "provide/inject",
591
- icon: "carbon:branch",
592
- view: { type: "iframe", src: `${base}/provide` }
593
- });
594
- }
595
- if (options.composableTracker) {
596
- tabs.push({
597
- name: "observatory-composables",
598
- title: "Composables",
599
- icon: "carbon:function",
600
- view: { type: "iframe", src: `${base}/composables` }
601
- });
615
+ nuxt.hook("render:response", (response, { url }) => {
616
+ if (url.startsWith("/trackers") || url === "/" || url.startsWith("/index.html")) {
617
+ const configScript = `<script>window.__observatoryConfig = ${JSON.stringify(nuxt.options.runtimeConfig.public.observatory)};<\/script>`;
618
+ response.body = response.body.replace("<head>", `<head>
619
+ ${configScript}`);
602
620
  }
603
- if (options.renderHeatmap) {
621
+ });
622
+ nuxt.hook("devtools:customTabs", (tabs) => {
623
+ if (resolved.fetchDashboard || resolved.provideInjectGraph || resolved.composableTracker || resolved.renderHeatmap || resolved.transitionTracker) {
604
624
  tabs.push({
605
- name: "observatory-heatmap",
606
- title: "Heatmap",
625
+ name: "observatory-trackers",
626
+ title: "Observatory Trackers",
607
627
  icon: "carbon:heat-map",
608
- view: { type: "iframe", src: `${base}/heatmap` }
609
- });
610
- }
611
- if (options.transitionTracker) {
612
- tabs.push({
613
- name: "observatory-transitions",
614
- title: "Transitions",
615
- icon: "carbon:movement",
616
- view: { type: "iframe", src: `${base}/transitions` }
628
+ view: { type: "iframe", src: `${base}/trackers` }
617
629
  });
618
630
  }
619
631
  });
620
632
  nuxt.options.runtimeConfig.public.observatory = {
621
- heatmapThreshold: options.heatmapThreshold ?? 5,
633
+ heatmapThresholdCount: resolved.heatmapThresholdCount,
634
+ heatmapThresholdTime: resolved.heatmapThresholdTime,
622
635
  clientOrigin,
623
- fetchDashboard: options.fetchDashboard,
624
- provideInjectGraph: options.provideInjectGraph,
625
- composableTracker: options.composableTracker,
626
- renderHeatmap: options.renderHeatmap,
627
- transitionTracker: options.transitionTracker
636
+ fetchDashboard: resolved.fetchDashboard,
637
+ provideInjectGraph: resolved.provideInjectGraph,
638
+ composableTracker: resolved.composableTracker,
639
+ renderHeatmap: resolved.renderHeatmap,
640
+ transitionTracker: resolved.transitionTracker,
641
+ maxFetchEntries: resolved.maxFetchEntries,
642
+ maxPayloadBytes: resolved.maxPayloadBytes,
643
+ maxTransitions: resolved.maxTransitions,
644
+ maxComposableHistory: resolved.maxComposableHistory,
645
+ maxComposableEntries: resolved.maxComposableEntries,
646
+ maxRenderTimeline: resolved.maxRenderTimeline
628
647
  };
629
648
  }
630
649
  });
@@ -41,7 +41,20 @@ export interface ComposableEntry {
41
41
  * - `register`: Registers a new composable entry.
42
42
  * - `update`: Updates an existing composable entry.
43
43
  * - `getAll`: Retrieves all composable entries.
44
- * @returns {{ register: (entry: ComposableEntry) => void, update: (id: string, patch: Partial<ComposableEntry>) => void, getAll: () => ComposableEntry[] }} An object with `register`, `update`, and `getAll` methods.
44
+ * - `getSnapshot`: Returns a cached pre-serialized JSON string, rebuilt only when dirty.
45
+ * @returns {{
46
+ * register: (entry: ComposableEntry) => void,
47
+ * registerLiveRefs: (id: string, refs: Record<string, import('vue').Ref<unknown>>) => void,
48
+ * registerRawRefs: (id: string, refs: Record<string, unknown>) => void,
49
+ * onComposableChange: (cb: () => void) => void,
50
+ * clear: () => void,
51
+ * setRoute: (path: string) => void,
52
+ * getRoute: () => string,
53
+ * update: (id: string, patch: Partial<ComposableEntry>) => void,
54
+ * getAll: () => ComposableEntry[],
55
+ * getSnapshot: () => string,
56
+ * editValue: (id: string, key: string, value: unknown) => void
57
+ * }} An object with `register`, `update`, `getAll`, `getSnapshot`, and related methods.
45
58
  */
46
59
  export declare function setupComposableRegistry(): {
47
60
  register: (entry: ComposableEntry) => void;
@@ -53,6 +66,7 @@ export declare function setupComposableRegistry(): {
53
66
  getRoute: () => string;
54
67
  update: (id: string, patch: Partial<ComposableEntry>) => void;
55
68
  getAll: () => ComposableEntry[];
69
+ getSnapshot: () => string;
56
70
  editValue: (id: string, key: string, value: unknown) => void;
57
71
  };
58
72
  export declare function __trackComposable<T>(name: string, callFn: () => T, meta: {
@@ -1,33 +1,68 @@
1
- import { ref, isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
1
+ import { isRef, isReactive, isReadonly, unref, computed, watchEffect, getCurrentInstance, onUnmounted } from "vue";
2
2
  export function setupComposableRegistry() {
3
- const entries = ref(/* @__PURE__ */ new Map());
3
+ const entries = /* @__PURE__ */ new Map();
4
4
  const liveRefs = /* @__PURE__ */ new Map();
5
5
  const liveRefWatchers = /* @__PURE__ */ new Map();
6
- const MAX_HISTORY = 50;
6
+ const MAX_HISTORY = typeof process !== "undefined" && process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_HISTORY) : 50;
7
+ const MAX_COMPOSABLE_ENTRIES = typeof process !== "undefined" && process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES ? Number(process.env.OBSERVATORY_MAX_COMPOSABLE_ENTRIES) : 300;
7
8
  const entryHistory = /* @__PURE__ */ new Map();
8
9
  const prevValues = /* @__PURE__ */ new Map();
9
10
  const rawRefs = /* @__PURE__ */ new Map();
10
- function computeSharedKeys(id, name) {
11
- const ownRaw = rawRefs.get(id);
12
- if (!ownRaw) {
13
- return [];
11
+ const sharedKeysCache = /* @__PURE__ */ new Map();
12
+ let dirty = true;
13
+ let cachedSnapshot = "[]";
14
+ function markDirty() {
15
+ dirty = true;
16
+ }
17
+ function invalidateSharedKeysForName(name) {
18
+ sharedKeysCache.delete(name);
19
+ markDirty();
20
+ }
21
+ function deleteEntry(entryId, entryName) {
22
+ const stop = liveRefWatchers.get(entryId);
23
+ if (stop) {
24
+ stop();
25
+ liveRefWatchers.delete(entryId);
14
26
  }
15
- const shared = /* @__PURE__ */ new Set();
16
- for (const [otherId, entry] of entries.value.entries()) {
17
- if (otherId === id || entry.name !== name) {
18
- continue;
19
- }
20
- const otherRaw = rawRefs.get(otherId);
21
- if (!otherRaw) {
27
+ liveRefs.delete(entryId);
28
+ rawRefs.delete(entryId);
29
+ prevValues.delete(entryId);
30
+ entryHistory.delete(entryId);
31
+ entries.delete(entryId);
32
+ invalidateSharedKeysForName(entryName);
33
+ }
34
+ function getSharedKeys(id, name) {
35
+ let nameCache = sharedKeysCache.get(name);
36
+ if (nameCache && nameCache.has(id)) {
37
+ return nameCache.get(id);
38
+ }
39
+ nameCache = /* @__PURE__ */ new Map();
40
+ sharedKeysCache.set(name, nameCache);
41
+ const peers = [...entries.entries()].filter(([, e]) => e.name === name);
42
+ for (const [eid] of peers) {
43
+ const ownRaw = rawRefs.get(eid);
44
+ if (!ownRaw) {
45
+ nameCache.set(eid, []);
22
46
  continue;
23
47
  }
24
- for (const [key, obj] of Object.entries(ownRaw)) {
25
- if (key in otherRaw && otherRaw[key] === obj) {
26
- shared.add(key);
48
+ const shared = /* @__PURE__ */ new Set();
49
+ for (const [otherId] of peers) {
50
+ if (otherId === eid) {
51
+ continue;
52
+ }
53
+ const otherRaw = rawRefs.get(otherId);
54
+ if (!otherRaw) {
55
+ continue;
56
+ }
57
+ for (const [key, obj] of Object.entries(ownRaw)) {
58
+ if (key in otherRaw && otherRaw[key] === obj) {
59
+ shared.add(key);
60
+ }
27
61
  }
28
62
  }
63
+ nameCache.set(eid, [...shared]);
29
64
  }
30
- return [...shared];
65
+ return nameCache.get(id) ?? [];
31
66
  }
32
67
  let currentRoute = "/";
33
68
  function setRoute(path) {
@@ -37,7 +72,17 @@ export function setupComposableRegistry() {
37
72
  return currentRoute;
38
73
  }
39
74
  function register(entry) {
40
- entries.value.set(entry.id, entry);
75
+ if (entries.size >= MAX_COMPOSABLE_ENTRIES) {
76
+ const unmountedId = [...entries.entries()].find(([, e]) => e.status === "unmounted")?.[0];
77
+ const evictId = unmountedId ?? entries.keys().next().value;
78
+ if (evictId !== void 0) {
79
+ const evictName = entries.get(evictId)?.name ?? "";
80
+ deleteEntry(evictId, evictName);
81
+ }
82
+ }
83
+ entries.set(entry.id, entry);
84
+ invalidateSharedKeysForName(entry.name);
85
+ markDirty();
41
86
  emit("composable:register", entry);
42
87
  }
43
88
  function registerLiveRefs(id, refs) {
@@ -53,54 +98,67 @@ export function setupComposableRegistry() {
53
98
  return;
54
99
  }
55
100
  liveRefs.set(id, refs);
56
- prevValues.set(
57
- id,
58
- Object.fromEntries(
59
- Object.entries(refs).map(([k, r]) => {
60
- try {
61
- return [k, JSON.stringify(unref(r)) ?? ""];
62
- } catch {
63
- return [k, ""];
64
- }
65
- })
66
- )
67
- );
68
- const stop = watchEffect(() => {
69
- const prev = prevValues.get(id) ?? {};
70
- const now = {};
71
- const t = typeof performance !== "undefined" ? performance.now() : Date.now();
72
- for (const [k, r] of Object.entries(refs)) {
101
+ const stopFns = [];
102
+ for (const [k, r] of Object.entries(refs)) {
103
+ if (!prevValues.has(id)) {
104
+ prevValues.set(id, {});
105
+ }
106
+ try {
107
+ prevValues.get(id)[k] = JSON.stringify(unref(r)) ?? "";
108
+ } catch {
109
+ prevValues.get(id)[k] = "";
110
+ }
111
+ const stopK = watchEffect(() => {
73
112
  const val = unref(r);
74
- const serialised = JSON.stringify(val) ?? "";
75
- now[k] = serialised;
76
- if (serialised !== prev[k]) {
113
+ let serialised = "";
114
+ try {
115
+ serialised = JSON.stringify(val) ?? "";
116
+ } catch {
117
+ }
118
+ const prev = prevValues.get(id);
119
+ if (prev && serialised !== prev[k]) {
120
+ const t = typeof performance !== "undefined" ? performance.now() : Date.now();
77
121
  const history = entryHistory.get(id) ?? [];
78
122
  history.push({ t, key: k, value: safeValue(val) });
79
123
  if (history.length > MAX_HISTORY) {
80
124
  history.shift();
81
125
  }
82
126
  entryHistory.set(id, history);
127
+ prev[k] = serialised;
128
+ markDirty();
129
+ _scheduleOnChange();
83
130
  }
84
- }
85
- prevValues.set(id, now);
86
- _onChange?.();
87
- });
131
+ });
132
+ stopFns.push(stopK);
133
+ }
134
+ const stop = () => stopFns.forEach((s) => s());
88
135
  liveRefWatchers.set(id, stop);
89
136
  }
90
137
  function registerRawRefs(id, refs) {
91
138
  rawRefs.set(id, refs);
92
139
  }
93
140
  let _onChange = null;
141
+ let _pendingFrame = null;
142
+ function _scheduleOnChange() {
143
+ if (_pendingFrame !== null) {
144
+ return;
145
+ }
146
+ _pendingFrame = requestAnimationFrame(() => {
147
+ _pendingFrame = null;
148
+ _onChange?.();
149
+ });
150
+ }
94
151
  function onComposableChange(cb) {
95
152
  _onChange = cb;
96
153
  }
97
154
  function update(id, patch) {
98
- const existing = entries.value.get(id);
155
+ const existing = entries.get(id);
99
156
  if (!existing) {
100
157
  return;
101
158
  }
102
159
  const updated = { ...existing, ...patch };
103
- entries.value.set(id, updated);
160
+ entries.set(id, updated);
161
+ markDirty();
104
162
  emit("composable:update", updated);
105
163
  }
106
164
  function safeValue(val) {
@@ -149,7 +207,7 @@ export function setupComposableRegistry() {
149
207
  leakReason: entry.leakReason,
150
208
  refs: freshRefs,
151
209
  history: entryHistory.get(entry.id) ?? [],
152
- sharedKeys: computeSharedKeys(entry.id, entry.name),
210
+ sharedKeys: getSharedKeys(entry.id, entry.name),
153
211
  watcherCount: entry.watcherCount,
154
212
  intervalCount: entry.intervalCount,
155
213
  lifecycle: entry.lifecycle,
@@ -159,7 +217,19 @@ export function setupComposableRegistry() {
159
217
  };
160
218
  }
161
219
  function getAll() {
162
- return [...entries.value.values()].map(sanitize);
220
+ return [...entries.values()].map(sanitize);
221
+ }
222
+ function getSnapshot() {
223
+ if (!dirty) {
224
+ return cachedSnapshot;
225
+ }
226
+ try {
227
+ cachedSnapshot = JSON.stringify([...entries.values()].map(sanitize)) ?? "[]";
228
+ } catch {
229
+ cachedSnapshot = "[]";
230
+ }
231
+ dirty = false;
232
+ return cachedSnapshot;
163
233
  }
164
234
  function emit(event, data) {
165
235
  if (!import.meta.client) {
@@ -169,13 +239,19 @@ export function setupComposableRegistry() {
169
239
  channel?.send(event, data);
170
240
  }
171
241
  function clear() {
242
+ if (_pendingFrame !== null) {
243
+ cancelAnimationFrame(_pendingFrame);
244
+ _pendingFrame = null;
245
+ }
172
246
  for (const stop of liveRefWatchers.values()) stop();
173
247
  liveRefWatchers.clear();
174
248
  liveRefs.clear();
175
249
  rawRefs.clear();
176
250
  prevValues.clear();
177
251
  entryHistory.clear();
178
- entries.value.clear();
252
+ sharedKeysCache.clear();
253
+ entries.clear();
254
+ markDirty();
179
255
  emit("composable:clear", {});
180
256
  }
181
257
  function editValue(id, key, value) {
@@ -187,7 +263,7 @@ export function setupComposableRegistry() {
187
263
  if (!r) {
188
264
  return;
189
265
  }
190
- const entry = entries.value.get(id);
266
+ const entry = entries.get(id);
191
267
  if (!entry) {
192
268
  return;
193
269
  }
@@ -196,7 +272,19 @@ export function setupComposableRegistry() {
196
272
  }
197
273
  r.value = value;
198
274
  }
199
- return { register, registerLiveRefs, registerRawRefs, onComposableChange, clear, setRoute, getRoute, update, getAll, editValue };
275
+ return {
276
+ register,
277
+ registerLiveRefs,
278
+ registerRawRefs,
279
+ onComposableChange,
280
+ clear,
281
+ setRoute,
282
+ getRoute,
283
+ update,
284
+ getAll,
285
+ getSnapshot,
286
+ editValue
287
+ };
200
288
  }
201
289
  export function __trackComposable(name, callFn, meta) {
202
290
  if (!import.meta.dev) {
@@ -286,8 +374,10 @@ export function __trackComposable(name, callFn, meta) {
286
374
  route: registry.getRoute()
287
375
  };
288
376
  registry.register(entry);
289
- registry.registerLiveRefs(id, liveRefMap);
290
- registry.registerRawRefs(id, rawRefMap);
377
+ if (instance) {
378
+ registry.registerLiveRefs(id, liveRefMap);
379
+ registry.registerRawRefs(id, rawRefMap);
380
+ }
291
381
  if (instance) {
292
382
  onUnmounted(() => {
293
383
  const leakedWatchers = trackedWatchers.filter((w) => w.effect.active);