@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.
Files changed (93) hide show
  1. package/README.md +1 -1
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +33 -7
  4. package/dist/App.js.map +1 -1
  5. package/dist/ComponentSection.js +1 -1
  6. package/dist/ComponentSection.js.map +1 -1
  7. package/dist/ErrorBoundary.d.ts.map +1 -1
  8. package/dist/ErrorBoundary.js +2 -1
  9. package/dist/ErrorBoundary.js.map +1 -1
  10. package/dist/cache.d.ts.map +1 -1
  11. package/dist/cache.js +5 -0
  12. package/dist/cache.js.map +1 -1
  13. package/dist/client-tree.d.ts.map +1 -1
  14. package/dist/client-tree.js +2 -2
  15. package/dist/client-tree.js.map +1 -1
  16. package/dist/components/ui/button.d.ts +2 -2
  17. package/dist/components/ui/button.d.ts.map +1 -1
  18. package/dist/components/ui/button.js +3 -3
  19. package/dist/components/ui/button.js.map +1 -1
  20. package/dist/components/ui/pagination.d.ts +2 -2
  21. package/dist/components/ui/pagination.d.ts.map +1 -1
  22. package/dist/components/ui/pagination.js +3 -3
  23. package/dist/components/ui/pagination.js.map +1 -1
  24. package/dist/components/ui/textarea.js +1 -1
  25. package/dist/components/ui/textarea.js.map +1 -1
  26. package/dist/events.d.ts +2 -0
  27. package/dist/events.d.ts.map +1 -1
  28. package/dist/events.js +47 -2
  29. package/dist/events.js.map +1 -1
  30. package/dist/fiber-tree.d.ts.map +1 -1
  31. package/dist/fiber-tree.js.map +1 -1
  32. package/dist/hooks.d.ts +9 -0
  33. package/dist/hooks.d.ts.map +1 -1
  34. package/dist/hooks.js +80 -5
  35. package/dist/hooks.js.map +1 -1
  36. package/dist/lib/minimd.d.ts.map +1 -1
  37. package/dist/lib/minimd.js +8 -1
  38. package/dist/lib/minimd.js.map +1 -1
  39. package/dist/lib/sanitize-href.d.ts +3 -0
  40. package/dist/lib/sanitize-href.d.ts.map +1 -0
  41. package/dist/lib/sanitize-href.js +14 -0
  42. package/dist/lib/sanitize-href.js.map +1 -0
  43. package/dist/main.js +8 -2
  44. package/dist/main.js.map +1 -1
  45. package/dist/mods/editor-ui/FieldLabel.d.ts.map +1 -1
  46. package/dist/mods/editor-ui/FieldLabel.js +2 -1
  47. package/dist/mods/editor-ui/FieldLabel.js.map +1 -1
  48. package/dist/mods/editor-ui/default-edit.js +4 -2
  49. package/dist/mods/editor-ui/default-edit.js.map +1 -1
  50. package/dist/mods/editor-ui/form-field.d.ts.map +1 -1
  51. package/dist/mods/editor-ui/form-field.js +3 -2
  52. package/dist/mods/editor-ui/form-field.js.map +1 -1
  53. package/dist/mods/editor-ui/type-picker.d.ts.map +1 -1
  54. package/dist/mods/editor-ui/type-picker.js +2 -1
  55. package/dist/mods/editor-ui/type-picker.js.map +1 -1
  56. package/dist/mods/treenity/preview.d.ts.map +1 -1
  57. package/dist/mods/treenity/preview.js +2 -3
  58. package/dist/mods/treenity/preview.js.map +1 -1
  59. package/dist/mods/treenity/ref-view.js +2 -1
  60. package/dist/mods/treenity/ref-view.js.map +1 -1
  61. package/dist/mods/treenity/seed.js +3 -2
  62. package/dist/mods/treenity/seed.js.map +1 -1
  63. package/dist/symbols.d.ts.map +1 -1
  64. package/dist/symbols.js +11 -5
  65. package/dist/symbols.js.map +1 -1
  66. package/package.json +4 -2
  67. package/src/App.tsx +29 -1
  68. package/src/ComponentSection.tsx +1 -1
  69. package/src/ErrorBoundary.tsx +6 -3
  70. package/src/cache.ts +7 -0
  71. package/src/client-tree.ts +7 -7
  72. package/src/components/ui/button.tsx +4 -5
  73. package/src/components/ui/pagination.tsx +4 -9
  74. package/src/components/ui/textarea.tsx +1 -1
  75. package/src/events.ts +46 -6
  76. package/src/fiber-tree.ts +3 -3
  77. package/src/hooks.ts +73 -4
  78. package/src/lib/minimd.ts +7 -1
  79. package/src/lib/sanitize-href.ts +13 -0
  80. package/src/main.tsx +11 -3
  81. package/src/mods/editor-ui/FieldLabel.tsx +5 -4
  82. package/src/mods/editor-ui/default-edit.tsx +6 -4
  83. package/src/mods/editor-ui/form-field.tsx +4 -2
  84. package/src/mods/editor-ui/type-picker.tsx +3 -2
  85. package/src/mods/treenity/preview.tsx +6 -7
  86. package/src/mods/treenity/ref-view.tsx +11 -6
  87. package/src/mods/treenity/seed.ts +3 -2
  88. package/src/symbols.ts +12 -5
  89. package/src/bind/bind.test.ts +0 -316
  90. package/src/cache.test.ts +0 -139
  91. package/src/client-tree.test.ts +0 -116
  92. package/src/optimistic.test.ts +0 -111
  93. 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("button", { className: "text-xs px-2 py-0.5 rounded bg-muted hover:bg-muted/80 text-foreground border border-border", onClick: () => onSelect(target), children: "Go to" })), _jsx("button", { className: "text-xs px-2 py-0.5 rounded bg-muted hover:bg-muted/80 text-foreground border border-border", 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
+ 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,iBACE,SAAS,EAAC,6FAA6F,EACvG,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,sBAGxB,CACV,EAED,iBACE,SAAS,EAAC,6FAA6F,EACvG,OAAO,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,YAEpC,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS,GAC3B,IACL,EAGL,QAAQ,IAAI,CACX,cAAK,SAAS,EAAC,8CAA8C,YAC1D,UAAU,CAAC,CAAC,CAAC,CACZ,eAAK,SAAS,EAAC,WAAW,aACxB,eAAK,SAAS,EAAC,4DAA4D,aACzE,eAAM,SAAS,EAAC,WAAW,YAAE,UAAU,CAAC,KAAK,GAAQ,EACrD,yBAAO,UAAU,CAAC,KAAK,GAAQ,IAC3B,EACN,KAAC,MAAM,IAAC,KAAK,EAAE,UAAU,GAAI,IACzB,CACP,CAAC,CAAC,CAAC,CACF,cAAK,SAAS,EAAC,+BAA+B,2BAAiB,CAChE,GACG,CACP,IACG,CACP,CAAC;AACJ,CAAC;AAED,qBAAqB;AAErB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,WAAkB,CAAC,CAAC;AAC7C,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,WAAkB,CAAC,CAAC"}
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":"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"}
1
+ {"version":3,"file":"seed.js","sourceRoot":"","sources":["../../../src/mods/treenity/seed.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"symbols.d.ts","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE5D,eAAO,MAAM,IAAI,eAA8B,CAAC;AAChD,eAAO,MAAM,KAAK,eAA+B,CAAC;AAElD,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAS9C"}
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). Survive spread, invisible to JSON/keys/entries.
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[$key] = '';
8
- node[$node] = node;
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[$key] = k;
13
- v[$node] = node;
18
+ hide(v, $key, k);
19
+ hide(v, $node, node);
14
20
  }
15
21
  }
16
22
  //# sourceMappingURL=symbols.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"symbols.js","sourceRoot":"","sources":["../src/symbols.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,0FAA0F;AAE1F,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,MAAM,UAAU,SAAS,CAAC,IAAc;IACrC,IAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;IACxB,IAAY,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAE5B,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;QAClD,CAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAS,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAC3B,CAAC;AACH,CAAC"}
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.1",
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": "AGPL-3.0",
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
@@ -59,7 +59,7 @@ export function ComponentSection({
59
59
  toast,
60
60
  onActionComplete,
61
61
  }: ComponentSectionProps) {
62
- const isMain = !onToggle;
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">
@@ -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
- <button
14
- className="mt-1 text-[10px] text-muted-foreground hover:text-foreground underline"
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
- </button>
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) {
@@ -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 { createFilterTree, createMemoryTree } from '@treenity/core/tree'
7
- import { withCache } from '@treenity/core/tree/cache'
8
- import { withMounts } from '@treenity/core/server/mount'
9
- import './fiber-tree' // registers t.mount.react
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 * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { Slot } from "radix-ui"
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 * as React from "react"
2
- import {
3
- ChevronLeftIcon,
4
- ChevronRightIcon,
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
- applyPatch(existing, event.patches as Operation[]);
49
- cache.put(existing);
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 (unsub) {
78
- unsub();
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) => cache.putMany(result.items as NodeData[], parentPath));
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
- await tree.set(next);
116
- const fresh = await tree.get(next.$path);
117
- if (fresh) cache.put(fresh);
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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, '<a href="$2" target="_blank" rel="noopener">$1</a>');
15
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) => {
16
+ const safe = sanitizeHref(url);
17
+ if (!safe) return text;
18
+ const safeUrl = safe.replace(/"/g, '&quot;');
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
- <StrictMode>
26
+ <Strict>
19
27
  <QueryClientProvider client={queryClient}>
20
28
  <App />
21
29
  <Toaster />
22
30
  </QueryClientProvider>
23
- </StrictMode>,
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
- <input
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
- <input
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"