@treenity/react 3.0.1 → 3.0.2
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/README.md +1 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +33 -7
- package/dist/App.js.map +1 -1
- package/dist/ComponentSection.js +1 -1
- package/dist/ComponentSection.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +2 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +5 -0
- package/dist/cache.js.map +1 -1
- package/dist/client-tree.d.ts.map +1 -1
- package/dist/client-tree.js +2 -2
- package/dist/client-tree.js.map +1 -1
- package/dist/components/ui/button.d.ts +2 -2
- package/dist/components/ui/button.d.ts.map +1 -1
- package/dist/components/ui/button.js +3 -3
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/pagination.d.ts +2 -2
- package/dist/components/ui/pagination.d.ts.map +1 -1
- package/dist/components/ui/pagination.js +3 -3
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/events.d.ts +2 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +47 -2
- package/dist/events.js.map +1 -1
- package/dist/fiber-tree.d.ts.map +1 -1
- package/dist/fiber-tree.js.map +1 -1
- package/dist/hooks.d.ts +9 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +80 -5
- package/dist/hooks.js.map +1 -1
- package/dist/lib/minimd.d.ts.map +1 -1
- package/dist/lib/minimd.js +8 -1
- package/dist/lib/minimd.js.map +1 -1
- package/dist/lib/sanitize-href.d.ts +3 -0
- package/dist/lib/sanitize-href.d.ts.map +1 -0
- package/dist/lib/sanitize-href.js +14 -0
- package/dist/lib/sanitize-href.js.map +1 -0
- package/dist/main.js +8 -2
- package/dist/main.js.map +1 -1
- package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -1
- package/dist/mods/editor-ui/FieldLabel.js +2 -1
- package/dist/mods/editor-ui/FieldLabel.js.map +1 -1
- package/dist/mods/editor-ui/default-edit.js +4 -2
- package/dist/mods/editor-ui/default-edit.js.map +1 -1
- package/dist/mods/editor-ui/form-field.d.ts.map +1 -1
- package/dist/mods/editor-ui/form-field.js +3 -2
- package/dist/mods/editor-ui/form-field.js.map +1 -1
- package/dist/mods/editor-ui/type-picker.d.ts.map +1 -1
- package/dist/mods/editor-ui/type-picker.js +2 -1
- package/dist/mods/editor-ui/type-picker.js.map +1 -1
- package/dist/mods/treenity/preview.d.ts.map +1 -1
- package/dist/mods/treenity/preview.js +2 -3
- package/dist/mods/treenity/preview.js.map +1 -1
- package/dist/mods/treenity/ref-view.js +2 -1
- package/dist/mods/treenity/ref-view.js.map +1 -1
- package/dist/mods/treenity/seed.js +3 -2
- package/dist/mods/treenity/seed.js.map +1 -1
- package/dist/symbols.d.ts.map +1 -1
- package/dist/symbols.js +11 -5
- package/dist/symbols.js.map +1 -1
- package/package.json +4 -2
- package/src/App.tsx +29 -1
- package/src/ComponentSection.tsx +1 -1
- package/src/ErrorBoundary.tsx +6 -3
- package/src/cache.ts +7 -0
- package/src/client-tree.ts +7 -7
- package/src/components/ui/button.tsx +4 -5
- package/src/components/ui/pagination.tsx +4 -9
- package/src/components/ui/textarea.tsx +1 -1
- package/src/events.ts +46 -6
- package/src/fiber-tree.ts +3 -3
- package/src/hooks.ts +73 -4
- package/src/lib/minimd.ts +7 -1
- package/src/lib/sanitize-href.ts +13 -0
- package/src/main.tsx +11 -3
- package/src/mods/editor-ui/FieldLabel.tsx +5 -4
- package/src/mods/editor-ui/default-edit.tsx +6 -4
- package/src/mods/editor-ui/form-field.tsx +4 -2
- package/src/mods/editor-ui/type-picker.tsx +3 -2
- package/src/mods/treenity/preview.tsx +6 -7
- package/src/mods/treenity/ref-view.tsx +11 -6
- package/src/mods/treenity/seed.ts +3 -2
- package/src/symbols.ts +12 -5
- package/src/bind/bind.test.ts +0 -316
- package/src/cache.test.ts +0 -139
- package/src/client-tree.test.ts +0 -116
- package/src/optimistic.test.ts +0 -111
- package/src/remote-tree.test.ts +0 -142
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// Ref node/component view — shows target path, resolve button, inline preview
|
|
3
|
+
import { Button } from '#components/ui/button';
|
|
3
4
|
import { Render } from '#context';
|
|
4
5
|
import { usePath } from '#hooks';
|
|
5
6
|
import { register } from '@treenity/core';
|
|
@@ -21,7 +22,7 @@ function RefListItem({ value }) {
|
|
|
21
22
|
function RefDisplay({ target, onSelect }) {
|
|
22
23
|
const [resolved, setResolved] = useState(false);
|
|
23
24
|
const targetNode = usePath(resolved ? target : null);
|
|
24
|
-
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: "ref" }), _jsx("span", { className: "text-sm font-mono text-primary", children: target }), onSelect && (_jsx("
|
|
25
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-muted-foreground text-sm", children: "ref" }), _jsx("span", { className: "text-sm font-mono text-primary", children: target }), onSelect && (_jsx(Button, { variant: "outline", size: "sm", className: "h-auto px-2 py-0.5 text-xs", onClick: () => onSelect(target), children: "Go to" })), _jsx(Button, { variant: "outline", size: "sm", className: "h-auto px-2 py-0.5 text-xs", onClick: () => setResolved(!resolved), children: resolved ? 'Collapse' : 'Resolve' })] }), resolved && (_jsx("div", { className: "border border-border rounded p-3 bg-muted/30", children: targetNode ? (_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex items-center gap-2 text-xs text-muted-foreground mb-2", children: [_jsx("span", { className: "font-mono", children: targetNode.$type }), _jsx("span", { children: targetNode.$path })] }), _jsx(Render, { value: targetNode })] })) : (_jsx("div", { className: "text-sm text-muted-foreground", children: "Loading..." })) }))] }));
|
|
25
26
|
}
|
|
26
27
|
// ── Registration ──
|
|
27
28
|
register('ref', 'react', RefNodeView);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ref-view.js","sourceRoot":"","sources":["../../../src/mods/treenity/ref-view.tsx"],"names":[],"mappings":";AAAA,8EAA8E;AAE9E,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,OAAO,EAAiB,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAEjC,gEAAgE;AAEhE,SAAS,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAuD;IAC3F,MAAM,GAAG,GAAI,KAAa,CAAC,IAA0B,CAAC;IACtD,IAAI,CAAC,GAAG;QAAE,OAAO,cAAK,SAAS,EAAC,+BAA+B,sCAA4B,CAAC;IAE5F,OAAO,KAAC,UAAU,IAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,GAAI,CAAC;AACzD,CAAC;AAED,oDAAoD;AAEpD,SAAS,WAAW,CAAC,EAAE,KAAK,EAAuB;IACjD,MAAM,GAAG,GAAI,KAAa,CAAC,IAA0B,CAAC;IACtD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC;IAE1D,OAAO,CACL,8BACE,eAAM,SAAS,EAAC,YAAY,6BAAiB,EAC7C,eAAK,SAAS,EAAC,YAAY,aACzB,eAAM,SAAS,EAAC,YAAY,YAAE,IAAI,GAAQ,EAC1C,eAAM,SAAS,EAAC,YAAY,YAAE,GAAG,IAAI,KAAK,GAAQ,IAC9C,IACL,CACJ,CAAC;AACJ,CAAC;AAED,uBAAuB;AAEvB,SAAS,UAAU,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAsD;IAC1F,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAErD,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aAExB,eAAK,SAAS,EAAC,yBAAyB,aACtC,eAAM,SAAS,EAAC,+BAA+B,oBAAW,EAC1D,eAAM,SAAS,EAAC,gCAAgC,YAAE,MAAM,GAAQ,EAE/D,QAAQ,IAAI,CACX,
|
|
1
|
+
{"version":3,"file":"ref-view.js","sourceRoot":"","sources":["../../../src/mods/treenity/ref-view.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { R, S } from '@treenity/core';
|
|
1
|
+
import { A, R, S, W } from '@treenity/core';
|
|
2
2
|
import { registerPrefab } from '@treenity/core/mod';
|
|
3
3
|
registerPrefab('core', 'seed', [
|
|
4
4
|
{ $path: 'sys', $type: 'treenity.system' },
|
|
5
|
-
{ $path: 'auth', $type: 'dir' },
|
|
5
|
+
{ $path: 'auth', $type: 'dir', $acl: [{ g: 'admins', p: R | W | A | S }, { g: 'public', p: 0 }] },
|
|
6
6
|
{ $path: 'auth/users', $type: 'mount-point',
|
|
7
7
|
connection: { $type: 'connection', db: 'treenity', collection: 'users' },
|
|
8
8
|
mount: { $type: 't.mount.mongo' },
|
|
@@ -19,6 +19,7 @@ registerPrefab('core', 'seed', [
|
|
|
19
19
|
{ $path: 'auth/sessions', $type: 'mount-point',
|
|
20
20
|
connection: { $type: 'connection', db: 'treenity', collection: 'sessions' },
|
|
21
21
|
mount: { $type: 't.mount.mongo' },
|
|
22
|
+
$acl: [{ g: 'admins', p: R | W | A | S }, { g: 'authenticated', p: 0 }, { g: 'public', p: 0 }],
|
|
22
23
|
},
|
|
23
24
|
{ $path: 'mnt', $type: 'dir' },
|
|
24
25
|
{ $path: 'mnt/orders', $type: 't.mount.mongo',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../../src/mods/treenity/seed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,CAAC,EAAE,CAAC,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"seed.js","sourceRoot":"","sources":["../../../src/mods/treenity/seed.ts"],"names":[],"mappings":"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"}
|
package/dist/symbols.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AAIA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE5D,eAAO,MAAM,IAAI,eAA8B,CAAC;AAChD,eAAO,MAAM,KAAK,eAA+B,CAAC;AAMlD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAW9C"}
|
package/dist/symbols.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// Symbol-based component location metadata.
|
|
2
|
-
// Stamped on deserialization (cache.put).
|
|
2
|
+
// Stamped on deserialization (cache.put). Non-enumerable so they're
|
|
3
|
+
// invisible to structuredClone, spread, and JSON/keys/entries.
|
|
3
4
|
import { isComponent } from '@treenity/core';
|
|
4
5
|
export const $key = Symbol.for('treenity.$key');
|
|
5
6
|
export const $node = Symbol.for('treenity.$node');
|
|
7
|
+
function hide(obj, sym, value) {
|
|
8
|
+
Object.defineProperty(obj, sym, { value, enumerable: false, writable: false, configurable: true });
|
|
9
|
+
}
|
|
6
10
|
export function stampNode(node) {
|
|
7
|
-
node[$
|
|
8
|
-
|
|
11
|
+
if (node[$node] === node)
|
|
12
|
+
return;
|
|
13
|
+
hide(node, $key, '');
|
|
14
|
+
hide(node, $node, node);
|
|
9
15
|
for (const [k, v] of Object.entries(node)) {
|
|
10
16
|
if (k.startsWith('$') || !isComponent(v))
|
|
11
17
|
continue;
|
|
12
|
-
v
|
|
13
|
-
v
|
|
18
|
+
hide(v, $key, k);
|
|
19
|
+
hide(v, $node, node);
|
|
14
20
|
}
|
|
15
21
|
}
|
|
16
22
|
//# sourceMappingURL=symbols.js.map
|
package/dist/symbols.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"symbols.js","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,
|
|
1
|
+
{"version":3,"file":"symbols.js","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,oEAAoE;AACpE,+DAA+D;AAE/D,OAAO,EAAE,WAAW,EAAiB,MAAM,gBAAgB,CAAC;AAE5D,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAChD,MAAM,CAAC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;AAElD,SAAS,IAAI,CAAC,GAAW,EAAE,GAAW,EAAE,KAAc;IACpD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;AACrG,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,IAAK,IAAY,CAAC,KAAK,CAAC,KAAK,IAAI;QAAE,OAAO;IAE1C,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACrB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAExB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;YAAE,SAAS;QACnD,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACjB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IACvB,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treenity/react",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "React binding for Treenity — reactive hooks, admin UI, and context-based component rendering.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
|
-
"license": "
|
|
9
|
+
"license": "FSL-1.1-MIT",
|
|
10
10
|
"author": "Treenity <hello@treenity.ai> (https://treenity.ai)",
|
|
11
11
|
"homepage": "https://github.com/treenity-ai/treenity",
|
|
12
12
|
"repository": {
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"files": [
|
|
74
74
|
"dist",
|
|
75
75
|
"src",
|
|
76
|
+
"!src/**/*.test.ts",
|
|
77
|
+
"!src/**/*.test.tsx",
|
|
76
78
|
"LICENSE",
|
|
77
79
|
"README.md"
|
|
78
80
|
],
|
package/src/App.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from '
|
|
|
17
17
|
import { toast } from 'sonner';
|
|
18
18
|
import * as cache from './cache';
|
|
19
19
|
import { tree } from './client';
|
|
20
|
-
import { startEvents, stopEvents } from './events';
|
|
20
|
+
import { SSE_CONNECTED, SSE_DISCONNECTED, startEvents, stopEvents } from './events';
|
|
21
21
|
import { NavigateProvider } from './hooks';
|
|
22
22
|
import { Inspector } from './Inspector';
|
|
23
23
|
import { LoginModal, LoginScreen } from './Login';
|
|
@@ -109,6 +109,29 @@ export function App() {
|
|
|
109
109
|
const [filter, setFilter] = useState('');
|
|
110
110
|
const [showHidden, setShowHidden] = useState(false);
|
|
111
111
|
const [toastMsg, setToastMsg] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
|
112
|
+
const [sseDown, setSseDown] = useState(false);
|
|
113
|
+
const sseDownTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
114
|
+
|
|
115
|
+
// TODO: remove debounce, extract from App code, remove debounce
|
|
116
|
+
// SSE connection indicator — debounce disconnect by 2s to avoid flicker
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const onConnect = () => {
|
|
119
|
+
if (sseDownTimer.current) { clearTimeout(sseDownTimer.current); sseDownTimer.current = undefined; }
|
|
120
|
+
setSseDown(false);
|
|
121
|
+
};
|
|
122
|
+
const onDisconnect = () => {
|
|
123
|
+
if (!sseDownTimer.current) {
|
|
124
|
+
sseDownTimer.current = setTimeout(() => { sseDownTimer.current = undefined; setSseDown(true); }, 2000);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
window.addEventListener(SSE_CONNECTED, onConnect);
|
|
128
|
+
window.addEventListener(SSE_DISCONNECTED, onDisconnect);
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener(SSE_CONNECTED, onConnect);
|
|
131
|
+
window.removeEventListener(SSE_DISCONNECTED, onDisconnect);
|
|
132
|
+
if (sseDownTimer.current) clearTimeout(sseDownTimer.current);
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
112
135
|
|
|
113
136
|
// Granular: only re-render App when root node appears/disappears
|
|
114
137
|
const hasRootNode = useSyncExternalStore(
|
|
@@ -438,6 +461,11 @@ export function App() {
|
|
|
438
461
|
|
|
439
462
|
return (
|
|
440
463
|
<NavigateProvider value={navigate}>
|
|
464
|
+
{sseDown && (
|
|
465
|
+
<div className="fixed top-0 inset-x-0 z-50 bg-yellow-500 text-black text-center text-sm py-1">
|
|
466
|
+
Reconnecting to server…
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
441
469
|
<div className="flex h-screen bg-background text-foreground overflow-hidden">
|
|
442
470
|
<ResizablePanelGroup orientation="horizontal" className="h-full">
|
|
443
471
|
<ResizablePanel
|
package/src/ComponentSection.tsx
CHANGED
|
@@ -59,7 +59,7 @@ export function ComponentSection({
|
|
|
59
59
|
toast,
|
|
60
60
|
onActionComplete,
|
|
61
61
|
}: ComponentSectionProps) {
|
|
62
|
-
const isMain = !
|
|
62
|
+
const isMain = !name;
|
|
63
63
|
|
|
64
64
|
return (
|
|
65
65
|
<div className="border-t border-border mt-2 pt-0.5 first:border-t-0 first:mt-0 first:pt-0">
|
package/src/ErrorBoundary.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Button } from '#components/ui/button';
|
|
1
2
|
import { Component, type ReactNode } from 'react';
|
|
2
3
|
|
|
3
4
|
type Props = {
|
|
@@ -10,12 +11,14 @@ type State = { error: string | null };
|
|
|
10
11
|
const defaultFallback = (error: string, reset: () => void) => (
|
|
11
12
|
<div className="rounded border border-destructive/30 bg-destructive/5 p-2 text-[11px]">
|
|
12
13
|
<div className="text-destructive font-mono break-all">{error}</div>
|
|
13
|
-
<
|
|
14
|
-
|
|
14
|
+
<Button
|
|
15
|
+
variant="link"
|
|
16
|
+
size="sm"
|
|
17
|
+
className="mt-1 h-auto p-0 text-[10px] text-muted-foreground hover:text-foreground underline"
|
|
15
18
|
onClick={reset}
|
|
16
19
|
>
|
|
17
20
|
Retry
|
|
18
|
-
</
|
|
21
|
+
</Button>
|
|
19
22
|
</div>
|
|
20
23
|
);
|
|
21
24
|
|
package/src/cache.ts
CHANGED
|
@@ -6,6 +6,10 @@ import type { NodeData } from '@treenity/core';
|
|
|
6
6
|
import * as idb from './idb';
|
|
7
7
|
import { stampNode } from './symbols';
|
|
8
8
|
|
|
9
|
+
/** Shallow-freeze in dev mode to catch accidental cache mutation at the source */
|
|
10
|
+
const devFreeze: (node: NodeData) => void =
|
|
11
|
+
import.meta.env?.DEV ? (node) => Object.freeze(node) : () => {};
|
|
12
|
+
|
|
9
13
|
type Sub = () => void;
|
|
10
14
|
|
|
11
15
|
const nodes = new Map<string, NodeData>();
|
|
@@ -112,6 +116,7 @@ export function removeFromParent(path: string, parent: string) {
|
|
|
112
116
|
export function put(node: NodeData, virtualParent?: string) {
|
|
113
117
|
stampNode(node);
|
|
114
118
|
nodes.set(node.$path, node);
|
|
119
|
+
devFreeze(node);
|
|
115
120
|
const p = virtualParent ?? parentOf(node.$path);
|
|
116
121
|
if (p !== null) {
|
|
117
122
|
if (!parentIndex.has(p)) parentIndex.set(p, new Set());
|
|
@@ -139,6 +144,7 @@ export function putMany(items: NodeData[], virtualParent?: string) {
|
|
|
139
144
|
for (const n of items) {
|
|
140
145
|
stampNode(n);
|
|
141
146
|
nodes.set(n.$path, n);
|
|
147
|
+
devFreeze(n);
|
|
142
148
|
lastUpdated.set(n.$path, ts);
|
|
143
149
|
fire(pathSubs, n.$path);
|
|
144
150
|
const p = virtualParent ?? parentOf(n.$path);
|
|
@@ -230,6 +236,7 @@ export async function hydrate(): Promise<void> {
|
|
|
230
236
|
for (const { data, lastUpdated: ts, virtualParent } of entries) {
|
|
231
237
|
stampNode(data);
|
|
232
238
|
nodes.set(data.$path, data);
|
|
239
|
+
devFreeze(data);
|
|
233
240
|
lastUpdated.set(data.$path, ts);
|
|
234
241
|
const p = virtualParent ?? parentOf(data.$path);
|
|
235
242
|
if (p !== null) {
|
package/src/client-tree.ts
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// FilterTree: /local/* → memory+mounts, everything else → remote.
|
|
3
3
|
// Reads merge both — /local visible alongside server nodes.
|
|
4
4
|
|
|
5
|
-
import type { NodeData } from '@treenity/core'
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import '
|
|
10
|
-
import { createRemoteTree } from './remote-tree'
|
|
11
|
-
import type { trpc } from './trpc'
|
|
5
|
+
import type { NodeData } from '@treenity/core';
|
|
6
|
+
import { withMounts } from '@treenity/core/server/mount';
|
|
7
|
+
import './fiber-tree'; // registers t.mount.react
|
|
8
|
+
import { createFilterTree, createMemoryTree } from '@treenity/core/tree';
|
|
9
|
+
import { withCache } from '@treenity/core/tree/cache';
|
|
10
|
+
import { createRemoteTree } from './remote-tree';
|
|
11
|
+
import type { trpc } from './trpc';
|
|
12
12
|
|
|
13
13
|
type TrpcClient = typeof trpc
|
|
14
14
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { cva, type VariantProps } from
|
|
3
|
-
import { Slot } from
|
|
4
|
-
|
|
5
|
-
import { cn } from "#components/lib/utils"
|
|
1
|
+
import { cn } from '#components/lib/utils';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { Slot } from 'radix-ui';
|
|
4
|
+
import * as React from 'react';
|
|
6
5
|
|
|
7
6
|
const buttonVariants = cva(
|
|
8
7
|
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
MoreHorizontalIcon,
|
|
6
|
-
} from "lucide-react"
|
|
7
|
-
|
|
8
|
-
import { cn } from "#components/lib/utils"
|
|
9
|
-
import { buttonVariants, type Button } from "#components/ui/button"
|
|
1
|
+
import { cn } from '#components/lib/utils';
|
|
2
|
+
import { type Button, buttonVariants } from '#components/ui/button';
|
|
3
|
+
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
10
5
|
|
|
11
6
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
|
12
7
|
return (
|
|
@@ -6,7 +6,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|
|
6
6
|
<textarea
|
|
7
7
|
data-slot="textarea"
|
|
8
8
|
className={cn(
|
|
9
|
-
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
9
|
+
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 max-h-64 overflow-y-auto w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
10
10
|
className
|
|
11
11
|
)}
|
|
12
12
|
{...props}
|
package/src/events.ts
CHANGED
|
@@ -14,14 +14,44 @@ interface EventsConfig {
|
|
|
14
14
|
getSelected: () => string | null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// ── SSE connection events (consumed by App.tsx) ──
|
|
18
|
+
|
|
19
|
+
export const SSE_CONNECTED = 'sse-connected';
|
|
20
|
+
export const SSE_DISCONNECTED = 'sse-disconnected';
|
|
21
|
+
|
|
17
22
|
let unsub: (() => void) | null = null;
|
|
23
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
let lastConfig: EventsConfig | null = null;
|
|
18
25
|
|
|
19
26
|
export function startEvents(config: EventsConfig) {
|
|
20
27
|
stopEvents();
|
|
28
|
+
lastConfig = config;
|
|
21
29
|
|
|
22
30
|
const { loadChildren, getExpanded, getSelected } = config;
|
|
23
31
|
|
|
24
32
|
const sub = trpc.events.subscribe(undefined as void, {
|
|
33
|
+
onStarted() {
|
|
34
|
+
window.dispatchEvent(new Event(SSE_CONNECTED));
|
|
35
|
+
},
|
|
36
|
+
onConnectionStateChange(state: { state: string }) {
|
|
37
|
+
if (state.state === 'connecting') {
|
|
38
|
+
window.dispatchEvent(new Event(SSE_DISCONNECTED));
|
|
39
|
+
} else if (state.state === 'pending') {
|
|
40
|
+
// 'pending' = connected and waiting for data — SSE is alive
|
|
41
|
+
window.dispatchEvent(new Event(SSE_CONNECTED));
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
onError(err: unknown) {
|
|
45
|
+
console.error('[sse] subscription error (non-retryable):', err);
|
|
46
|
+
window.dispatchEvent(new Event(SSE_DISCONNECTED));
|
|
47
|
+
// tRPC exhausted retries — schedule a single delayed re-subscribe
|
|
48
|
+
scheduleResubscribe();
|
|
49
|
+
},
|
|
50
|
+
onStopped() {
|
|
51
|
+
// Server closed the stream — schedule re-subscribe
|
|
52
|
+
window.dispatchEvent(new Event(SSE_DISCONNECTED));
|
|
53
|
+
scheduleResubscribe();
|
|
54
|
+
},
|
|
25
55
|
onData(event) {
|
|
26
56
|
if (event.type === 'reconnect') {
|
|
27
57
|
if (!event.preserved) {
|
|
@@ -45,8 +75,9 @@ export function startEvents(config: EventsConfig) {
|
|
|
45
75
|
const existing = cache.get(event.path);
|
|
46
76
|
if (existing && event.patches) {
|
|
47
77
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
const patched = structuredClone(existing);
|
|
79
|
+
applyPatch(patched, event.patches as Operation[]);
|
|
80
|
+
cache.put(patched);
|
|
50
81
|
} catch (e) {
|
|
51
82
|
console.error('Failed to apply patches, fetching full node:', e);
|
|
52
83
|
trpc.get.query({ path: event.path }).then((n) => {
|
|
@@ -73,9 +104,18 @@ export function startEvents(config: EventsConfig) {
|
|
|
73
104
|
unsub = () => sub.unsubscribe();
|
|
74
105
|
}
|
|
75
106
|
|
|
107
|
+
const RESUBSCRIBE_DELAY = 5_000;
|
|
108
|
+
|
|
109
|
+
function scheduleResubscribe() {
|
|
110
|
+
if (reconnectTimer || !lastConfig) return;
|
|
111
|
+
console.log(`[sse] will re-subscribe in ${RESUBSCRIBE_DELAY}ms`);
|
|
112
|
+
reconnectTimer = setTimeout(() => {
|
|
113
|
+
reconnectTimer = null;
|
|
114
|
+
if (lastConfig) startEvents(lastConfig);
|
|
115
|
+
}, RESUBSCRIBE_DELAY);
|
|
116
|
+
}
|
|
117
|
+
|
|
76
118
|
export function stopEvents() {
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
unsub = null;
|
|
80
|
-
}
|
|
119
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
120
|
+
if (unsub) { unsub(); unsub = null; }
|
|
81
121
|
}
|
package/src/fiber-tree.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Walks the live Fiber tree on each query — no state, no tracking.
|
|
3
3
|
// Registered as t.mount.react — isomorphic mount, same as server-side mounts.
|
|
4
4
|
|
|
5
|
-
import type { NodeData } from '@treenity/core'
|
|
6
|
-
import { register } from '@treenity/core'
|
|
7
|
-
import type { Tree } from '@treenity/core/tree'
|
|
5
|
+
import type { NodeData } from '@treenity/core';
|
|
6
|
+
import { register } from '@treenity/core';
|
|
7
|
+
import type { Tree } from '@treenity/core/tree';
|
|
8
8
|
|
|
9
9
|
const PREFIX = '/local/react'
|
|
10
10
|
|
package/src/hooks.ts
CHANGED
|
@@ -99,7 +99,10 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
99
99
|
|
|
100
100
|
trpc.getChildren
|
|
101
101
|
.query({ path: parentPath, limit: opts?.limit, watch: opts?.watch, watchNew: opts?.watchNew })
|
|
102
|
-
.then((result: any) =>
|
|
102
|
+
.then((result: any) => {
|
|
103
|
+
if (result.truncated) console.warn(`[tree] Children of ${parentPath} truncated — results may be incomplete`);
|
|
104
|
+
cache.putMany(result.items as NodeData[], parentPath);
|
|
105
|
+
});
|
|
103
106
|
}, [parentPath, gen, opts?.limit, opts?.watch, opts?.watchNew]);
|
|
104
107
|
|
|
105
108
|
return useSyncExternalStore(
|
|
@@ -111,10 +114,17 @@ export function useChildren(parentPath: string, opts?: WatchOpts) {
|
|
|
111
114
|
// ── set: optimistic update + server persist ──
|
|
112
115
|
|
|
113
116
|
export async function set(next: NodeData) {
|
|
117
|
+
const prev = cache.get(next.$path);
|
|
114
118
|
cache.put(next);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
try {
|
|
120
|
+
await tree.set(next);
|
|
121
|
+
const fresh = await tree.get(next.$path);
|
|
122
|
+
if (fresh) cache.put(fresh);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
// F15: rollback optimistic cache on server reject (validation, ACL, OCC)
|
|
125
|
+
if (prev) cache.put(prev); else cache.remove(next.$path);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
// ── execute: action caller ──
|
|
@@ -168,6 +178,65 @@ export function useCanWrite(path: string | null): boolean {
|
|
|
168
178
|
return (perm & W) !== 0;
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
// ── useAutoSave: throttled auto-persist for editable views ──
|
|
182
|
+
// First save fires after 500ms (responsive), subsequent saves throttled to 2s.
|
|
183
|
+
// Returns [localData, setField, dirty] — local updates are instant, server writes are batched.
|
|
184
|
+
|
|
185
|
+
// TODO: check why unused
|
|
186
|
+
export function useAutoSave(node: NodeData) {
|
|
187
|
+
const [local, setLocal] = useState<Record<string, unknown>>({});
|
|
188
|
+
const dirtyRef = useRef(false);
|
|
189
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
+
const savingRef = useRef(false);
|
|
191
|
+
const pendingRef = useRef(false);
|
|
192
|
+
const lastSaveRef = useRef(0);
|
|
193
|
+
const nodeRef = useRef(node);
|
|
194
|
+
nodeRef.current = node;
|
|
195
|
+
|
|
196
|
+
const flush = useCallback(async () => {
|
|
197
|
+
if (savingRef.current) { pendingRef.current = true; return; }
|
|
198
|
+
if (!dirtyRef.current) return;
|
|
199
|
+
savingRef.current = true;
|
|
200
|
+
try {
|
|
201
|
+
const merged = { ...nodeRef.current, ...local };
|
|
202
|
+
delete merged.$rev; // skip OCC — force-write
|
|
203
|
+
await set(merged);
|
|
204
|
+
dirtyRef.current = false;
|
|
205
|
+
lastSaveRef.current = Date.now();
|
|
206
|
+
} catch (e) {
|
|
207
|
+
console.error('[autoSave] failed:', e);
|
|
208
|
+
} finally {
|
|
209
|
+
savingRef.current = false;
|
|
210
|
+
if (pendingRef.current) {
|
|
211
|
+
pendingRef.current = false;
|
|
212
|
+
flush();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}, [local]);
|
|
216
|
+
|
|
217
|
+
const setField = useCallback((field: string, value: unknown) => {
|
|
218
|
+
setLocal(prev => ({ ...prev, [field]: value }));
|
|
219
|
+
dirtyRef.current = true;
|
|
220
|
+
|
|
221
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
222
|
+
const elapsed = Date.now() - lastSaveRef.current;
|
|
223
|
+
const delay = elapsed > 2000 ? 500 : 2000; // first batch fast, then throttle
|
|
224
|
+
timerRef.current = setTimeout(() => { timerRef.current = null; flush(); }, delay);
|
|
225
|
+
}, [flush]);
|
|
226
|
+
|
|
227
|
+
// Flush on unmount
|
|
228
|
+
useEffect(() => () => {
|
|
229
|
+
if (timerRef.current) { clearTimeout(timerRef.current); flush(); }
|
|
230
|
+
}, [flush]);
|
|
231
|
+
|
|
232
|
+
// Reset local on node path change
|
|
233
|
+
useEffect(() => { setLocal({}); dirtyRef.current = false; }, [node.$path]);
|
|
234
|
+
|
|
235
|
+
const merged = useMemo(() => ({ ...node, ...local }), [node, local]);
|
|
236
|
+
|
|
237
|
+
return [merged, setField, dirtyRef.current] as const;
|
|
238
|
+
}
|
|
239
|
+
|
|
171
240
|
// ── Internals ──
|
|
172
241
|
|
|
173
242
|
const AsyncGenFn = Object.getPrototypeOf(async function* () { }).constructor;
|
package/src/lib/minimd.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Minimal markdown → HTML for chat bubbles (~50 lines)
|
|
2
2
|
// Covers: headers, bold, italic, inline code, code blocks, links, lists, blockquotes, hr
|
|
3
3
|
import './minimd.css';
|
|
4
|
+
import { sanitizeHref } from './sanitize-href';
|
|
4
5
|
|
|
5
6
|
const esc = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
6
7
|
|
|
@@ -11,7 +12,12 @@ function inline(s: string): string {
|
|
|
11
12
|
.replace(/__(.+?)__/g, '<strong>$1</strong>')
|
|
12
13
|
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
13
14
|
.replace(/_(.+?)_/g, '<em>$1</em>')
|
|
14
|
-
.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
15
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
|
|
16
|
+
const safe = sanitizeHref(url);
|
|
17
|
+
if (!safe) return text;
|
|
18
|
+
const safeUrl = safe.replace(/"/g, '"');
|
|
19
|
+
return `<a href="${safeUrl}" target="_blank" rel="noopener">${text}</a>`;
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
export function minimd(src: string): string {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Allowlist-based href sanitizer — blocks javascript:, data:, vbscript:, etc.
|
|
2
|
+
// Strips control characters before protocol check to defeat browser bypass vectors
|
|
3
|
+
// (e.g. "java\tscript:" → browsers collapse to "javascript:")
|
|
4
|
+
|
|
5
|
+
const SAFE_PROTOCOL = /^(https?:|mailto:|tel:|\/|#)/i;
|
|
6
|
+
|
|
7
|
+
/** Returns sanitized URL string, or null if the protocol is unsafe. */
|
|
8
|
+
export function sanitizeHref(url: string): string | null {
|
|
9
|
+
const trimmed = url.replace(/[\x00-\x20]+/g, '');
|
|
10
|
+
if (!trimmed) return null;
|
|
11
|
+
if (SAFE_PROTOCOL.test(trimmed)) return url.trim();
|
|
12
|
+
return null;
|
|
13
|
+
}
|
package/src/main.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
2
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
3
3
|
import { enablePatches } from 'immer';
|
|
4
|
-
import { StrictMode } from 'react';
|
|
4
|
+
import { StrictMode, type ReactNode } from 'react';
|
|
5
5
|
import { createRoot } from 'react-dom/client';
|
|
6
6
|
import { App } from './App';
|
|
7
7
|
import './load-client';
|
|
@@ -12,13 +12,21 @@ enablePatches();
|
|
|
12
12
|
|
|
13
13
|
const queryClient = new QueryClient();
|
|
14
14
|
|
|
15
|
+
// StrictMode off: FlowGram inversify container breaks on double-mount
|
|
16
|
+
// https://github.com/bytedance/flowgram.ai/issues/402
|
|
17
|
+
// TODO: re-enable once FlowGram fixes React 19 StrictMode support
|
|
18
|
+
// const Strict = import.meta.env.VITE_STRICT_MODE !== 'false'
|
|
19
|
+
// ? StrictMode
|
|
20
|
+
// : ({ children }: { children: ReactNode }) => children;
|
|
21
|
+
const Strict = ({ children }: { children: ReactNode }) => children;
|
|
22
|
+
|
|
15
23
|
const root = document.getElementById('root');
|
|
16
24
|
if (!root) throw new Error('No #root element');
|
|
17
25
|
createRoot(root).render(
|
|
18
|
-
<
|
|
26
|
+
<Strict>
|
|
19
27
|
<QueryClientProvider client={queryClient}>
|
|
20
28
|
<App />
|
|
21
29
|
<Toaster />
|
|
22
30
|
</QueryClientProvider>
|
|
23
|
-
</
|
|
31
|
+
</Strict>,
|
|
24
32
|
);
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DropdownMenuSeparator,
|
|
9
9
|
DropdownMenuTrigger,
|
|
10
10
|
} from '#components/ui/dropdown-menu';
|
|
11
|
+
import { Input } from '#components/ui/input';
|
|
11
12
|
import { isRef } from '@treenity/core';
|
|
12
13
|
import { useState } from 'react';
|
|
13
14
|
|
|
@@ -101,8 +102,8 @@ export function RefEditor({ value, onChange }: {
|
|
|
101
102
|
<div className="flex flex-col gap-1 flex-1 min-w-0">
|
|
102
103
|
<div className="flex items-center gap-1">
|
|
103
104
|
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$ref</span>
|
|
104
|
-
<
|
|
105
|
-
className="flex-1 min-w-0"
|
|
105
|
+
<Input
|
|
106
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
106
107
|
value={value.$ref}
|
|
107
108
|
onChange={(e) => onChange(hasMap ? { $ref: e.target.value, $map: value.$map } : { $ref: e.target.value })}
|
|
108
109
|
placeholder="path"
|
|
@@ -111,8 +112,8 @@ export function RefEditor({ value, onChange }: {
|
|
|
111
112
|
{hasMap && (
|
|
112
113
|
<div className="flex items-center gap-1">
|
|
113
114
|
<span className="text-[9px] text-muted-foreground shrink-0 w-5">$map</span>
|
|
114
|
-
<
|
|
115
|
-
className="flex-1 min-w-0"
|
|
115
|
+
<Input
|
|
116
|
+
className="h-7 text-xs flex-1 min-w-0"
|
|
116
117
|
value={value.$map ?? ''}
|
|
117
118
|
onChange={(e) => onChange({ $ref: value.$ref, $map: e.target.value })}
|
|
118
119
|
placeholder="field"
|