decantr 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module devtools
|
|
3
|
+
* Development-only reactive debugging tools for the Decantr framework.
|
|
4
|
+
*
|
|
5
|
+
* All exports are no-ops when `globalThis.__DECANTR_DEVTOOLS__` is not set,
|
|
6
|
+
* ensuring zero production cost. Call `enableDevTools()` to activate.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { enableDevTools, inspectSignal, label, snapshot, restore } from './devtools.js';
|
|
10
|
+
* import { createSignal, createEffect } from './index.js';
|
|
11
|
+
*
|
|
12
|
+
* enableDevTools();
|
|
13
|
+
* const [count, setCount] = createSignal(0);
|
|
14
|
+
* label(count, 'count');
|
|
15
|
+
* console.log(inspectSignal(count));
|
|
16
|
+
* // { value: 0, subscriberCount: 0, id: 1, label: 'count' }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createSignal, createEffect, createMemo } from './index.js';
|
|
20
|
+
|
|
21
|
+
// ─── Internal State ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Metadata for reactive nodes (signal getters, effect disposers, memo getters).
|
|
25
|
+
* WeakMap keyed on the public-facing function so entries don't prevent GC.
|
|
26
|
+
* @type {WeakMap<Function, { id: number, label: string, type: string }>}
|
|
27
|
+
*/
|
|
28
|
+
const _meta = new WeakMap();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Strong references for iteration (WeakMap is not enumerable).
|
|
32
|
+
* @type {Map<number, { ref: Function, type: string, effectObj?: object }>}
|
|
33
|
+
*/
|
|
34
|
+
const _nodes = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reverse lookup: internal effect/memo object -> node id.
|
|
38
|
+
* Used by getReactiveGraph() to resolve subscriber-set members back to nodes.
|
|
39
|
+
* @type {WeakMap<object, number>}
|
|
40
|
+
*/
|
|
41
|
+
const _effectToNodeId = new WeakMap();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Signal getter -> setter, so restore() can write values back.
|
|
45
|
+
* @type {WeakMap<Function, Function>}
|
|
46
|
+
*/
|
|
47
|
+
const _setters = new WeakMap();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stored snapshots keyed by string id.
|
|
51
|
+
* @type {Map<string, { id: string, timestamp: number, values: Map<string, any> }>}
|
|
52
|
+
*/
|
|
53
|
+
const _snapshots = new Map();
|
|
54
|
+
|
|
55
|
+
/** Auto-incrementing node id counter. */
|
|
56
|
+
let _nextId = 1;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Circular buffer for execution traces.
|
|
60
|
+
* @type {Array<TraceEntry>}
|
|
61
|
+
*/
|
|
62
|
+
let _traceBuffer = [];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @typedef {{ id: number, label: string, type: string, timestamp: number, duration: number, trigger: string|null }} TraceEntry
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/** Write cursor in the circular buffer. */
|
|
69
|
+
let _traceIndex = 0;
|
|
70
|
+
|
|
71
|
+
/** Total trace entries ever recorded (may exceed buffer capacity). */
|
|
72
|
+
let _traceTotal = 0;
|
|
73
|
+
|
|
74
|
+
/** Maximum trace history entries. */
|
|
75
|
+
let _maxTraceEntries = 1000;
|
|
76
|
+
|
|
77
|
+
/** Maximum stored snapshots. */
|
|
78
|
+
let _maxSnapshots = 50;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The most recent signal setter that fired, used to attribute effect triggers.
|
|
82
|
+
* @type {{ label: string, id: number }|null}
|
|
83
|
+
*/
|
|
84
|
+
let _lastTrigger = null;
|
|
85
|
+
|
|
86
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check whether devtools are currently active.
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function isActive() {
|
|
93
|
+
return !!globalThis.__DECANTR_DEVTOOLS__;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register a reactive node in both _meta (WeakMap) and _nodes (Map).
|
|
98
|
+
* @param {Function} target - Public-facing getter or dispose function.
|
|
99
|
+
* @param {string} type - 'signal' | 'effect' | 'memo'
|
|
100
|
+
* @param {string} [name] - Optional human-readable label.
|
|
101
|
+
* @param {object} [effectObj] - Internal effect object (for graph edge resolution).
|
|
102
|
+
* @returns {number} Assigned id.
|
|
103
|
+
*/
|
|
104
|
+
function registerNode(target, type, name, effectObj) {
|
|
105
|
+
const id = _nextId++;
|
|
106
|
+
const entry = { id, label: name || `${type}#${id}`, type };
|
|
107
|
+
_meta.set(target, entry);
|
|
108
|
+
const nodeEntry = { ref: target, type };
|
|
109
|
+
if (effectObj) {
|
|
110
|
+
nodeEntry.effectObj = effectObj;
|
|
111
|
+
_effectToNodeId.set(effectObj, id);
|
|
112
|
+
}
|
|
113
|
+
_nodes.set(id, nodeEntry);
|
|
114
|
+
return id;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write a trace entry into the circular buffer.
|
|
119
|
+
* @param {number} id
|
|
120
|
+
* @param {string} lbl
|
|
121
|
+
* @param {string} type
|
|
122
|
+
* @param {number} duration - Execution time in ms.
|
|
123
|
+
* @param {string|null} trigger - Label of the triggering signal.
|
|
124
|
+
*/
|
|
125
|
+
function recordTrace(id, lbl, type, duration, trigger) {
|
|
126
|
+
/** @type {TraceEntry} */
|
|
127
|
+
const entry = { id, label: lbl, type, timestamp: Date.now(), duration, trigger };
|
|
128
|
+
|
|
129
|
+
if (_traceBuffer.length < _maxTraceEntries) {
|
|
130
|
+
_traceBuffer.push(entry);
|
|
131
|
+
} else {
|
|
132
|
+
_traceBuffer[_traceIndex] = entry;
|
|
133
|
+
}
|
|
134
|
+
_traceIndex = (_traceIndex + 1) % _maxTraceEntries;
|
|
135
|
+
_traceTotal++;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read trace entries in chronological order.
|
|
140
|
+
* @param {number} limit - Max entries to return.
|
|
141
|
+
* @returns {TraceEntry[]}
|
|
142
|
+
*/
|
|
143
|
+
function readTrace(limit) {
|
|
144
|
+
const len = _traceBuffer.length;
|
|
145
|
+
const count = Math.min(len, limit);
|
|
146
|
+
if (count === 0) return [];
|
|
147
|
+
|
|
148
|
+
if (len < _maxTraceEntries) {
|
|
149
|
+
// Buffer has not wrapped — entries are already chronological
|
|
150
|
+
return _traceBuffer.slice(len - count);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Buffer has wrapped — _traceIndex points to the oldest slot
|
|
154
|
+
const result = new Array(count);
|
|
155
|
+
const start = _traceIndex; // oldest entry position
|
|
156
|
+
const offset = len - count;
|
|
157
|
+
for (let i = 0; i < count; i++) {
|
|
158
|
+
result[i] = _traceBuffer[(start + offset + i) % len];
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Activate devtools instrumentation.
|
|
167
|
+
*
|
|
168
|
+
* Sets `globalThis.__DECANTR_DEVTOOLS__ = true` and configures limits.
|
|
169
|
+
* After calling this, use the module's `label()`, `registerSignalSetter()`,
|
|
170
|
+
* and the `*Tracked` factory wrappers to register reactive nodes for
|
|
171
|
+
* inspection, tracing, and snapshotting.
|
|
172
|
+
*
|
|
173
|
+
* Safe to call multiple times — subsequent calls update options only.
|
|
174
|
+
*
|
|
175
|
+
* @param {{ maxTraceEntries?: number, maxSnapshots?: number }} [options]
|
|
176
|
+
* @returns {void}
|
|
177
|
+
*/
|
|
178
|
+
export function enableDevTools(options) {
|
|
179
|
+
globalThis.__DECANTR_DEVTOOLS__ = true;
|
|
180
|
+
|
|
181
|
+
if (options) {
|
|
182
|
+
if (typeof options.maxTraceEntries === 'number' && options.maxTraceEntries > 0) {
|
|
183
|
+
_maxTraceEntries = options.maxTraceEntries;
|
|
184
|
+
}
|
|
185
|
+
if (typeof options.maxSnapshots === 'number' && options.maxSnapshots > 0) {
|
|
186
|
+
_maxSnapshots = options.maxSnapshots;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Shrink trace buffer if new limit is smaller
|
|
191
|
+
if (_traceBuffer.length > _maxTraceEntries) {
|
|
192
|
+
_traceBuffer = readTrace(_maxTraceEntries);
|
|
193
|
+
_traceIndex = 0;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Inspect a signal getter and return its current debug state.
|
|
199
|
+
*
|
|
200
|
+
* Also works on memo getters (they expose `_subscribers` the same way).
|
|
201
|
+
* Returns `null` if devtools are not enabled or the function is not a
|
|
202
|
+
* recognized reactive getter.
|
|
203
|
+
*
|
|
204
|
+
* @param {Function} getter - A signal or memo getter.
|
|
205
|
+
* @returns {{ value: any, subscriberCount: number, id: number, label: string }|null}
|
|
206
|
+
*/
|
|
207
|
+
export function inspectSignal(getter) {
|
|
208
|
+
if (!isActive()) return null;
|
|
209
|
+
if (typeof getter !== 'function') return null;
|
|
210
|
+
|
|
211
|
+
const meta = _meta.get(getter);
|
|
212
|
+
const subscribers = getter._subscribers;
|
|
213
|
+
const subscriberCount = subscribers instanceof Set ? subscribers.size : 0;
|
|
214
|
+
|
|
215
|
+
if (meta) {
|
|
216
|
+
return {
|
|
217
|
+
value: getter(),
|
|
218
|
+
subscriberCount,
|
|
219
|
+
id: meta.id,
|
|
220
|
+
label: meta.label
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Unregistered signal — still return useful info if it has _subscribers
|
|
225
|
+
if (subscribers instanceof Set) {
|
|
226
|
+
return {
|
|
227
|
+
value: getter(),
|
|
228
|
+
subscriberCount,
|
|
229
|
+
id: -1,
|
|
230
|
+
label: '(unlabeled)'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Assign a human-readable debug label to a reactive node.
|
|
239
|
+
*
|
|
240
|
+
* Works on signal getters, memo getters, and effect dispose functions.
|
|
241
|
+
* If the node was not previously registered (e.g. created before
|
|
242
|
+
* `enableDevTools()`), it is registered on first `label()` call.
|
|
243
|
+
*
|
|
244
|
+
* @param {Function} target - Signal getter, memo getter, or effect dispose.
|
|
245
|
+
* @param {string} name - Debug label.
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
export function label(target, name) {
|
|
249
|
+
if (!isActive()) return;
|
|
250
|
+
if (typeof target !== 'function' || typeof name !== 'string') return;
|
|
251
|
+
|
|
252
|
+
const existing = _meta.get(target);
|
|
253
|
+
if (existing) {
|
|
254
|
+
existing.label = name;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Infer node type from shape
|
|
259
|
+
let type = 'unknown';
|
|
260
|
+
if (target._subscribers instanceof Set) {
|
|
261
|
+
// Both signals and memos expose _subscribers — distinguish by checking
|
|
262
|
+
// whether there is a setter registered (signals have one, memos don't).
|
|
263
|
+
type = _setters.has(target) ? 'signal' : 'signal';
|
|
264
|
+
// Conservative: default to 'signal' — label() is called before we can
|
|
265
|
+
// reliably distinguish. Callers can use registerSignalSetter to clarify.
|
|
266
|
+
} else if (target.name === 'dispose') {
|
|
267
|
+
type = 'effect';
|
|
268
|
+
}
|
|
269
|
+
registerNode(target, type, name);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Register a signal's setter so `snapshot()`/`restore()` can capture and
|
|
274
|
+
* write values. Also registers the getter as a 'signal' node if not already
|
|
275
|
+
* tracked.
|
|
276
|
+
*
|
|
277
|
+
* @param {Function} getter - Signal getter from `createSignal`.
|
|
278
|
+
* @param {Function} setter - Signal setter from `createSignal`.
|
|
279
|
+
* @returns {void}
|
|
280
|
+
*/
|
|
281
|
+
export function registerSignalSetter(getter, setter) {
|
|
282
|
+
if (!isActive()) return;
|
|
283
|
+
_setters.set(getter, setter);
|
|
284
|
+
// Ensure the getter is registered as a node
|
|
285
|
+
if (!_meta.has(getter)) {
|
|
286
|
+
registerNode(getter, 'signal');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Build the full reactive dependency graph from all registered nodes.
|
|
292
|
+
*
|
|
293
|
+
* Nodes represent signals, effects, and memos. Edges flow from source
|
|
294
|
+
* (signal/memo) to subscriber (effect/memo).
|
|
295
|
+
*
|
|
296
|
+
* Edge resolution works by iterating each signal/memo's `_subscribers` set
|
|
297
|
+
* and matching each internal effect object back to a registered node via
|
|
298
|
+
* `_effectToNodeId`.
|
|
299
|
+
*
|
|
300
|
+
* @returns {{ nodes: Array<{ id: number, type: string, label: string, value: any, level: number }>, edges: Array<{ from: number, to: number }> }|null}
|
|
301
|
+
*/
|
|
302
|
+
export function getReactiveGraph() {
|
|
303
|
+
if (!isActive()) return null;
|
|
304
|
+
|
|
305
|
+
/** @type {Array<{ id: number, type: string, label: string, value: any, level: number }>} */
|
|
306
|
+
const nodes = [];
|
|
307
|
+
/** @type {Array<{ from: number, to: number }>} */
|
|
308
|
+
const edges = [];
|
|
309
|
+
|
|
310
|
+
// Collect all nodes
|
|
311
|
+
for (const [nodeId, entry] of _nodes) {
|
|
312
|
+
const meta = _meta.get(entry.ref);
|
|
313
|
+
if (!meta) continue;
|
|
314
|
+
|
|
315
|
+
let value;
|
|
316
|
+
let level = 0;
|
|
317
|
+
|
|
318
|
+
if (entry.type === 'signal' || entry.type === 'memo') {
|
|
319
|
+
try {
|
|
320
|
+
// Safe to call getter here — no currentEffect is set at module scope
|
|
321
|
+
value = entry.ref();
|
|
322
|
+
} catch (_) {
|
|
323
|
+
value = '<error>';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (entry.effectObj && typeof entry.effectObj.level === 'number') {
|
|
328
|
+
level = entry.effectObj.level;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
nodes.push({ id: nodeId, type: entry.type, label: meta.label, value, level });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build edges: for each signal/memo, walk its subscribers and resolve
|
|
335
|
+
for (const [nodeId, entry] of _nodes) {
|
|
336
|
+
if (entry.type !== 'signal' && entry.type !== 'memo') continue;
|
|
337
|
+
|
|
338
|
+
const subs = entry.ref._subscribers;
|
|
339
|
+
if (!(subs instanceof Set)) continue;
|
|
340
|
+
|
|
341
|
+
for (const subscriber of subs) {
|
|
342
|
+
const targetId = _effectToNodeId.get(subscriber);
|
|
343
|
+
if (targetId !== undefined && targetId !== nodeId) {
|
|
344
|
+
edges.push({ from: nodeId, to: targetId });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return { nodes, edges };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Retrieve the execution trace log.
|
|
354
|
+
*
|
|
355
|
+
* Returns an array of trace entries in chronological order, recording what
|
|
356
|
+
* effects and memos ran, when, how long they took, and what signal triggered
|
|
357
|
+
* each execution.
|
|
358
|
+
*
|
|
359
|
+
* @param {{ limit?: number }} [options]
|
|
360
|
+
* @returns {TraceEntry[]|null} Returns `null` if devtools are not enabled.
|
|
361
|
+
*/
|
|
362
|
+
export function getTrace(options) {
|
|
363
|
+
if (!isActive()) return null;
|
|
364
|
+
|
|
365
|
+
const limit = (options && typeof options.limit === 'number')
|
|
366
|
+
? options.limit
|
|
367
|
+
: _traceBuffer.length;
|
|
368
|
+
return readTrace(Math.max(0, limit));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Capture a snapshot of all labeled signals that have a registered setter.
|
|
373
|
+
*
|
|
374
|
+
* Returns a snapshot object whose `id` can be passed to `restore()` to
|
|
375
|
+
* reinstate the captured values.
|
|
376
|
+
*
|
|
377
|
+
* @returns {{ id: string, timestamp: number, values: Map<string, any> }|null}
|
|
378
|
+
*/
|
|
379
|
+
export function snapshot() {
|
|
380
|
+
if (!isActive()) return null;
|
|
381
|
+
|
|
382
|
+
/** @type {Map<string, any>} */
|
|
383
|
+
const values = new Map();
|
|
384
|
+
|
|
385
|
+
for (const [, entry] of _nodes) {
|
|
386
|
+
if (entry.type !== 'signal') continue;
|
|
387
|
+
if (!_setters.has(entry.ref)) continue;
|
|
388
|
+
|
|
389
|
+
const meta = _meta.get(entry.ref);
|
|
390
|
+
if (!meta) continue;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
values.set(meta.label, entry.ref());
|
|
394
|
+
} catch (_) {
|
|
395
|
+
// Skip signals that throw on read
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const id = `snap_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
400
|
+
const ts = Date.now();
|
|
401
|
+
const snap = { id, timestamp: ts, values };
|
|
402
|
+
|
|
403
|
+
// Enforce max snapshots — evict oldest
|
|
404
|
+
if (_snapshots.size >= _maxSnapshots) {
|
|
405
|
+
const oldest = _snapshots.keys().next().value;
|
|
406
|
+
_snapshots.delete(oldest);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_snapshots.set(id, snap);
|
|
410
|
+
return snap;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Restore a previously captured snapshot by writing all captured signal
|
|
415
|
+
* values back through their registered setters.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} snapshotId - The `id` from a previous `snapshot()` call.
|
|
418
|
+
* @returns {boolean} `true` if at least one signal was restored.
|
|
419
|
+
*/
|
|
420
|
+
export function restore(snapshotId) {
|
|
421
|
+
if (!isActive()) return false;
|
|
422
|
+
if (typeof snapshotId !== 'string') return false;
|
|
423
|
+
|
|
424
|
+
const snap = _snapshots.get(snapshotId);
|
|
425
|
+
if (!snap) return false;
|
|
426
|
+
|
|
427
|
+
// Build label -> setter lookup
|
|
428
|
+
/** @type {Map<string, Function>} */
|
|
429
|
+
const labelToSetter = new Map();
|
|
430
|
+
for (const [, entry] of _nodes) {
|
|
431
|
+
if (entry.type !== 'signal') continue;
|
|
432
|
+
const meta = _meta.get(entry.ref);
|
|
433
|
+
if (!meta) continue;
|
|
434
|
+
const setter = _setters.get(entry.ref);
|
|
435
|
+
if (!setter) continue;
|
|
436
|
+
labelToSetter.set(meta.label, setter);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let restored = false;
|
|
440
|
+
for (const [lbl, value] of snap.values) {
|
|
441
|
+
const setter = labelToSetter.get(lbl);
|
|
442
|
+
if (setter) {
|
|
443
|
+
setter(value);
|
|
444
|
+
restored = true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return restored;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Detect potential memory leaks in the reactive graph.
|
|
452
|
+
*
|
|
453
|
+
* Scans all registered signal/memo subscriber sets for:
|
|
454
|
+
* 1. **Orphaned effects** — subscribers marked `.disposed` but still present
|
|
455
|
+
* in a subscriber set (should have been cleaned up).
|
|
456
|
+
* 2. **Growing subscribers** — subscriber sets whose size exceeds a threshold,
|
|
457
|
+
* which may indicate effects subscribing in a loop without cleanup.
|
|
458
|
+
*
|
|
459
|
+
* @param {{ subscriberThreshold?: number }} [options]
|
|
460
|
+
* @returns {{ orphanedEffects: number, growingSubscribers: Array<{ label: string, count: number }> }|null}
|
|
461
|
+
*/
|
|
462
|
+
export function detectLeaks(options) {
|
|
463
|
+
if (!isActive()) return null;
|
|
464
|
+
|
|
465
|
+
const threshold = (options && typeof options.subscriberThreshold === 'number')
|
|
466
|
+
? options.subscriberThreshold
|
|
467
|
+
: 10;
|
|
468
|
+
|
|
469
|
+
let orphanedEffects = 0;
|
|
470
|
+
/** @type {Array<{ label: string, count: number }>} */
|
|
471
|
+
const growingSubscribers = [];
|
|
472
|
+
|
|
473
|
+
for (const [, entry] of _nodes) {
|
|
474
|
+
if (entry.type !== 'signal' && entry.type !== 'memo') continue;
|
|
475
|
+
|
|
476
|
+
const subs = entry.ref._subscribers;
|
|
477
|
+
if (!(subs instanceof Set)) continue;
|
|
478
|
+
|
|
479
|
+
const meta = _meta.get(entry.ref);
|
|
480
|
+
const lbl = meta ? meta.label : '(unlabeled)';
|
|
481
|
+
|
|
482
|
+
// Count orphaned subscribers
|
|
483
|
+
for (const subscriber of subs) {
|
|
484
|
+
if (subscriber.disposed) {
|
|
485
|
+
orphanedEffects++;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Flag suspiciously large subscriber sets
|
|
490
|
+
if (subs.size > threshold) {
|
|
491
|
+
growingSubscribers.push({ label: lbl, count: subs.size });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { orphanedEffects, growingSubscribers };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─── Instrumented Factory Wrappers ──────────────────────────────────────────
|
|
499
|
+
//
|
|
500
|
+
// Drop-in replacements for createSignal / createEffect / createMemo that
|
|
501
|
+
// automatically register nodes, record traces, and store effect object
|
|
502
|
+
// references for graph edge resolution.
|
|
503
|
+
//
|
|
504
|
+
// For nodes created *before* devtools are enabled, use label() +
|
|
505
|
+
// registerSignalSetter() to register them manually.
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Instrumented `createSignal`.
|
|
509
|
+
*
|
|
510
|
+
* Returns the same `[getter, setter]` tuple but registers the signal for
|
|
511
|
+
* inspection and wraps the setter to attribute trigger sources.
|
|
512
|
+
*
|
|
513
|
+
* @template T
|
|
514
|
+
* @param {T} initialValue
|
|
515
|
+
* @param {{ label?: string }} [options]
|
|
516
|
+
* @returns {[() => T, (v: T | ((prev: T) => T)) => void]}
|
|
517
|
+
*/
|
|
518
|
+
export function createSignalTracked(initialValue, options) {
|
|
519
|
+
const [getter, setter] = createSignal(initialValue);
|
|
520
|
+
if (!isActive()) return [getter, setter];
|
|
521
|
+
|
|
522
|
+
const name = (options && options.label) ? options.label : undefined;
|
|
523
|
+
registerNode(getter, 'signal', name);
|
|
524
|
+
_setters.set(getter, setter);
|
|
525
|
+
|
|
526
|
+
const meta = _meta.get(getter);
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Wrapped setter that records trigger attribution before delegating.
|
|
530
|
+
* @param {T | ((prev: T) => T)} next
|
|
531
|
+
*/
|
|
532
|
+
function trackedSetter(next) {
|
|
533
|
+
_lastTrigger = meta;
|
|
534
|
+
try {
|
|
535
|
+
setter(next);
|
|
536
|
+
} finally {
|
|
537
|
+
_lastTrigger = null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return [getter, trackedSetter];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Instrumented `createEffect`.
|
|
546
|
+
*
|
|
547
|
+
* Wraps the user function to record execution duration and trigger source
|
|
548
|
+
* into the trace buffer.
|
|
549
|
+
*
|
|
550
|
+
* @param {Function} fn
|
|
551
|
+
* @param {{ label?: string }} [options]
|
|
552
|
+
* @returns {Function} dispose
|
|
553
|
+
*/
|
|
554
|
+
export function createEffectTracked(fn, options) {
|
|
555
|
+
if (!isActive()) return createEffect(fn);
|
|
556
|
+
|
|
557
|
+
/** @type {{ id: number, label: string, type: string }|null} */
|
|
558
|
+
let meta = null;
|
|
559
|
+
|
|
560
|
+
// We need the internal effect object for graph resolution. The scheduler
|
|
561
|
+
// creates it inside createEffect — we capture it through the _subscribers
|
|
562
|
+
// sets that get populated when the effect first runs.
|
|
563
|
+
const dispose = createEffect(() => {
|
|
564
|
+
const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
565
|
+
const result = fn();
|
|
566
|
+
const elapsed = typeof performance !== 'undefined'
|
|
567
|
+
? performance.now() - start
|
|
568
|
+
: Date.now() - start;
|
|
569
|
+
|
|
570
|
+
if (meta) {
|
|
571
|
+
const triggerLabel = _lastTrigger ? _lastTrigger.label : null;
|
|
572
|
+
recordTrace(meta.id, meta.label, 'effect', elapsed, triggerLabel);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return result;
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const name = (options && options.label) ? options.label : undefined;
|
|
579
|
+
registerNode(dispose, 'effect', name);
|
|
580
|
+
meta = _meta.get(dispose);
|
|
581
|
+
|
|
582
|
+
return dispose;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Instrumented `createMemo`.
|
|
587
|
+
*
|
|
588
|
+
* Wraps the computation to record execution traces.
|
|
589
|
+
*
|
|
590
|
+
* @template T
|
|
591
|
+
* @param {() => T} fn
|
|
592
|
+
* @param {{ label?: string }} [options]
|
|
593
|
+
* @returns {() => T}
|
|
594
|
+
*/
|
|
595
|
+
export function createMemoTracked(fn, options) {
|
|
596
|
+
if (!isActive()) return createMemo(fn);
|
|
597
|
+
|
|
598
|
+
/** @type {{ id: number, label: string, type: string }|null} */
|
|
599
|
+
let meta = null;
|
|
600
|
+
|
|
601
|
+
const getter = createMemo(() => {
|
|
602
|
+
const start = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
603
|
+
const result = fn();
|
|
604
|
+
const elapsed = typeof performance !== 'undefined'
|
|
605
|
+
? performance.now() - start
|
|
606
|
+
: Date.now() - start;
|
|
607
|
+
|
|
608
|
+
if (meta) {
|
|
609
|
+
const triggerLabel = _lastTrigger ? _lastTrigger.label : null;
|
|
610
|
+
recordTrace(meta.id, meta.label, 'memo', elapsed, triggerLabel);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return result;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const name = (options && options.label) ? options.label : undefined;
|
|
617
|
+
registerNode(getter, 'memo', name);
|
|
618
|
+
meta = _meta.get(getter);
|
|
619
|
+
|
|
620
|
+
return getter;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ─── Reset / Teardown ───────────────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Reset all devtools internal state. Intended for test teardown.
|
|
627
|
+
* Does not clear `globalThis.__DECANTR_DEVTOOLS__`.
|
|
628
|
+
* @returns {void}
|
|
629
|
+
*/
|
|
630
|
+
export function _reset() {
|
|
631
|
+
_nodes.clear();
|
|
632
|
+
_snapshots.clear();
|
|
633
|
+
_traceBuffer = [];
|
|
634
|
+
_traceIndex = 0;
|
|
635
|
+
_traceTotal = 0;
|
|
636
|
+
_nextId = 1;
|
|
637
|
+
_lastTrigger = null;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Disable devtools and clear all state.
|
|
642
|
+
* @returns {void}
|
|
643
|
+
*/
|
|
644
|
+
export function disableDevTools() {
|
|
645
|
+
globalThis.__DECANTR_DEVTOOLS__ = false;
|
|
646
|
+
_reset();
|
|
647
|
+
}
|