@reactra/devtools 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/ValueTree.d.ts +38 -0
- package/dist/ValueTree.d.ts.map +1 -0
- package/dist/ValueTree.js +178 -0
- package/dist/ValueTree.js.map +1 -0
- package/dist/helpers.d.ts +59 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +110 -0
- package/dist/helpers.js.map +1 -0
- package/dist/hookRecorder.d.ts +72 -0
- package/dist/hookRecorder.d.ts.map +1 -0
- package/dist/hookRecorder.js +142 -0
- package/dist/hookRecorder.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.d.ts.map +1 -0
- package/dist/invoke.js +67 -0
- package/dist/invoke.js.map +1 -0
- package/dist/model.d.ts +99 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +160 -0
- package/dist/model.js.map +1 -0
- package/dist/panel.d.ts +17 -0
- package/dist/panel.d.ts.map +1 -0
- package/dist/panel.js +438 -0
- package/dist/panel.js.map +1 -0
- package/dist/routerWatcher.d.ts +47 -0
- package/dist/routerWatcher.d.ts.map +1 -0
- package/dist/routerWatcher.js +51 -0
- package/dist/routerWatcher.js.map +1 -0
- package/dist/serialize.d.ts +38 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +217 -0
- package/dist/serialize.js.map +1 -0
- package/dist/storeRecorder.d.ts +25 -0
- package/dist/storeRecorder.d.ts.map +1 -0
- package/dist/storeRecorder.js +94 -0
- package/dist/storeRecorder.js.map +1 -0
- package/dist/storeWatcher.d.ts +26 -0
- package/dist/storeWatcher.d.ts.map +1 -0
- package/dist/storeWatcher.js +24 -0
- package/dist/storeWatcher.js.map +1 -0
- package/dist/styles.d.ts +5 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +288 -0
- package/dist/styles.js.map +1 -0
- package/dist/tabs/router.d.ts +10 -0
- package/dist/tabs/router.d.ts.map +1 -0
- package/dist/tabs/router.js +48 -0
- package/dist/tabs/router.js.map +1 -0
- package/dist/tabs/stores.d.ts +14 -0
- package/dist/tabs/stores.d.ts.map +1 -0
- package/dist/tabs/stores.js +167 -0
- package/dist/tabs/stores.js.map +1 -0
- package/dist/tabs/timeTravel.d.ts +46 -0
- package/dist/tabs/timeTravel.d.ts.map +1 -0
- package/dist/tabs/timeTravel.js +322 -0
- package/dist/tabs/timeTravel.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @reactra/devtools — the hook recorder: a conforming `__REACTRA_TEST__`
|
|
2
|
+
// implementation feeding the Components tab + the passive time-travel ring.
|
|
3
|
+
//
|
|
4
|
+
// Owner: reactra-devtools-spec.md §3 (hook coexistence — DVT001) + §7
|
|
5
|
+
// (passive history). The hook INTERFACE is Testing spec §2 — consumed as-is,
|
|
6
|
+
// never extended. Recorded events are Replay §3 types (architect
|
|
7
|
+
// condition 5): full-state `StateSnapshotEvent`s + synthesized mount
|
|
8
|
+
// markers, instance ids `Name#1` (no instance identity on the hook —
|
|
9
|
+
// DVT-LIM-02).
|
|
10
|
+
import { dataBindings } from "./helpers.js";
|
|
11
|
+
/**
|
|
12
|
+
* Create the devtools hook recorder. Pure logic — no DOM, no React — so the
|
|
13
|
+
* DT-01…03 scenarios run renderer-free.
|
|
14
|
+
*/
|
|
15
|
+
export const createHookRecorder = (opts = {}) => {
|
|
16
|
+
const historyLimit = opts.historyLimit ?? 5000;
|
|
17
|
+
const now = opts.now ?? Date.now;
|
|
18
|
+
const g = opts.globalObject ?? globalThis;
|
|
19
|
+
const components = new Map();
|
|
20
|
+
const listeners = new Set();
|
|
21
|
+
// The ring counts COMMITS (snapshot events); synthesized mount markers
|
|
22
|
+
// ride along and are evicted with their era, never counted.
|
|
23
|
+
let ring = [];
|
|
24
|
+
let snapshotCount = 0;
|
|
25
|
+
let recordingOn = true;
|
|
26
|
+
let evictedFlag = false;
|
|
27
|
+
let warnedEviction = false;
|
|
28
|
+
// componentIds already mount-marked in the CURRENT ring era (cleared with it).
|
|
29
|
+
let mounted = new Set();
|
|
30
|
+
// RAW store snapshots for lossless re-drive (the ring's `state` is serialized
|
|
31
|
+
// and lossy). Keyed by store componentId → [timestamp, raw] oldest-first,
|
|
32
|
+
// trimmed to historyLimit per id. Only `store:` ids are stashed.
|
|
33
|
+
const rawStore = new Map();
|
|
34
|
+
// Shared ring-append logic for both component commits and synthetic
|
|
35
|
+
// (route) snapshots: synthesize the first-seen mount marker, push the
|
|
36
|
+
// full-state snapshot, count it, and evict oldest-first past the cap.
|
|
37
|
+
const appendSnapshot = (componentId, state, t) => {
|
|
38
|
+
if (!mounted.has(componentId)) {
|
|
39
|
+
mounted.add(componentId);
|
|
40
|
+
ring.push({ type: "mount", componentId, phase: "mount", timestamp: t });
|
|
41
|
+
}
|
|
42
|
+
ring.push({ type: "state_snapshot", componentId, state, timestamp: t });
|
|
43
|
+
snapshotCount++;
|
|
44
|
+
if (snapshotCount > historyLimit) {
|
|
45
|
+
// Evict the oldest snapshot (and any mount marker stranded ahead of it) —
|
|
46
|
+
// "2.0" snapshots are full state, so dropping history never corrupts
|
|
47
|
+
// later folds.
|
|
48
|
+
const idx = ring.findIndex((e) => e.type === "state_snapshot");
|
|
49
|
+
ring.splice(0, idx + 1);
|
|
50
|
+
snapshotCount--;
|
|
51
|
+
evictedFlag = true;
|
|
52
|
+
if (!warnedEviction) {
|
|
53
|
+
warnedEviction = true;
|
|
54
|
+
console.warn(`[reactra:devtools] DVT005: passive history reached ${historyLimit} commits — oldest evicted (raise historyLimit to keep more)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const hook = {
|
|
59
|
+
update: (componentName, bindings) => {
|
|
60
|
+
const t = now();
|
|
61
|
+
const prev = components.get(componentName);
|
|
62
|
+
components.set(componentName, {
|
|
63
|
+
bindings,
|
|
64
|
+
lastSeen: t,
|
|
65
|
+
commits: (prev?.commits ?? 0) + 1,
|
|
66
|
+
});
|
|
67
|
+
if (recordingOn)
|
|
68
|
+
appendSnapshot(`${componentName}#1`, dataBindings(bindings), t);
|
|
69
|
+
for (const l of listeners)
|
|
70
|
+
l();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
install: () => {
|
|
75
|
+
const slot = g.__REACTRA_TEST__;
|
|
76
|
+
if (slot !== undefined && slot !== hook) {
|
|
77
|
+
console.warn("[reactra:devtools] DVT001: __REACTRA_TEST__ is already installed by another consumer — Components + passive time travel disabled (the occupant is never overwritten)");
|
|
78
|
+
return "occupied";
|
|
79
|
+
}
|
|
80
|
+
g.__REACTRA_TEST__ = hook;
|
|
81
|
+
return "installed";
|
|
82
|
+
},
|
|
83
|
+
uninstall: () => {
|
|
84
|
+
if (g.__REACTRA_TEST__ === hook)
|
|
85
|
+
delete g.__REACTRA_TEST__;
|
|
86
|
+
},
|
|
87
|
+
subscribe: (fn) => {
|
|
88
|
+
listeners.add(fn);
|
|
89
|
+
return () => listeners.delete(fn);
|
|
90
|
+
},
|
|
91
|
+
components: () => components,
|
|
92
|
+
recordSnapshot: (componentId, state) => {
|
|
93
|
+
// Respects the arm state (paused / frozen-while-traveling → full no-op,
|
|
94
|
+
// including no listener notification so scrub-driven route changes don't
|
|
95
|
+
// spuriously re-render). Serializes through the same depth/size-capped
|
|
96
|
+
// path as commits so the synthetic snapshot folds identically.
|
|
97
|
+
if (!recordingOn)
|
|
98
|
+
return;
|
|
99
|
+
const t = now();
|
|
100
|
+
appendSnapshot(componentId, dataBindings(state), t);
|
|
101
|
+
// Stash the RAW value for stores so re-drive is lossless (see rawStoreStateAt).
|
|
102
|
+
if (componentId.startsWith("store:")) {
|
|
103
|
+
const hist = rawStore.get(componentId) ?? [];
|
|
104
|
+
hist.push({ t, raw: state });
|
|
105
|
+
if (hist.length > historyLimit)
|
|
106
|
+
hist.shift();
|
|
107
|
+
rawStore.set(componentId, hist);
|
|
108
|
+
}
|
|
109
|
+
for (const l of listeners)
|
|
110
|
+
l();
|
|
111
|
+
},
|
|
112
|
+
rawStoreStateAt: (componentId, atTime) => {
|
|
113
|
+
const hist = rawStore.get(componentId);
|
|
114
|
+
if (hist === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
// Latest raw snapshot at or before atTime (fold semantics, single id).
|
|
117
|
+
let found;
|
|
118
|
+
for (const entry of hist) {
|
|
119
|
+
if (entry.t <= atTime)
|
|
120
|
+
found = entry.raw;
|
|
121
|
+
else
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
},
|
|
126
|
+
setRecording: (on) => {
|
|
127
|
+
recordingOn = on;
|
|
128
|
+
},
|
|
129
|
+
recording: () => recordingOn,
|
|
130
|
+
events: () => ring,
|
|
131
|
+
evicted: () => evictedFlag,
|
|
132
|
+
clearHistory: () => {
|
|
133
|
+
ring = [];
|
|
134
|
+
snapshotCount = 0;
|
|
135
|
+
mounted = new Set();
|
|
136
|
+
evictedFlag = false;
|
|
137
|
+
warnedEviction = false;
|
|
138
|
+
rawStore.clear();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
//# sourceMappingURL=hookRecorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hookRecorder.js","sourceRoot":"","sources":["../src/hookRecorder.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ReactraDevtools, type ReactraDevtoolsProps } from "./panel.ts";
|
|
2
|
+
export { callableNames as __callableNames, dataBindings as __dataBindings, isStale as __isStale, parseInvokeArgs as __parseInvokeArgs, passiveBundle as __passiveBundle, } from "./helpers.ts";
|
|
3
|
+
export { __resetStylesForTest, ensureStyles as __ensureStyles } from "./styles.ts";
|
|
4
|
+
export { createHookRecorder as __createHookRecorder, type CommitInfo as __CommitInfo, type HookRecorder as __HookRecorder, } from "./hookRecorder.ts";
|
|
5
|
+
export { listStores as __listStores, storeSnapshot as __storeSnapshot, subscribeStore as __subscribeStore, } from "./storeWatcher.ts";
|
|
6
|
+
export { attachStoreRecorder as __attachStoreRecorder, type StoreRecorderHandle as __StoreRecorderHandle, } from "./storeRecorder.ts";
|
|
7
|
+
export { createNavLog as __createNavLog, currentRoute as __currentRoute, listRoutes as __listRoutes, subscribeRouter as __subscribeRouter, } from "./routerWatcher.ts";
|
|
8
|
+
export { createDevtoolsModel as __createDevtoolsModel, type DevtoolsModel } from "./model.ts";
|
|
9
|
+
export { serialize as __serialize, serializeBindings as __serializeBindings } from "./serialize.ts";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAGvE,OAAO,EACL,aAAa,IAAI,eAAe,EAChC,YAAY,IAAI,cAAc,EAC9B,OAAO,IAAI,SAAS,EACpB,eAAe,IAAI,iBAAiB,EACpC,aAAa,IAAI,eAAe,GACjC,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,oBAAoB,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EACL,kBAAkB,IAAI,oBAAoB,EAC1C,KAAK,UAAU,IAAI,YAAY,EAC/B,KAAK,YAAY,IAAI,cAAc,GACpC,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,UAAU,IAAI,YAAY,EAC1B,aAAa,IAAI,eAAe,EAChC,cAAc,IAAI,gBAAgB,GACnC,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,mBAAmB,IAAI,qBAAqB,EAC5C,KAAK,mBAAmB,IAAI,qBAAqB,GAClD,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,YAAY,IAAI,cAAc,EAC9B,YAAY,IAAI,cAAc,EAC9B,UAAU,IAAI,YAAY,EAC1B,eAAe,IAAI,iBAAiB,GACrC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,mBAAmB,IAAI,qBAAqB,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAC7F,OAAO,EAAE,SAAS,IAAI,WAAW,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @reactra/devtools — public surface.
|
|
2
|
+
//
|
|
3
|
+
// Owner: reactra-devtools-spec.md + reactra-runtime-spec.md §3. User-facing:
|
|
4
|
+
// the `<ReactraDevtools>` panel. The data layer below is internal tier —
|
|
5
|
+
// re-exported __-prefixed for tests only, never documented as user API.
|
|
6
|
+
export { ReactraDevtools } from "./panel.js";
|
|
7
|
+
// Internal tier (tests only — Runtime spec §1 visibility tiers).
|
|
8
|
+
export { callableNames as __callableNames, dataBindings as __dataBindings, isStale as __isStale, parseInvokeArgs as __parseInvokeArgs, passiveBundle as __passiveBundle, } from "./helpers.js";
|
|
9
|
+
export { __resetStylesForTest, ensureStyles as __ensureStyles } from "./styles.js";
|
|
10
|
+
export { createHookRecorder as __createHookRecorder, } from "./hookRecorder.js";
|
|
11
|
+
export { listStores as __listStores, storeSnapshot as __storeSnapshot, subscribeStore as __subscribeStore, } from "./storeWatcher.js";
|
|
12
|
+
export { attachStoreRecorder as __attachStoreRecorder, } from "./storeRecorder.js";
|
|
13
|
+
export { createNavLog as __createNavLog, currentRoute as __currentRoute, listRoutes as __listRoutes, subscribeRouter as __subscribeRouter, } from "./routerWatcher.js";
|
|
14
|
+
export { createDevtoolsModel as __createDevtoolsModel } from "./model.js";
|
|
15
|
+
export { serialize as __serialize, serializeBindings as __serializeBindings } from "./serialize.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,6EAA6E;AAC7E,yEAAyE;AACzE,wEAAwE;AAExE,OAAO,EAAE,eAAe,EAA6B,MAAM,YAAY,CAAA;AAEvE,iEAAiE;AACjE,OAAO,EACL,aAAa,IAAI,eAAe,EAChC,YAAY,IAAI,cAAc,EAC9B,OAAO,IAAI,SAAS,EACpB,eAAe,IAAI,iBAAiB,EACpC,aAAa,IAAI,eAAe,GACjC,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,oBAAoB,EAAE,YAAY,IAAI,cAAc,EAAE,MAAM,aAAa,CAAA;AAClF,OAAO,EACL,kBAAkB,IAAI,oBAAoB,GAG3C,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,UAAU,IAAI,YAAY,EAC1B,aAAa,IAAI,eAAe,EAChC,cAAc,IAAI,gBAAgB,GACnC,MAAM,mBAAmB,CAAA;AAC1B,OAAO,EACL,mBAAmB,IAAI,qBAAqB,GAE7C,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,YAAY,IAAI,cAAc,EAC9B,YAAY,IAAI,cAAc,EAC9B,UAAU,IAAI,YAAY,EAC1B,eAAe,IAAI,iBAAiB,GACrC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,mBAAmB,IAAI,qBAAqB,EAAsB,MAAM,YAAY,CAAA;AAC7F,OAAO,EAAE,SAAS,IAAI,WAAW,EAAE,iBAAiB,IAAI,mBAAmB,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/invoke.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Result of a successful invoke. */
|
|
2
|
+
export interface InvokeOk {
|
|
3
|
+
ok: true;
|
|
4
|
+
}
|
|
5
|
+
/** Result of a failed invoke (DVT004 — bad args string or thrown action). */
|
|
6
|
+
export interface InvokeError {
|
|
7
|
+
ok: false;
|
|
8
|
+
/** The full DVT004-prefixed error message for inline display. */
|
|
9
|
+
error: string;
|
|
10
|
+
}
|
|
11
|
+
/** Return type of `runInvoke`. */
|
|
12
|
+
export type InvokeResult = InvokeOk | InvokeError;
|
|
13
|
+
/**
|
|
14
|
+
* Invoke an action function with the args text from the input field.
|
|
15
|
+
*
|
|
16
|
+
* - Empty `argsText` (or absent) → zero-arg call: `fn()` — the "▶ run"
|
|
17
|
+
* shortcut that doesn't require the JSON-array input.
|
|
18
|
+
* - Non-empty `argsText` must be a JSON array string; anything else yields a
|
|
19
|
+
* `DVT004: invalid JSON` error (no throw into the host app).
|
|
20
|
+
* - A thrown action is caught and returned as `DVT004: <message>`.
|
|
21
|
+
* - Fix 6b: an async action that returns a rejected promise is also caught and
|
|
22
|
+
* surfaced as `DVT004: …` via `Promise.resolve(...).catch(...)` — prevents
|
|
23
|
+
* the rejection from escaping to `window.unhandledrejection`.
|
|
24
|
+
*
|
|
25
|
+
* @param fn - The action function to invoke (from bindings or snap).
|
|
26
|
+
* @param argsText - The raw text from the args input field (may be empty).
|
|
27
|
+
* @param onAsyncError - Optional callback for async DVT004 errors. Called if
|
|
28
|
+
* the action returns a rejected promise. Defaults to a no-op (callers that
|
|
29
|
+
* want to show async errors must pass this).
|
|
30
|
+
* @returns `{ ok: true }` on success (sync), or `{ ok: false, error: "DVT004: …" }`
|
|
31
|
+
* on a bad-args string or synchronous throw. Async rejections are routed to
|
|
32
|
+
* `onAsyncError` and the synchronous return is `{ ok: true }`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const runInvoke: (fn: unknown, argsText?: string, onAsyncError?: (error: string) => void) => InvokeResult;
|
|
35
|
+
//# sourceMappingURL=invoke.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoke.d.ts","sourceRoot":"","sources":["../src/invoke.ts"],"names":[],"mappings":"AAUA,qCAAqC;AACrC,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,IAAI,CAAA;CACT;AAED,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,KAAK,CAAA;IACT,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;CACd;AAED,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAA;AAEjD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,GACpB,IAAI,OAAO,EACX,WAAW,MAAM,EACjB,eAAe,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KACrC,YAqCF,CAAA"}
|
package/dist/invoke.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @reactra/devtools — action-invoke helper (T1.4).
|
|
2
|
+
//
|
|
3
|
+
// Owner: reactra-devtools-spec.md §4 (action invoke, DVT004). Extracts the
|
|
4
|
+
// duplicated DVT004 invoke block from panel.ts and stores.ts into one shared
|
|
5
|
+
// helper. Adds a zero-arg shortcut (when argsText is empty or absent, the
|
|
6
|
+
// action is called with no arguments — no input field needed). The per-param
|
|
7
|
+
// labeled-input feature is gated (Testing §2 param metadata) — NOT here.
|
|
8
|
+
import { parseInvokeArgs } from "./helpers.js";
|
|
9
|
+
/**
|
|
10
|
+
* Invoke an action function with the args text from the input field.
|
|
11
|
+
*
|
|
12
|
+
* - Empty `argsText` (or absent) → zero-arg call: `fn()` — the "▶ run"
|
|
13
|
+
* shortcut that doesn't require the JSON-array input.
|
|
14
|
+
* - Non-empty `argsText` must be a JSON array string; anything else yields a
|
|
15
|
+
* `DVT004: invalid JSON` error (no throw into the host app).
|
|
16
|
+
* - A thrown action is caught and returned as `DVT004: <message>`.
|
|
17
|
+
* - Fix 6b: an async action that returns a rejected promise is also caught and
|
|
18
|
+
* surfaced as `DVT004: …` via `Promise.resolve(...).catch(...)` — prevents
|
|
19
|
+
* the rejection from escaping to `window.unhandledrejection`.
|
|
20
|
+
*
|
|
21
|
+
* @param fn - The action function to invoke (from bindings or snap).
|
|
22
|
+
* @param argsText - The raw text from the args input field (may be empty).
|
|
23
|
+
* @param onAsyncError - Optional callback for async DVT004 errors. Called if
|
|
24
|
+
* the action returns a rejected promise. Defaults to a no-op (callers that
|
|
25
|
+
* want to show async errors must pass this).
|
|
26
|
+
* @returns `{ ok: true }` on success (sync), or `{ ok: false, error: "DVT004: …" }`
|
|
27
|
+
* on a bad-args string or synchronous throw. Async rejections are routed to
|
|
28
|
+
* `onAsyncError` and the synchronous return is `{ ok: true }`.
|
|
29
|
+
*/
|
|
30
|
+
export const runInvoke = (fn, argsText, onAsyncError) => {
|
|
31
|
+
const text = argsText?.trim() ?? "";
|
|
32
|
+
let args;
|
|
33
|
+
if (text === "") {
|
|
34
|
+
// Zero-arg shortcut — no parse needed.
|
|
35
|
+
args = [];
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const parsed = parseInvokeArgs(text);
|
|
39
|
+
if ("error" in parsed) {
|
|
40
|
+
// Bad args string — DVT004 (same prefix as the thrown-error path;
|
|
41
|
+
// both are invoke failures, Devtools §4).
|
|
42
|
+
return { ok: false, error: `DVT004: ${parsed.error}` };
|
|
43
|
+
}
|
|
44
|
+
args = parsed.args;
|
|
45
|
+
}
|
|
46
|
+
let result;
|
|
47
|
+
try {
|
|
48
|
+
result = fn(...args);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// Synchronous throw — DVT004, surfaced inline, never rethrown.
|
|
52
|
+
return { ok: false, error: `DVT004: ${String(err)}` };
|
|
53
|
+
}
|
|
54
|
+
// Fix 6b: if the action returned a Promise, attach a rejection handler so
|
|
55
|
+
// an async failure surfaces as DVT004 rather than escaping to the host as
|
|
56
|
+
// an unhandledrejection. The synchronous return is still { ok: true }
|
|
57
|
+
// (we can't know sync whether the promise will reject).
|
|
58
|
+
if (result !== null && result !== undefined && typeof result.then === "function") {
|
|
59
|
+
Promise.resolve(result).catch((err) => {
|
|
60
|
+
const msg = `DVT004: ${String(err)}`;
|
|
61
|
+
if (onAsyncError)
|
|
62
|
+
onAsyncError(msg);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return { ok: true };
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=invoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invoke.js","sourceRoot":"","sources":["../src/invoke.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,0EAA0E;AAC1E,6EAA6E;AAC7E,yEAAyE;AAEzE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAiB9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,EAAW,EACX,QAAiB,EACjB,YAAsC,EACxB,EAAE;IAChB,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IACnC,IAAI,IAAe,CAAA;IAEnB,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;QAChB,uCAAuC;QACvC,IAAI,GAAG,EAAE,CAAA;IACX,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,OAAO,IAAI,MAAM,EAAE,CAAC;YACtB,kEAAkE;YAClE,0CAA0C;YAC1C,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,MAAM,CAAC,KAAK,EAAE,EAAE,CAAA;QACxD,CAAC;QACD,IAAI,GAAG,MAAM,CAAC,IAAI,CAAA;IACpB,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAI,EAAmC,CAAC,GAAG,IAAI,CAAC,CAAA;IACxD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,+DAA+D;QAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAA;IACvD,CAAC;IAED,0EAA0E;IAC1E,0EAA0E;IAC1E,sEAAsE;IACtE,wDAAwD;IACxD,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,IAAI,OAAQ,MAAkC,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC9G,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;YAC7C,MAAM,GAAG,GAAG,WAAW,MAAM,CAAC,GAAG,CAAC,EAAE,CAAA;YACpC,IAAI,YAAY;gBAAE,YAAY,CAAC,GAAG,CAAC,CAAA;QACrC,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;AACrB,CAAC,CAAA"}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { HookRecorder } from "./hookRecorder.ts";
|
|
2
|
+
import type { CommitInfo } from "./hookRecorder.ts";
|
|
3
|
+
import { type NavLog } from "./routerWatcher.ts";
|
|
4
|
+
import type { ReplayEvent } from "@reactra/behaviours/replayable";
|
|
5
|
+
/** One store snapshot row for the model surface (facade over StoreRow). */
|
|
6
|
+
export interface ModelStoreEntry {
|
|
7
|
+
/** Registered store name. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Store kind as registered. */
|
|
10
|
+
kind: "export" | "session" | "route";
|
|
11
|
+
/** True when the store instance is live. */
|
|
12
|
+
instantiated: boolean;
|
|
13
|
+
/** Preserved state field names (Store §6). */
|
|
14
|
+
preservedFields: readonly string[];
|
|
15
|
+
/** Subtree path when applicable. */
|
|
16
|
+
subtreePath?: string;
|
|
17
|
+
/** Current snapshot — undefined when not instantiated. */
|
|
18
|
+
snapshot?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** Current router state for the model surface. */
|
|
21
|
+
export interface ModelRoute {
|
|
22
|
+
/** Matched route id, or null on no match. */
|
|
23
|
+
routeId: string | null;
|
|
24
|
+
/** Matched route path pattern, or null. */
|
|
25
|
+
path: string | null;
|
|
26
|
+
/** Current pathname. */
|
|
27
|
+
pathname: string;
|
|
28
|
+
/** Extracted path params. */
|
|
29
|
+
params: Record<string, string>;
|
|
30
|
+
/** Coerced query params. */
|
|
31
|
+
query: Record<string, string | string[]>;
|
|
32
|
+
}
|
|
33
|
+
/** The DevtoolsModel facade — one plain-reads object wrapping the data layer. */
|
|
34
|
+
export interface DevtoolsModel {
|
|
35
|
+
/**
|
|
36
|
+
* Latest-commit info per component name (mirrors `HookRecorder.components()`).
|
|
37
|
+
* Empty map when the hook is occupied (DVT001).
|
|
38
|
+
*/
|
|
39
|
+
components(): ReadonlyMap<string, CommitInfo>;
|
|
40
|
+
/**
|
|
41
|
+
* All registered store rows plus their live snapshots.
|
|
42
|
+
* Fresh read each call (same as listStores() + storeSnapshot()).
|
|
43
|
+
*/
|
|
44
|
+
stores(): readonly ModelStoreEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* Current router state derived from RouterRegistry.
|
|
47
|
+
*/
|
|
48
|
+
route(): ModelRoute;
|
|
49
|
+
/**
|
|
50
|
+
* The passive-history ring from the recorder (Devtools §7).
|
|
51
|
+
*/
|
|
52
|
+
timeline(): readonly ReplayEvent[];
|
|
53
|
+
/**
|
|
54
|
+
* Subscribe to data changes: recorder commits, store value changes, and
|
|
55
|
+
* route transitions all fire `fn`. Returns an unsubscribe function.
|
|
56
|
+
* Ticker-based: subscribing starts a single consolidated interval for
|
|
57
|
+
* store/route polling; unsubscribing the last subscriber stops it.
|
|
58
|
+
*/
|
|
59
|
+
subscribe(fn: () => void): () => void;
|
|
60
|
+
/**
|
|
61
|
+
* The current active subscriber count — exposed for test assertions.
|
|
62
|
+
*/
|
|
63
|
+
subscriberCount(): number;
|
|
64
|
+
/**
|
|
65
|
+
* Dispose the model: unsubscribe from the recorder and the router registry,
|
|
66
|
+
* stop the consolidated ticker, and clear all remaining listeners.
|
|
67
|
+
*
|
|
68
|
+
* Call from the panel's unmount effect to prevent the module-singleton
|
|
69
|
+
* `RouterRegistry` from accumulating a listener per panel mount (Fix 3 —
|
|
70
|
+
* subscription leak). After dispose, subscribe() becomes a no-op unsubscriber.
|
|
71
|
+
*/
|
|
72
|
+
dispose(): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Options for `createDevtoolsModel` (injectable for tests).
|
|
76
|
+
*/
|
|
77
|
+
export interface DevtoolsModelOptions {
|
|
78
|
+
/** The hook recorder instance from the panel. */
|
|
79
|
+
recorder: HookRecorder;
|
|
80
|
+
/**
|
|
81
|
+
* The panel-owned nav log (records navigations while the panel is alive).
|
|
82
|
+
* Currently unused in the model interface — passed for future use.
|
|
83
|
+
*/
|
|
84
|
+
navLog?: NavLog;
|
|
85
|
+
/**
|
|
86
|
+
* Polling interval in ms for the consolidated ticker (arch#2a; default 700ms).
|
|
87
|
+
* This controls how often store-registry + route changes are polled for
|
|
88
|
+
* subscribers. Exact millisecond timing isn't critical — delivery is
|
|
89
|
+
* best-effort, not guaranteed.
|
|
90
|
+
*/
|
|
91
|
+
tickIntervalMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create the DevtoolsModel facade (arch#8). One per panel instance — the
|
|
95
|
+
* panel creates it in its `useRef` alongside the recorder. Additive: no
|
|
96
|
+
* existing tab code is changed by this module.
|
|
97
|
+
*/
|
|
98
|
+
export declare const createDevtoolsModel: (opts: DevtoolsModelOptions) => DevtoolsModel;
|
|
99
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAQnD,OAAO,EAA6C,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAC3F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAA;AAEjE,2EAA2E;AAC3E,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,gCAAgC;IAChC,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAA;IACpC,4CAA4C;IAC5C,YAAY,EAAE,OAAO,CAAA;IACrB,8CAA8C;IAC9C,eAAe,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAA;CACzC;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,UAAU,IAAI,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IAC7C;;;OAGG;IACH,MAAM,IAAI,SAAS,eAAe,EAAE,CAAA;IACpC;;OAEG;IACH,KAAK,IAAI,UAAU,CAAA;IACnB;;OAEG;IACH,QAAQ,IAAI,SAAS,WAAW,EAAE,CAAA;IAClC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAA;IACrC;;OAEG;IACH,eAAe,IAAI,MAAM,CAAA;IACzB;;;;;;;OAOG;IACH,OAAO,IAAI,IAAI,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,QAAQ,EAAE,YAAY,CAAA;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,oBAAoB,KAAG,aAuJhE,CAAA"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @reactra/devtools — the DevtoolsModel facade.
|
|
2
|
+
//
|
|
3
|
+
// Owner: reactra-devtools-spec.md §3 (data sources). A plain-reads object
|
|
4
|
+
// exposing components(), stores(), route(), timeline() + subscribe(fn),
|
|
5
|
+
// wrapping the existing recorder + watchers. Additive only — no existing
|
|
6
|
+
// tab code is rewired here; tabs still consume the raw recorder/watchers
|
|
7
|
+
// until each migrates in a later phase.
|
|
8
|
+
//
|
|
9
|
+
// Browser globals: NONE at module level. subscribe() starts the store/router
|
|
10
|
+
// polls inside the registered handler via the ticker; stoppers are returned.
|
|
11
|
+
import { listStores, storeInstanceToken, storeSnapshot, subscribeStore, } from "./storeWatcher.js";
|
|
12
|
+
import { currentRoute, listRoutes, subscribeRouter } from "./routerWatcher.js";
|
|
13
|
+
/**
|
|
14
|
+
* Create the DevtoolsModel facade (arch#8). One per panel instance — the
|
|
15
|
+
* panel creates it in its `useRef` alongside the recorder. Additive: no
|
|
16
|
+
* existing tab code is changed by this module.
|
|
17
|
+
*/
|
|
18
|
+
export const createDevtoolsModel = (opts) => {
|
|
19
|
+
const { recorder } = opts;
|
|
20
|
+
const tickIntervalMs = opts.tickIntervalMs ?? 700;
|
|
21
|
+
const listeners = new Set();
|
|
22
|
+
// The ONE shared interval for the consolidated ticker (arch#2a).
|
|
23
|
+
// Exists only while there are subscribers — created on first subscribe,
|
|
24
|
+
// cleared on last unsubscribe.
|
|
25
|
+
let tickerId = null;
|
|
26
|
+
const notify = () => {
|
|
27
|
+
for (const fn of listeners)
|
|
28
|
+
fn();
|
|
29
|
+
};
|
|
30
|
+
// QW-7: a SINGLE shared map of store subscriptions, keyed by store name. Each
|
|
31
|
+
// entry holds the store's unsubscribe fn plus the instance-identity token at
|
|
32
|
+
// subscribe time. Every subscription calls `notify` (NOT a per-listener fn), so
|
|
33
|
+
// a store is wired exactly ONCE regardless of how many panel listeners exist —
|
|
34
|
+
// this is what guarantees no double-fire (the old per-listener subscribeStore
|
|
35
|
+
// path is gone). `notify` already fans out to all listeners.
|
|
36
|
+
const storeSubs = new Map();
|
|
37
|
+
/**
|
|
38
|
+
* QW-7: reconcile `storeSubs` against the live registry. Called on subscribe
|
|
39
|
+
* and on every tick, so a store instantiated AFTER subscribe-time (a route
|
|
40
|
+
* store entering on navigation) gets a live subscription by the next tick
|
|
41
|
+
* (≤ tickIntervalMs) instead of waiting on poll cadence forever.
|
|
42
|
+
*
|
|
43
|
+
* - Newcomer (instantiated, not in the map): subscribe via `notify`, record
|
|
44
|
+
* `{ off, token }`. Dedup — never double-subscribe an already-tracked store.
|
|
45
|
+
* - HMR swap (token changed): the old subscription is bound to the dead
|
|
46
|
+
* instance — drop it and re-subscribe to the new one (preserves the
|
|
47
|
+
* devtools' existing HMR-resubscribe behavior).
|
|
48
|
+
* - Gone (de-instantiated on nav exit, or token now undefined): unsubscribe
|
|
49
|
+
* and remove — keeps the map from growing without bound across navigations.
|
|
50
|
+
*/
|
|
51
|
+
const reconcileStoreSubs = () => {
|
|
52
|
+
const live = new Map();
|
|
53
|
+
for (const r of listStores()) {
|
|
54
|
+
if (r.instantiated)
|
|
55
|
+
live.set(r.name, storeInstanceToken(r.name));
|
|
56
|
+
}
|
|
57
|
+
// Drop subs for stores no longer live, or whose instance was swapped (HMR).
|
|
58
|
+
for (const [name, entry] of storeSubs) {
|
|
59
|
+
const token = live.get(name);
|
|
60
|
+
if (token === undefined || token !== entry.token) {
|
|
61
|
+
entry.off();
|
|
62
|
+
storeSubs.delete(name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Add subs for newly-live stores not already tracked.
|
|
66
|
+
for (const [name, token] of live) {
|
|
67
|
+
if (storeSubs.has(name))
|
|
68
|
+
continue;
|
|
69
|
+
storeSubs.set(name, { off: subscribeStore(name, notify), token });
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/** Tear down every shared store subscription and empty the map. */
|
|
73
|
+
const clearStoreSubs = () => {
|
|
74
|
+
for (const entry of storeSubs.values())
|
|
75
|
+
entry.off();
|
|
76
|
+
storeSubs.clear();
|
|
77
|
+
};
|
|
78
|
+
// Fix 3: capture both unsubscribers so dispose() can call them.
|
|
79
|
+
// RouterRegistry is a module singleton — failing to call routerOff leaks a
|
|
80
|
+
// listener per panel mount/unmount cycle.
|
|
81
|
+
const recOff = recorder.subscribe(notify);
|
|
82
|
+
const routerOff = subscribeRouter(notify);
|
|
83
|
+
// Tracks whether dispose() has been called — prevents double-dispose.
|
|
84
|
+
let disposed = false;
|
|
85
|
+
return {
|
|
86
|
+
components: () => recorder.components(),
|
|
87
|
+
stores: () => {
|
|
88
|
+
const rows = [...listStores()];
|
|
89
|
+
return rows.map((r) => ({
|
|
90
|
+
name: r.name,
|
|
91
|
+
kind: r.kind,
|
|
92
|
+
instantiated: r.instantiated,
|
|
93
|
+
preservedFields: r.preservedFields,
|
|
94
|
+
subtreePath: r.subtreePath,
|
|
95
|
+
snapshot: storeSnapshot(r.name),
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
98
|
+
route: () => {
|
|
99
|
+
const s = currentRoute();
|
|
100
|
+
return {
|
|
101
|
+
routeId: s.route?.id ?? null,
|
|
102
|
+
path: s.route?.path ?? null,
|
|
103
|
+
pathname: s.pathname,
|
|
104
|
+
params: s.params,
|
|
105
|
+
query: s.query,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
timeline: () => recorder.events(),
|
|
109
|
+
subscribe: (fn) => {
|
|
110
|
+
listeners.add(fn);
|
|
111
|
+
if (listeners.size === 1) {
|
|
112
|
+
// First subscriber — start the consolidated ticker. Each tick reconciles
|
|
113
|
+
// store subscriptions FIRST (so a route store that just instantiated gets
|
|
114
|
+
// a live sub) THEN notifies. The registry has no registration event
|
|
115
|
+
// (arch#2b deferred), so the tick is also the catch-up for newcomers —
|
|
116
|
+
// but now they're wired for INSTANT delivery thereafter, not polled.
|
|
117
|
+
tickerId = setInterval(() => {
|
|
118
|
+
reconcileStoreSubs();
|
|
119
|
+
notify();
|
|
120
|
+
}, tickIntervalMs);
|
|
121
|
+
}
|
|
122
|
+
// QW-7: reconcile on subscribe so stores already live at subscribe-time are
|
|
123
|
+
// wired immediately (not only on the first tick). Subscriptions are shared
|
|
124
|
+
// (keyed by store name, calling `notify`), so this is per-model, not
|
|
125
|
+
// per-listener — adding a second listener does NOT re-subscribe any store.
|
|
126
|
+
reconcileStoreSubs();
|
|
127
|
+
return () => {
|
|
128
|
+
listeners.delete(fn);
|
|
129
|
+
if (listeners.size === 0) {
|
|
130
|
+
// Last subscriber gone — tear down the shared store subs + the ticker.
|
|
131
|
+
clearStoreSubs();
|
|
132
|
+
if (tickerId !== null) {
|
|
133
|
+
clearInterval(tickerId);
|
|
134
|
+
tickerId = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
},
|
|
139
|
+
subscriberCount: () => listeners.size,
|
|
140
|
+
dispose: () => {
|
|
141
|
+
if (disposed)
|
|
142
|
+
return;
|
|
143
|
+
disposed = true;
|
|
144
|
+
// Stop the ticker.
|
|
145
|
+
if (tickerId !== null) {
|
|
146
|
+
clearInterval(tickerId);
|
|
147
|
+
tickerId = null;
|
|
148
|
+
}
|
|
149
|
+
// QW-7: tear down every shared store subscription (else they leak past the
|
|
150
|
+
// panel's life — the store registry is a module singleton).
|
|
151
|
+
clearStoreSubs();
|
|
152
|
+
// Remove all panel listeners so stale refs don't fire after unmount.
|
|
153
|
+
listeners.clear();
|
|
154
|
+
// Unsubscribe from the module-level singletons.
|
|
155
|
+
recOff();
|
|
156
|
+
routerOff();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
|
package/dist/panel.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/** Props of the developer panel (Devtools spec §2). */
|
|
3
|
+
export interface ReactraDevtoolsProps {
|
|
4
|
+
/** Open the drawer on mount (default false — collapsed pill). */
|
|
5
|
+
defaultOpen?: boolean;
|
|
6
|
+
/** Passive-history ring capacity in commits (default 5000; DVT005 on eviction). */
|
|
7
|
+
historyLimit?: number;
|
|
8
|
+
/** Arm the passive recorder at mount (default true). */
|
|
9
|
+
record?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Reactra developer panel: Components / Stores / Router / Time Travel
|
|
13
|
+
* tabs over the shipped runtime surfaces (Devtools spec §3 — zero new
|
|
14
|
+
* compiler emission). Hand-mounted; the mount is the sole opt-in.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ReactraDevtools: (props: ReactraDevtoolsProps) => ReactNode;
|
|
17
|
+
//# sourceMappingURL=panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../src/panel.ts"],"names":[],"mappings":"AAYA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AAoBd,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAWD;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,oBAAoB,KAAG,SAuhB7D,CAAA"}
|