@nkzw/fate-devtools 0.0.0-stage → 2.0.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/lib/index.d.mts +19 -0
- package/lib/index.mjs +1 -0
- package/lib/panel/assets/index-B8BrSDrl.js +13 -0
- package/lib/panel/assets/index-NKFD_JQx.css +1 -0
- package/lib/panel/index.html +13 -0
- package/lib/react.d.mts +10 -0
- package/lib/react.mjs +25 -0
- package/lib/src-BAd3o4sS.mjs +563 -0
- package/lib/src-CBLiWt9-.mjs +556 -0
- package/lib/vite.d.mts +31 -0
- package/lib/vite.mjs +98 -0
- package/package.json +57 -3
- package/README.md +0 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;--bg:#fff;--bg-subtle:#f6f7f9;--bg-hover:#eef1f5;--bg-selected:#e5efff;--border:#e3e6eb;--text:#1c2026;--text-muted:#6b7380;--accent:#3d87f5;--accent-text:#1f65d6;--green:#1a8f4c;--green-bg:#e3f5ea;--amber:#a15f00;--amber-bg:#fdf1dc;--red:#c73434;--red-bg:#fde6e6;--blue:#1f65d6;--blue-bg:#e2edff;--purple:#7a3fd1;--purple-bg:#efe6fd;--json-key:#7a3fd1;--json-string:#1a8f4c;--json-number:#1f65d6;--json-boolean:#c25e00;--mono:ui-monospace, "SF Mono", "Fira Code", Menlo, Consolas, monospace;--sans:system-ui, -apple-system, "Segoe UI", Roboto, sans-serif}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;--bg:#121418;--bg-subtle:#191c21;--bg-hover:#20242a;--bg-selected:#1b2b45;--border:#2a2f37;--text:#e3e6eb;--text-muted:#8b94a3;--accent-text:#79a9ff;--green:#5fd38d;--green-bg:#15301f;--amber:#f2b45a;--amber-bg:#352711;--red:#ff7b7b;--red-bg:#3a1a1a;--blue:#79a9ff;--blue-bg:#172846;--purple:#b994ff;--purple-bg:#2a1d45;--json-key:#b994ff;--json-string:#7ddc9f;--json-number:#79a9ff;--json-boolean:#f2a35a}}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg);color:var(--text);font:13px/1.45 var(--sans);-webkit-font-smoothing:antialiased}button,input,select{font:inherit;color:inherit}code,.mono{font-family:var(--mono);font-size:12px}.muted{color:var(--text-muted)}.app{flex-direction:column;height:100%;display:flex}.toolbar{border-bottom:1px solid var(--border);background:var(--bg-subtle);flex-wrap:wrap;align-items:center;gap:8px 16px;padding:6px 12px;display:flex}.brand{align-items:center;gap:8px;display:flex}.brand select{border:1px solid var(--border);corner-shape:squircle;background:var(--bg);border-radius:16px;max-width:240px;padding:2px 6px}.logo{corner-shape:squircle;background:var(--accent);color:#fff;border-radius:12px;place-items:center;width:22px;height:22px;padding-right:3px;font:italic 18px/1 Georgia,serif;display:inline-grid}.logo.large{border-radius:28px;width:56px;height:56px;padding-right:6px;font-size:44px}.brand-name{font:italic 18px/1 Georgia,serif}.tabs{gap:4px;display:flex}.tab{corner-shape:squircle;color:var(--text-muted);cursor:pointer;background:0 0;border:0;border-radius:16px;align-items:center;gap:6px;padding:4px 10px;display:flex}.tab:hover{background:var(--bg-hover);color:var(--text)}.tab.active{background:var(--bg);color:var(--text);box-shadow:0 0 0 1px var(--border)}.count{corner-shape:squircle;background:var(--bg-hover);min-width:18px;color:var(--text-muted);text-align:center;border-radius:12px;padding:0 5px;font-size:11px}.stats{gap:6px;margin-left:auto;display:flex}.button-group{flex-shrink:0;gap:6px;display:flex}.button-group button{border:1px solid var(--border);corner-shape:squircle;background:var(--bg);cursor:pointer;white-space:nowrap;border-radius:16px;padding:3px 10px}.button-group button:hover{background:var(--bg-hover)}.button-group button.danger{color:var(--red)}.content{flex:1;min-height:0}.master-detail{grid-template-columns:minmax(260px,38%) 1fr;height:100%;display:grid}.master{border-right:1px solid var(--border);flex-direction:column;min-height:0;display:flex}.filter{border-bottom:1px solid var(--border);padding:8px}.filter input{border:1px solid var(--border);corner-shape:squircle;background:var(--bg-subtle);border-radius:16px;outline:none;width:100%;padding:5px 8px}.filter input:focus{border-color:var(--accent)}.chips{border-bottom:1px solid var(--border);flex-wrap:wrap;gap:4px;padding:1px 4px;display:flex}.chip{border:1px solid var(--border);corner-shape:squircle;cursor:pointer;background:0 0;border-radius:16px;align-items:center;gap:4px;padding:1px 8px;font-size:12px;display:flex}.chip.active{border-color:var(--accent);background:var(--bg-selected);color:var(--accent-text)}.rows{flex:1;overflow:auto}.row{border:0;border-bottom:1px solid var(--border);cursor:pointer;text-align:left;background:0 0;justify-content:space-between;align-items:center;gap:8px;width:100%;padding:5px 10px;display:flex}.row:hover{background:var(--bg-hover)}.row.selected{background:var(--bg-selected)}.row-title{min-width:0;font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:6px;font-size:12px;display:flex;overflow:hidden}.row-meta{flex-shrink:0;align-items:center;gap:6px;font-size:12px;display:flex}.type{color:var(--accent-text);flex-shrink:0}.label{font-family:var(--sans);text-overflow:ellipsis;font-size:13px;overflow:hidden}.hash{margin-left:4px}.detail{min-height:0;overflow:auto}.detail-body{padding:12px 16px 24px}.detail-header{justify-content:space-between;align-items:flex-start;gap:12px;margin-bottom:12px;display:flex}.detail-header h2{font-family:var(--mono);word-break:break-all;align-items:center;gap:6px;margin:0 0 6px;font-size:15px;font-weight:600;display:flex}.detail-badges{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.section{border:1px solid var(--border);corner-shape:squircle;border-radius:24px;margin-top:12px;overflow:hidden}.section-header{border-bottom:1px solid var(--border);background:var(--bg-subtle);corner-shape:squircle;border-radius:24px 24px 0 0;justify-content:space-between;align-items:center;padding:7px 14px;display:flex}.section-header h3{color:var(--text-muted);letter-spacing:.04em;text-transform:uppercase;margin:0;font-size:11px;font-weight:600}.section-body{padding:10px 14px;overflow-x:auto}.empty{color:var(--text-muted);text-align:center;padding:24px}.badge{corner-shape:squircle;white-space:nowrap;border-radius:12px;padding:0 7px;font-size:11px;font-weight:500;line-height:18px;display:inline-block}.badge-neutral{background:var(--bg-hover);color:var(--text-muted)}.badge-green{background:var(--green-bg);color:var(--green)}.badge-amber{background:var(--amber-bg);color:var(--amber)}.badge-red{background:var(--red-bg);color:var(--red)}.badge-blue{background:var(--blue-bg);color:var(--blue)}.badge-purple{background:var(--purple-bg);color:var(--purple)}.dot{background:var(--text-muted);border-radius:50%;flex-shrink:0;width:7px;height:7px}.dot-green{background:var(--green)}.dot-amber{background:var(--amber)}.dot-red{background:var(--red)}.dot-blue{background:var(--blue)}.kind{corner-shape:squircle;background:var(--bg-hover);color:var(--text-muted);letter-spacing:.03em;text-transform:uppercase;border-radius:8px;padding:0 5px;font-size:10px;font-weight:600}.kind-mutation{background:var(--purple-bg);color:var(--purple)}.kind-live{background:var(--blue-bg);color:var(--blue)}.kind-list{background:var(--green-bg);color:var(--green)}.paths{flex-wrap:wrap;gap:4px;display:flex}.path{corner-shape:squircle;background:var(--bg-hover);font-family:var(--mono);border-radius:8px;padding:0 6px;font-size:11.5px}.path.stale{background:var(--amber-bg);color:var(--amber)}.ref{corner-shape:squircle;background:var(--blue-bg);color:var(--blue);font-family:var(--mono);cursor:pointer;border:0;border-radius:8px;padding:0 5px;font-size:11.5px}.ref:hover{text-decoration:underline}.link{font-family:var(--mono);cursor:pointer;text-align:left;background:0 0;border:0;padding:0;font-size:12px}.link:hover{text-decoration:underline}.link-list{flex-direction:column;gap:4px;display:flex}.id-list{color:var(--text-muted);font-family:var(--mono);flex-direction:column;gap:3px;margin:0;padding-left:28px;font-size:11px;display:flex}.props{grid-template-columns:max-content 1fr;gap:6px 16px;margin:0;display:grid}.props dt{color:var(--text-muted)}.props dd{min-width:0;margin:0}.error{color:var(--red);font-family:var(--mono);white-space:pre-wrap;margin:0;font-size:12px}.json,.json-node,.json-row{font-family:var(--mono);font-size:12px}.json-row{white-space:nowrap;align-items:baseline;gap:2px;padding:1px 0;display:flex}.json-toggle{cursor:pointer;text-align:left;background:0 0;border:0;width:100%}.json-children{border-left:1px solid var(--border);margin-left:6px;padding-left:10px}.caret{width:12px;color:var(--text-muted);transition:transform .1s;display:inline-block}.caret.open{transform:rotate(90deg)}.json-key{color:var(--json-key)}.json-string{color:var(--json-string);white-space:pre-wrap;word-break:break-word}.json-number{color:var(--json-number)}.json-boolean{color:var(--json-boolean)}.json-null,.json-summary{color:var(--text-muted)}.log{flex-direction:column;height:100%;display:flex;overflow:auto}.changes{border-collapse:collapse;width:100%}.changes td{border-bottom:1px solid var(--border);vertical-align:baseline;padding:4px 10px}.changes td:first-child{white-space:nowrap;width:1%}.changes td:nth-child(2){width:1%}.waiting{text-align:center;flex-direction:column;align-items:center;max-width:520px;margin:0 auto;padding:48px 24px;display:flex}.waiting h2{margin:16px 0 4px;font-size:16px}.waiting p{color:var(--text-muted)}.code{border:1px solid var(--border);corner-shape:squircle;background:var(--bg-subtle);width:100%;font-family:var(--mono);text-align:left;border-radius:24px;padding:12px 14px;font-size:12px}@media (width<=720px){.master-detail{grid-template-rows:minmax(160px,40%) 1fr;grid-template-columns:1fr}.master{border-right:0;border-bottom:1px solid var(--border)}.stats{margin-left:0}}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>fate devtools</title>
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-B8BrSDrl.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-NKFD_JQx.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/lib/react.d.mts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { FateDevtoolsOptions } from "./index.mjs";
|
|
2
|
+
import { FateClient } from "@nkzw/fate";
|
|
3
|
+
/**
|
|
4
|
+
* Connects a fate client to the devtools while the calling component is mounted.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useFateDevtools(client: FateClient<any, any, any>, { logSize, name }?: FateDevtoolsOptions): void;
|
|
7
|
+
/**
|
|
8
|
+
* Connects the client from the nearest `<FateClient>` to the devtools. Renders nothing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function FateDevtools(props: FateDevtoolsOptions): null;
|
package/lib/react.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { t as connectFateDevtools } from "./src-BAd3o4sS.mjs";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { useFateClient } from "react-fate";
|
|
4
|
+
// src/react.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Connects a fate client to the devtools while the calling component is mounted.
|
|
7
|
+
*/
|
|
8
|
+
function useFateDevtools(client, { logSize, name } = {}) {
|
|
9
|
+
useEffect(() => connectFateDevtools(client, {
|
|
10
|
+
logSize,
|
|
11
|
+
name
|
|
12
|
+
}), [
|
|
13
|
+
client,
|
|
14
|
+
logSize,
|
|
15
|
+
name
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Connects the client from the nearest `<FateClient>` to the devtools. Renders nothing.
|
|
20
|
+
*/
|
|
21
|
+
function FateDevtools(props) {
|
|
22
|
+
useFateDevtools(useFateClient(), props);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export { FateDevtools, useFateDevtools };
|
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
/**
|
|
3
|
+
* Messages exchanged between an app running fate and the devtools panel.
|
|
4
|
+
*
|
|
5
|
+
* Both sides talk over a `BroadcastChannel`, so the panel works inside the
|
|
6
|
+
* Vite DevTools dock (served from the dev server's origin) and as a standalone
|
|
7
|
+
* tab on the same origin.
|
|
8
|
+
*/
|
|
9
|
+
const channelName = "fate-devtools";
|
|
10
|
+
/** The app page exposes its page id here so an embedded panel can pick its own clients. */
|
|
11
|
+
const pageIdGlobal = "__FATE_DEVTOOLS_PAGE_ID__";
|
|
12
|
+
const isPanelMessage = (value) => typeof value === "object" && value !== null && value.source === "fate-devtools:panel";
|
|
13
|
+
const listKeySeparator = " __fate__ ";
|
|
14
|
+
const decodeListKeyPart = (part) => {
|
|
15
|
+
try {
|
|
16
|
+
return decodeURIComponent(part);
|
|
17
|
+
} catch {
|
|
18
|
+
return part;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Splits a fate list key (`owner __fate__ field __fate__ hash`) into its parts.
|
|
23
|
+
* Root lists are owned by `__root__`.
|
|
24
|
+
*/
|
|
25
|
+
const parseListKey = (key) => {
|
|
26
|
+
const parts = key.split(listKeySeparator);
|
|
27
|
+
return parts.length >= 3 ? {
|
|
28
|
+
field: decodeListKeyPart(parts[1]),
|
|
29
|
+
hash: decodeListKeyPart(parts[2]),
|
|
30
|
+
owner: decodeListKeyPart(parts[0])
|
|
31
|
+
} : {
|
|
32
|
+
field: key,
|
|
33
|
+
hash: "default",
|
|
34
|
+
owner: ""
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
// src/serialize.ts
|
|
38
|
+
const maxDepth = 12;
|
|
39
|
+
const maxArrayLength = 200;
|
|
40
|
+
const maxStringLength = 2e3;
|
|
41
|
+
/**
|
|
42
|
+
* Converts arbitrary values into plain JSON that can cross a
|
|
43
|
+
* `BroadcastChannel`. Cycles, functions, class instances, and very large
|
|
44
|
+
* values are summarized instead of throwing.
|
|
45
|
+
*/
|
|
46
|
+
function toJSONValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
47
|
+
try {
|
|
48
|
+
return serialize(value, depth, seen);
|
|
49
|
+
} catch {
|
|
50
|
+
return "[Unserializable]";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function serialize(value, depth, seen) {
|
|
54
|
+
switch (typeof value) {
|
|
55
|
+
case "string": return value.length > maxStringLength ? `${value.slice(0, maxStringLength)}… (${value.length} chars)` : value;
|
|
56
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
57
|
+
case "boolean": return value;
|
|
58
|
+
case "bigint": return `${value}n`;
|
|
59
|
+
case "undefined": return null;
|
|
60
|
+
case "symbol": return value.toString();
|
|
61
|
+
case "function": return `ƒ ${value.name || "anonymous"}()`;
|
|
62
|
+
}
|
|
63
|
+
if (value === null) return null;
|
|
64
|
+
const object = value;
|
|
65
|
+
const tag = Object.prototype.toString.call(object);
|
|
66
|
+
if (tag === "[object Date]") {
|
|
67
|
+
const date = object;
|
|
68
|
+
return Number.isNaN(date.getTime()) ? "Invalid Date" : date.toISOString();
|
|
69
|
+
}
|
|
70
|
+
if (tag === "[object Error]") {
|
|
71
|
+
const error = object;
|
|
72
|
+
return {
|
|
73
|
+
message: error.message,
|
|
74
|
+
name: error.name
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (seen.has(object)) return "[Circular]";
|
|
78
|
+
if (depth >= maxDepth) return Array.isArray(object) ? `[Array(${object.length})]` : "[Object]";
|
|
79
|
+
const ref = getNodeRef(object);
|
|
80
|
+
if (ref !== void 0) return { $ref: ref };
|
|
81
|
+
seen.add(object);
|
|
82
|
+
try {
|
|
83
|
+
if (tag === "[object Map]") return Object.fromEntries([...object].slice(0, maxArrayLength).map(([key, entry]) => [String(key), toJSONValue(entry, depth + 1, seen)]));
|
|
84
|
+
if (tag === "[object Set]" || Array.isArray(object)) {
|
|
85
|
+
const items = [...object];
|
|
86
|
+
const result = items.slice(0, maxArrayLength).map((entry) => toJSONValue(entry, depth + 1, seen));
|
|
87
|
+
return items.length > maxArrayLength ? [...result, `… ${items.length - maxArrayLength} more`] : result;
|
|
88
|
+
}
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const key of Object.keys(object)) {
|
|
91
|
+
let entry;
|
|
92
|
+
try {
|
|
93
|
+
entry = object[key];
|
|
94
|
+
} catch (error) {
|
|
95
|
+
entry = error;
|
|
96
|
+
}
|
|
97
|
+
result[key] = toJSONValue(entry, depth + 1, seen);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
} finally {
|
|
101
|
+
seen.delete(object);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* fate stores relations as frozen objects keyed by a private symbol. The
|
|
106
|
+
* devtools turn them into `{ $ref: 'Type:id' }` so the panel can link to them.
|
|
107
|
+
*/
|
|
108
|
+
const getNodeRef = (object) => {
|
|
109
|
+
for (const symbol of Object.getOwnPropertySymbols(object)) if (symbol.description === "__fate__node-ref") {
|
|
110
|
+
const id = object[symbol];
|
|
111
|
+
return typeof id === "string" ? id : void 0;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const errorMessage = (error) => error instanceof Error ? error.message : typeof error === "string" ? error : JSON.stringify(toJSONValue(error));
|
|
115
|
+
// src/index.ts
|
|
116
|
+
const operationKinds = /* @__PURE__ */ new Set([
|
|
117
|
+
"fetchById",
|
|
118
|
+
"fetchList",
|
|
119
|
+
"fetchQuery",
|
|
120
|
+
"mutate",
|
|
121
|
+
"mutateDurably",
|
|
122
|
+
"subscribeById",
|
|
123
|
+
"subscribeConnection"
|
|
124
|
+
]);
|
|
125
|
+
const watcherTimeout = 5e3;
|
|
126
|
+
const snapshotDelay = 120;
|
|
127
|
+
const maxRecords = 5e3;
|
|
128
|
+
const pageId = Math.random().toString(36).slice(2, 10);
|
|
129
|
+
let nextClientId = 0;
|
|
130
|
+
const parseEntityId = (id) => {
|
|
131
|
+
const index = id.indexOf(":");
|
|
132
|
+
return index < 0 ? {
|
|
133
|
+
rawId: id,
|
|
134
|
+
type: ""
|
|
135
|
+
} : {
|
|
136
|
+
rawId: id.slice(index + 1),
|
|
137
|
+
type: id.slice(0, index)
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const nestedKeys = (value, prefix = "") => {
|
|
141
|
+
if (!value || typeof value !== "object") return [];
|
|
142
|
+
return Object.entries(value).flatMap(([key, entry]) => typeof entry === "function" ? [`${prefix}${key}`] : nestedKeys(entry, `${prefix}${key}.`));
|
|
143
|
+
};
|
|
144
|
+
const toPaths = (select) => {
|
|
145
|
+
if (!select || typeof select !== "object" || !(Symbol.iterator in select)) return [];
|
|
146
|
+
return [...select].map(String).sort();
|
|
147
|
+
};
|
|
148
|
+
/** Keeps a bounded log, dropping the oldest entries first. */
|
|
149
|
+
var RingLog = class {
|
|
150
|
+
constructor(size) {
|
|
151
|
+
this.size = size;
|
|
152
|
+
this.entries = [];
|
|
153
|
+
}
|
|
154
|
+
push(entry) {
|
|
155
|
+
this.entries.push(entry);
|
|
156
|
+
if (this.entries.length > this.size) this.entries.splice(0, this.entries.length - this.size);
|
|
157
|
+
}
|
|
158
|
+
clear() {
|
|
159
|
+
this.entries = [];
|
|
160
|
+
}
|
|
161
|
+
values() {
|
|
162
|
+
return this.entries;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const connections = /* @__PURE__ */ new WeakMap();
|
|
166
|
+
/**
|
|
167
|
+
* Connects a fate client to the devtools panel. Returns a function that
|
|
168
|
+
* disconnects it again and restores the client's original transport.
|
|
169
|
+
*
|
|
170
|
+
* Connecting the same client more than once is safe; the client stays
|
|
171
|
+
* connected until every returned disposer has been called.
|
|
172
|
+
*
|
|
173
|
+
* The devtools are intended for development. Guard the call with
|
|
174
|
+
* `import.meta.env.DEV` (or your bundler's equivalent) to keep them out of
|
|
175
|
+
* production bundles.
|
|
176
|
+
*/
|
|
177
|
+
function connectFateDevtools(client, options = {}) {
|
|
178
|
+
if (typeof BroadcastChannel === "undefined") return () => {};
|
|
179
|
+
const existing = connections.get(client);
|
|
180
|
+
if (existing) {
|
|
181
|
+
existing.refCount++;
|
|
182
|
+
return once(() => release(client, existing));
|
|
183
|
+
}
|
|
184
|
+
const connection = {
|
|
185
|
+
dispose: attach(client, options),
|
|
186
|
+
refCount: 1
|
|
187
|
+
};
|
|
188
|
+
connections.set(client, connection);
|
|
189
|
+
return once(() => release(client, connection));
|
|
190
|
+
}
|
|
191
|
+
const once = (fn) => {
|
|
192
|
+
let called = false;
|
|
193
|
+
return () => {
|
|
194
|
+
if (!called) {
|
|
195
|
+
called = true;
|
|
196
|
+
fn();
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const release = (client, connection) => {
|
|
201
|
+
connection.refCount--;
|
|
202
|
+
if (connection.refCount === 0) {
|
|
203
|
+
connections.delete(client);
|
|
204
|
+
connection.dispose();
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
function attach(client, options) {
|
|
208
|
+
const logSize = options.logSize ?? 300;
|
|
209
|
+
globalThis[pageIdGlobal] = pageId;
|
|
210
|
+
const summary = {
|
|
211
|
+
id: `${pageId}:${nextClientId++}`,
|
|
212
|
+
name: options.name ?? "fate",
|
|
213
|
+
pageId,
|
|
214
|
+
url: typeof location === "undefined" ? "" : location.href
|
|
215
|
+
};
|
|
216
|
+
const channel = new BroadcastChannel(channelName);
|
|
217
|
+
const operations = new RingLog(logSize);
|
|
218
|
+
const changes = new RingLog(logSize);
|
|
219
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
220
|
+
let nextOperationId = 0;
|
|
221
|
+
let nextChangeId = 0;
|
|
222
|
+
let snapshotTimer;
|
|
223
|
+
let disposed = false;
|
|
224
|
+
const post = (message) => {
|
|
225
|
+
if (!disposed) channel.postMessage(message);
|
|
226
|
+
};
|
|
227
|
+
const isWatched = () => {
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
for (const [watcher, lastSeen] of watchers) if (now - lastSeen > watcherTimeout) watchers.delete(watcher);
|
|
230
|
+
return watchers.size > 0;
|
|
231
|
+
};
|
|
232
|
+
const sendSnapshot = () => {
|
|
233
|
+
snapshotTimer = void 0;
|
|
234
|
+
if (!disposed && isWatched()) post({
|
|
235
|
+
snapshot: takeSnapshot(),
|
|
236
|
+
source: "fate-devtools:app",
|
|
237
|
+
type: "snapshot"
|
|
238
|
+
});
|
|
239
|
+
};
|
|
240
|
+
const scheduleSnapshot = () => {
|
|
241
|
+
if (snapshotTimer === void 0 && watchers.size > 0) snapshotTimer = setTimeout(sendSnapshot, snapshotDelay);
|
|
242
|
+
};
|
|
243
|
+
const originalTransport = client.transport;
|
|
244
|
+
if (originalTransport) {
|
|
245
|
+
const wrapped = /* @__PURE__ */ new Map();
|
|
246
|
+
client.transport = new Proxy(originalTransport, { get(target, property, receiver) {
|
|
247
|
+
const value = Reflect.get(target, property, receiver);
|
|
248
|
+
if (typeof value !== "function" || !operationKinds.has(property)) return value;
|
|
249
|
+
let fn = wrapped.get(property);
|
|
250
|
+
if (!fn) {
|
|
251
|
+
fn = instrument(property, value.bind(target));
|
|
252
|
+
wrapped.set(property, fn);
|
|
253
|
+
}
|
|
254
|
+
return fn;
|
|
255
|
+
} });
|
|
256
|
+
}
|
|
257
|
+
function instrument(kind, fn) {
|
|
258
|
+
return (...args) => {
|
|
259
|
+
const entry = startOperation(kind, args);
|
|
260
|
+
operations.push(entry);
|
|
261
|
+
scheduleSnapshot();
|
|
262
|
+
const settle = (status, value) => {
|
|
263
|
+
entry.endedAt = Date.now();
|
|
264
|
+
entry.status = status;
|
|
265
|
+
if (status === "error") entry.error = errorMessage(value);
|
|
266
|
+
else entry.result = toJSONValue(value);
|
|
267
|
+
scheduleSnapshot();
|
|
268
|
+
};
|
|
269
|
+
if (kind === "subscribeById" || kind === "subscribeConnection") {
|
|
270
|
+
const handlerIndex = kind === "subscribeById" ? 4 : 5;
|
|
271
|
+
const handlers = args[handlerIndex];
|
|
272
|
+
if (handlers) {
|
|
273
|
+
const event = (name) => (...eventArgs) => {
|
|
274
|
+
entry.events = (entry.events ?? 0) + 1;
|
|
275
|
+
entry.result = toJSONValue(eventArgs[0]);
|
|
276
|
+
scheduleSnapshot();
|
|
277
|
+
return handlers[name]?.(...eventArgs);
|
|
278
|
+
};
|
|
279
|
+
args[handlerIndex] = {
|
|
280
|
+
...handlers,
|
|
281
|
+
...handlers.onData && { onData: event("onData") },
|
|
282
|
+
...handlers.onDelete && { onDelete: event("onDelete") },
|
|
283
|
+
...handlers.onEvent && { onEvent: event("onEvent") },
|
|
284
|
+
onError: (error) => {
|
|
285
|
+
entry.error = errorMessage(error);
|
|
286
|
+
entry.status = "error";
|
|
287
|
+
scheduleSnapshot();
|
|
288
|
+
handlers.onError?.(error);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const unsubscribe = fn(...args);
|
|
294
|
+
entry.status = "live";
|
|
295
|
+
return () => {
|
|
296
|
+
entry.endedAt = Date.now();
|
|
297
|
+
if (entry.status === "live") entry.status = "closed";
|
|
298
|
+
scheduleSnapshot();
|
|
299
|
+
return unsubscribe();
|
|
300
|
+
};
|
|
301
|
+
} catch (error) {
|
|
302
|
+
settle("error", error);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const result = fn(...args);
|
|
308
|
+
Promise.resolve(result).then((value) => settle("success", value), (error) => settle("error", error));
|
|
309
|
+
return result;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
settle("error", error);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function startOperation(kind, args) {
|
|
317
|
+
const base = {
|
|
318
|
+
id: nextOperationId++,
|
|
319
|
+
kind,
|
|
320
|
+
startedAt: Date.now(),
|
|
321
|
+
status: "pending"
|
|
322
|
+
};
|
|
323
|
+
switch (kind) {
|
|
324
|
+
case "fetchById": return {
|
|
325
|
+
...base,
|
|
326
|
+
args: toJSONValue(args[3]),
|
|
327
|
+
input: toJSONValue({ ids: args[1] }),
|
|
328
|
+
name: String(args[0]),
|
|
329
|
+
select: toPaths(args[2])
|
|
330
|
+
};
|
|
331
|
+
case "fetchList":
|
|
332
|
+
case "fetchQuery": return {
|
|
333
|
+
...base,
|
|
334
|
+
args: toJSONValue(args[2]),
|
|
335
|
+
name: String(args[0]),
|
|
336
|
+
select: toPaths(args[1])
|
|
337
|
+
};
|
|
338
|
+
case "mutate": return {
|
|
339
|
+
...base,
|
|
340
|
+
args: toJSONValue(args[3]),
|
|
341
|
+
input: toJSONValue(args[1]),
|
|
342
|
+
name: String(args[0]),
|
|
343
|
+
select: toPaths(args[2])
|
|
344
|
+
};
|
|
345
|
+
case "mutateDurably": return {
|
|
346
|
+
...base,
|
|
347
|
+
args: toJSONValue(args[4]),
|
|
348
|
+
input: toJSONValue({
|
|
349
|
+
identity: args[3],
|
|
350
|
+
input: args[1]
|
|
351
|
+
}),
|
|
352
|
+
name: String(args[0]),
|
|
353
|
+
select: toPaths(args[2])
|
|
354
|
+
};
|
|
355
|
+
case "subscribeById": return {
|
|
356
|
+
...base,
|
|
357
|
+
args: toJSONValue(args[3]),
|
|
358
|
+
input: toJSONValue({ id: args[1] }),
|
|
359
|
+
name: String(args[0]),
|
|
360
|
+
select: toPaths(args[2])
|
|
361
|
+
};
|
|
362
|
+
case "subscribeConnection": return {
|
|
363
|
+
...base,
|
|
364
|
+
args: toJSONValue({
|
|
365
|
+
args: args[2],
|
|
366
|
+
selectionArgs: args[4]
|
|
367
|
+
}),
|
|
368
|
+
input: toJSONValue({ type: args[1] }),
|
|
369
|
+
name: String(args[0]),
|
|
370
|
+
select: toPaths(args[3])
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const store = client.store;
|
|
375
|
+
const hadOwnOnChange = !!store && Object.hasOwn(store, "onChange");
|
|
376
|
+
const originalOnChange = store?.onChange;
|
|
377
|
+
if (store) store.onChange = (change, owner) => {
|
|
378
|
+
try {
|
|
379
|
+
changes.push({
|
|
380
|
+
at: Date.now(),
|
|
381
|
+
id: nextChangeId++,
|
|
382
|
+
...change.invalidated && { invalidated: true },
|
|
383
|
+
key: String(change.key),
|
|
384
|
+
kind: change.kind,
|
|
385
|
+
optimistic: !!store.isRecordingOptimistic || !!store.hasOptimisticUpdates,
|
|
386
|
+
...change.paths && { paths: [...change.paths].map(String) }
|
|
387
|
+
});
|
|
388
|
+
scheduleSnapshot();
|
|
389
|
+
} catch {}
|
|
390
|
+
return originalOnChange?.call(store, change, owner);
|
|
391
|
+
};
|
|
392
|
+
const unsubscribePersistence = client.persistence?.subscribe?.(scheduleSnapshot);
|
|
393
|
+
function takeSnapshot() {
|
|
394
|
+
const state = store?.dehydrate?.() ?? {
|
|
395
|
+
coverage: [],
|
|
396
|
+
lists: [],
|
|
397
|
+
records: []
|
|
398
|
+
};
|
|
399
|
+
const confirmed = store?.hasOptimisticUpdates ? store.dehydrateConfirmed?.() : void 0;
|
|
400
|
+
const confirmedRecords = confirmed ? new Map(confirmed.records) : void 0;
|
|
401
|
+
const confirmedLists = confirmed ? new Map(confirmed.lists) : void 0;
|
|
402
|
+
const coverage = new Map(state.coverage);
|
|
403
|
+
const stale = new Map(state.stale ?? []);
|
|
404
|
+
const records = state.records.slice(0, maxRecords).map(([id, record]) => {
|
|
405
|
+
const { rawId, type } = parseEntityId(String(id));
|
|
406
|
+
return {
|
|
407
|
+
coverage: coverage.get(id) ?? [],
|
|
408
|
+
id,
|
|
409
|
+
lists: store?.getListKeysForEntity?.(id) ?? [],
|
|
410
|
+
optimistic: !!confirmedRecords && confirmedRecords.get(id) !== record,
|
|
411
|
+
rawId,
|
|
412
|
+
record: toJSONValue(record),
|
|
413
|
+
stale: stale.get(id) ?? [],
|
|
414
|
+
type
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
const lists = state.lists.map(([key, list]) => {
|
|
418
|
+
const { field, owner } = parseListKey(key);
|
|
419
|
+
return {
|
|
420
|
+
field,
|
|
421
|
+
ids: list.ids.map(String),
|
|
422
|
+
key,
|
|
423
|
+
optimistic: !!confirmedLists && confirmedLists.get(key) !== list,
|
|
424
|
+
owner,
|
|
425
|
+
...list.pagination !== void 0 && { pagination: toJSONValue(list.pagination) },
|
|
426
|
+
state: toJSONValue(list)
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
changes: [...changes.values()],
|
|
431
|
+
...typeof client.hydrationScope === "string" && { hydrationScope: client.hydrationScope },
|
|
432
|
+
id: summary.id,
|
|
433
|
+
lists,
|
|
434
|
+
live: [...client.liveSubscriptions ?? []].map(([key, subscription]) => ({
|
|
435
|
+
key,
|
|
436
|
+
subscribers: subscription.count ?? 0
|
|
437
|
+
})),
|
|
438
|
+
mutations: nestedKeys(client.mutations).sort(),
|
|
439
|
+
name: summary.name,
|
|
440
|
+
operations: operations.values().map((entry) => ({ ...entry })),
|
|
441
|
+
optimisticUpdates: !!store?.hasOptimisticUpdates,
|
|
442
|
+
...client.persistence?.getSnapshot && { persistence: toJSONValue(client.persistence.getSnapshot()) },
|
|
443
|
+
records,
|
|
444
|
+
requests: getRequests(client),
|
|
445
|
+
roots: Object.keys(client.roots ?? {}).sort(),
|
|
446
|
+
takenAt: Date.now(),
|
|
447
|
+
types: [...client.types?.keys() ?? []].sort()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function runAction(action) {
|
|
451
|
+
switch (action.type) {
|
|
452
|
+
case "gc":
|
|
453
|
+
client.gc?.();
|
|
454
|
+
break;
|
|
455
|
+
case "clear-log":
|
|
456
|
+
operations.clear();
|
|
457
|
+
changes.clear();
|
|
458
|
+
break;
|
|
459
|
+
case "invalidate-record":
|
|
460
|
+
store?.invalidateRecord?.(action.id);
|
|
461
|
+
break;
|
|
462
|
+
case "invalidate-all":
|
|
463
|
+
for (const [id] of store?.dehydrate?.().records ?? []) store?.invalidateRecord?.(id);
|
|
464
|
+
break;
|
|
465
|
+
case "delete-record": {
|
|
466
|
+
const { rawId, type } = parseEntityId(action.id);
|
|
467
|
+
client.deleteRecord?.(type, rawId);
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case "log-record": {
|
|
471
|
+
const record = store?.dehydrate?.().records.find(([id]) => id === action.id)?.[1];
|
|
472
|
+
globalThis.$fate = record;
|
|
473
|
+
console.log(`fate-devtools: ${action.id} (available as $fate)`, record);
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
scheduleSnapshot();
|
|
478
|
+
}
|
|
479
|
+
const onMessage = (event) => {
|
|
480
|
+
const message = event.data;
|
|
481
|
+
if (!isPanelMessage(message)) return;
|
|
482
|
+
if (message.type === "discover") {
|
|
483
|
+
post({
|
|
484
|
+
client: summary,
|
|
485
|
+
source: "fate-devtools:app",
|
|
486
|
+
type: "connected"
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (message.clientId !== summary.id) return;
|
|
491
|
+
switch (message.type) {
|
|
492
|
+
case "watch":
|
|
493
|
+
watchers.set(message.panelId, Date.now());
|
|
494
|
+
scheduleSnapshot();
|
|
495
|
+
break;
|
|
496
|
+
case "unwatch":
|
|
497
|
+
watchers.delete(message.panelId);
|
|
498
|
+
break;
|
|
499
|
+
case "action": try {
|
|
500
|
+
runAction(message.action);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
console.error("fate-devtools: Action failed.", error);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
channel.addEventListener("message", onMessage);
|
|
507
|
+
post({
|
|
508
|
+
client: summary,
|
|
509
|
+
source: "fate-devtools:app",
|
|
510
|
+
type: "connected"
|
|
511
|
+
});
|
|
512
|
+
const onPageHide = () => post({
|
|
513
|
+
clientId: summary.id,
|
|
514
|
+
source: "fate-devtools:app",
|
|
515
|
+
type: "disconnected"
|
|
516
|
+
});
|
|
517
|
+
globalThis.addEventListener?.("pagehide", onPageHide);
|
|
518
|
+
return () => {
|
|
519
|
+
onPageHide();
|
|
520
|
+
disposed = true;
|
|
521
|
+
globalThis.removeEventListener?.("pagehide", onPageHide);
|
|
522
|
+
if (snapshotTimer !== void 0) clearTimeout(snapshotTimer);
|
|
523
|
+
unsubscribePersistence?.();
|
|
524
|
+
channel.removeEventListener("message", onMessage);
|
|
525
|
+
channel.close();
|
|
526
|
+
if (originalTransport) client.transport = originalTransport;
|
|
527
|
+
if (store) {
|
|
528
|
+
if (hadOwnOnChange) store.onChange = originalOnChange;
|
|
529
|
+
else delete store.onChange;
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function getRequests(client) {
|
|
534
|
+
const retained = client.operationLifetime?.operations;
|
|
535
|
+
const entries = /* @__PURE__ */ new Map();
|
|
536
|
+
const add = (descriptor, mode, status) => {
|
|
537
|
+
const key = descriptor?.key;
|
|
538
|
+
if (!key) return;
|
|
539
|
+
const existing = entries.get(key);
|
|
540
|
+
const modes = existing?.modes ?? [];
|
|
541
|
+
entries.set(key, {
|
|
542
|
+
items: existing?.items ?? (descriptor.items ?? []).map((item) => ({
|
|
543
|
+
...item.argsPayload !== void 0 && { args: toJSONValue(item.argsPayload) },
|
|
544
|
+
...item.ids && { ids: item.ids.map(String) },
|
|
545
|
+
kind: String(item.kind ?? ""),
|
|
546
|
+
name: String(item.name ?? ""),
|
|
547
|
+
paths: toPaths(item.plan?.paths),
|
|
548
|
+
...item.procedure && { procedure: item.procedure },
|
|
549
|
+
type: String(item.type ?? "")
|
|
550
|
+
})),
|
|
551
|
+
key,
|
|
552
|
+
modes: mode ? [...modes, {
|
|
553
|
+
mode,
|
|
554
|
+
status: status ?? "unknown"
|
|
555
|
+
}] : modes,
|
|
556
|
+
retainers: retained?.get(key)?.refCount ?? 0
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
for (const [, modes] of client.requests ?? []) for (const [mode, handle] of modes) add(handle.descriptor, mode, handle.status);
|
|
560
|
+
for (const [, operation] of retained ?? []) add(operation.descriptor);
|
|
561
|
+
return [...entries.values()];
|
|
562
|
+
}
|
|
563
|
+
export { connectFateDevtools as t };
|