@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,116 +0,0 @@
1
- import type { NodeData } from '@treenity/core';
2
- import assert from 'node:assert/strict';
3
- import { describe, it } from 'node:test';
4
- import { createClientTree } from './client-tree';
5
-
6
- // ── Mock tRPC client ──
7
-
8
- function createMockTrpc(backing: Map<string, NodeData>) {
9
- let calls = 0;
10
-
11
- const mock = {
12
- get calls() { return calls; },
13
- resetCalls() { calls = 0; },
14
-
15
- get: {
16
- query: async ({ path }: { path: string }) => {
17
- calls++;
18
- return backing.get(path);
19
- },
20
- },
21
- getChildren: {
22
- query: async ({ path }: { path: string; limit?: number; offset?: number }) => {
23
- calls++;
24
- const prefix = path === '/' ? '/' : path + '/';
25
- const items = [...backing.values()].filter(
26
- n => n.$path.startsWith(prefix) && n.$path !== path
27
- && n.$path.slice(prefix.length).indexOf('/') === -1,
28
- );
29
- return { items, total: items.length };
30
- },
31
- },
32
- set: {
33
- mutate: async ({ node }: { node: Record<string, unknown> }) => {
34
- calls++;
35
- backing.set(node.$path as string, node as NodeData);
36
- },
37
- },
38
- remove: {
39
- mutate: async ({ path }: { path: string }) => {
40
- calls++;
41
- backing.delete(path);
42
- },
43
- },
44
- };
45
-
46
- return mock;
47
- }
48
-
49
- // ── Tests ──
50
-
51
- describe('createClientTree — unified client tree', () => {
52
- it('/local/* paths stay in memory, never hit tRPC', async () => {
53
- const mock = createMockTrpc(new Map());
54
- const { tree: store } = createClientTree(mock as any);
55
-
56
- await store.set({ $path: '/local/ui/theme', $type: 'theme', dark: true } as NodeData);
57
- mock.resetCalls();
58
-
59
- const node = await store.get('/local/ui/theme');
60
- assert.equal(mock.calls, 0, 'tRPC should not be called for /local paths');
61
- assert.equal(node?.$type, 'theme');
62
- assert.equal((node as any).dark, true);
63
- });
64
-
65
- it('non-local paths route through tRPC', async () => {
66
- const backing = new Map<string, NodeData>();
67
- backing.set('/orders/1', { $path: '/orders/1', $type: 'order', total: 42 } as NodeData);
68
- const mock = createMockTrpc(backing);
69
- const { tree: store } = createClientTree(mock as any);
70
-
71
- const node = await store.get('/orders/1');
72
- assert.ok(mock.calls > 0, 'tRPC should be called for remote paths');
73
- assert.equal((node as any).total, 42);
74
- });
75
-
76
- it('getChildren merges local and remote children', async () => {
77
- const backing = new Map<string, NodeData>();
78
- backing.set('/cloud', { $path: '/cloud', $type: 'dir' } as NodeData);
79
- const mock = createMockTrpc(backing);
80
- const { tree: store } = createClientTree(mock as any);
81
-
82
- // Write a local node
83
- await store.set({ $path: '/local', $type: 'dir' } as NodeData);
84
-
85
- // getChildren('/') should return both
86
- const { items } = await store.getChildren('/');
87
- const paths = items.map((n: { $path: string }) => n.$path).sort();
88
- assert.ok(paths.includes('/local'), 'should include local children');
89
- assert.ok(paths.includes('/cloud'), 'should include remote children');
90
- });
91
-
92
- it('remove /local/* does not call tRPC', async () => {
93
- const mock = createMockTrpc(new Map());
94
- const { tree: store } = createClientTree(mock as any);
95
-
96
- await store.set({ $path: '/local/temp', $type: 'tmp' } as NodeData);
97
- mock.resetCalls();
98
-
99
- await store.remove('/local/temp');
100
- // filterStore tries both, but remote remove is harmless no-op
101
- const node = await store.get('/local/temp');
102
- assert.equal(node, undefined, '/local/temp should be gone');
103
- });
104
-
105
- it('cached remote: second get skips tRPC', async () => {
106
- const backing = new Map<string, NodeData>();
107
- backing.set('/x', { $path: '/x', $type: 'test' } as NodeData);
108
- const mock = createMockTrpc(backing);
109
- const { tree: store } = createClientTree(mock as any);
110
-
111
- await store.get('/x'); // populates cache
112
- mock.resetCalls();
113
- await store.get('/x'); // should hit cache
114
- assert.equal(mock.calls, 0, 'second get should come from cache');
115
- });
116
- });
@@ -1,111 +0,0 @@
1
- // Optimistic prediction tests — predictOptimistic updates cache for sync methods
2
-
3
- import { registerType } from '@treenity/core/comp';
4
- import assert from 'node:assert';
5
- import { beforeEach, describe, it } from 'node:test';
6
- import * as cache from './cache';
7
- import { predictOptimistic } from './hooks';
8
-
9
- class Counter {
10
- count = 0;
11
-
12
- increment() {
13
- this.count++;
14
- }
15
-
16
- setCount(data: { count: number }) {
17
- this.count = data.count;
18
- }
19
-
20
- async asyncAction() {
21
- // Simulates server-only async method
22
- await Promise.resolve();
23
- this.count = 999;
24
- }
25
-
26
- broken() {
27
- throw new Error('intentional failure');
28
- }
29
- }
30
-
31
- registerType('test.counter', Counter);
32
-
33
- describe('predictOptimistic', () => {
34
- beforeEach(() => {
35
- cache.clear();
36
- });
37
-
38
- it('sync method updates cache immediately', () => {
39
- cache.put({ $path: '/c', $type: 'test.counter', count: 5 } as any);
40
-
41
- predictOptimistic('/c', Counter, undefined, Counter.prototype.increment, undefined);
42
-
43
- const node = cache.get('/c');
44
- assert.strictEqual((node as any).count, 6);
45
- });
46
-
47
- it('sync method with data updates cache', () => {
48
- cache.put({ $path: '/c', $type: 'test.counter', count: 0 } as any);
49
-
50
- predictOptimistic('/c', Counter, undefined, Counter.prototype.setCount, { count: 42 });
51
-
52
- const node = cache.get('/c');
53
- assert.strictEqual((node as any).count, 42);
54
- });
55
-
56
- it('does not mutate original cached node', () => {
57
- cache.put({ $path: '/c', $type: 'test.counter', count: 10 } as any);
58
- const before = cache.get('/c');
59
-
60
- predictOptimistic('/c', Counter, undefined, Counter.prototype.increment, undefined);
61
-
62
- const after = cache.get('/c');
63
- assert.notStrictEqual(before, after);
64
- });
65
-
66
- it('skips async methods', () => {
67
- cache.put({ $path: '/c', $type: 'test.counter', count: 0 } as any);
68
-
69
- predictOptimistic('/c', Counter, undefined, Counter.prototype.asyncAction, undefined);
70
-
71
- const node = cache.get('/c');
72
- assert.strictEqual((node as any).count, 0);
73
- });
74
-
75
- it('skips when path not in cache', () => {
76
- predictOptimistic('/missing', Counter, undefined, Counter.prototype.increment, undefined);
77
-
78
- assert.strictEqual(cache.get('/missing'), undefined);
79
- });
80
-
81
- it('swallows method errors without updating cache', () => {
82
- cache.put({ $path: '/c', $type: 'test.counter', count: 5 } as any);
83
-
84
- predictOptimistic('/c', Counter, undefined, Counter.prototype.broken, undefined);
85
-
86
- const node = cache.get('/c');
87
- assert.strictEqual((node as any).count, 5);
88
- });
89
-
90
- it('skips when component type not found on node', () => {
91
- cache.put({ $path: '/c', $type: 'unknown.type', count: 5 } as any);
92
-
93
- predictOptimistic('/c', Counter, undefined, Counter.prototype.increment, undefined);
94
-
95
- const node = cache.get('/c');
96
- assert.strictEqual((node as any).count, 5);
97
- });
98
-
99
- it('works with named component key', () => {
100
- cache.put({
101
- $path: '/n',
102
- $type: 'dir',
103
- stats: { $type: 'test.counter', count: 3 },
104
- } as any);
105
-
106
- predictOptimistic('/n', Counter, 'stats', Counter.prototype.increment, undefined);
107
-
108
- const node = cache.get('/n') as any;
109
- assert.strictEqual(node.stats.count, 4);
110
- });
111
- });
@@ -1,142 +0,0 @@
1
- import type { NodeData } from '@treenity/core';
2
- import { withCache } from '@treenity/core/tree/cache';
3
- import assert from 'node:assert/strict';
4
- import { describe, it } from 'node:test';
5
- import { createRemoteTree } from './remote-tree';
6
-
7
- // ── Mock tRPC client ──
8
-
9
- function createMockTrpc(backing: Map<string, NodeData>) {
10
- let getCalls = 0;
11
-
12
- const mock = {
13
- get getCalls() { return getCalls; },
14
- resetCalls() { getCalls = 0; },
15
-
16
- get: {
17
- query: async ({ path }: { path: string }) => {
18
- getCalls++;
19
- return backing.get(path);
20
- },
21
- },
22
- getChildren: {
23
- query: async ({ path, limit, offset }: { path: string; limit?: number; offset?: number }) => {
24
- getCalls++;
25
- const prefix = path === '/' ? '/' : path + '/';
26
- const items = [...backing.values()].filter(
27
- n => n.$path.startsWith(prefix) && n.$path !== path
28
- && n.$path.slice(prefix.length).indexOf('/') === -1,
29
- );
30
- const start = offset ?? 0;
31
- const sliced = limit ? items.slice(start, start + limit) : items.slice(start);
32
- return { items: sliced, total: items.length };
33
- },
34
- },
35
- set: {
36
- mutate: async ({ node }: { node: Record<string, unknown> }) => {
37
- backing.set(node.$path as string, node as NodeData);
38
- },
39
- },
40
- remove: {
41
- mutate: async ({ path }: { path: string }) => {
42
- backing.delete(path);
43
- },
44
- },
45
- };
46
-
47
- return mock;
48
- }
49
-
50
- // ── Tests ──
51
-
52
- describe('createRemoteTree — method mapping', () => {
53
- it('get delegates to trpc.get.query', async () => {
54
- const data = new Map<string, NodeData>();
55
- data.set('/a', { $path: '/a', $type: 'test', v: 1 } as NodeData);
56
- const mock = createMockTrpc(data);
57
- const store = createRemoteTree(mock as any);
58
-
59
- const node = await store.get('/a');
60
- assert.equal(node?.$path, '/a');
61
- assert.equal((node as any).v, 1);
62
- assert.equal(mock.getCalls, 1);
63
- });
64
-
65
- it('get returns undefined for missing path', async () => {
66
- const store = createRemoteTree(createMockTrpc(new Map()) as any);
67
- const node = await store.get('/missing');
68
- assert.equal(node, undefined);
69
- });
70
-
71
- it('getChildren delegates to trpc.getChildren.query', async () => {
72
- const data = new Map<string, NodeData>();
73
- data.set('/p', { $path: '/p', $type: 'dir' } as NodeData);
74
- data.set('/p/a', { $path: '/p/a', $type: 'test' } as NodeData);
75
- data.set('/p/b', { $path: '/p/b', $type: 'test' } as NodeData);
76
- const store = createRemoteTree(createMockTrpc(data) as any);
77
-
78
- const result = await store.getChildren('/p');
79
- assert.equal(result.items.length, 2);
80
- assert.equal(result.total, 2);
81
- });
82
-
83
- it('set delegates to trpc.set.mutate', async () => {
84
- const data = new Map<string, NodeData>();
85
- const store = createRemoteTree(createMockTrpc(data) as any);
86
-
87
- await store.set({ $path: '/x', $type: 'test', v: 42 } as NodeData);
88
- assert.ok(data.has('/x'));
89
- assert.equal((data.get('/x') as any).v, 42);
90
- });
91
-
92
- it('remove delegates to trpc.remove.mutate', async () => {
93
- const data = new Map<string, NodeData>();
94
- data.set('/x', { $path: '/x', $type: 'test' } as NodeData);
95
- const store = createRemoteTree(createMockTrpc(data) as any);
96
-
97
- const result = await store.remove('/x');
98
- assert.equal(result, true);
99
- assert.ok(!data.has('/x'));
100
- });
101
- });
102
-
103
- describe('withCache(remoteStore) — client pipeline', () => {
104
- it('caches get results — second call skips tRPC', async () => {
105
- const data = new Map<string, NodeData>();
106
- data.set('/a', { $path: '/a', $type: 'test' } as NodeData);
107
- const mock = createMockTrpc(data);
108
- const store = withCache(createRemoteTree(mock as any));
109
-
110
- await store.get('/a');
111
- mock.resetCalls();
112
- await store.get('/a'); // should hit cache
113
- assert.equal(mock.getCalls, 0);
114
- });
115
-
116
- it('write-populate: set warms cache for next get', async () => {
117
- const data = new Map<string, NodeData>();
118
- const mock = createMockTrpc(data);
119
- const store = withCache(createRemoteTree(mock as any));
120
-
121
- await store.set({ $path: '/a', $type: 'test', v: 1 } as NodeData);
122
- mock.resetCalls();
123
-
124
- const node = await store.get('/a'); // should hit cache (write-populated)
125
- assert.equal(mock.getCalls, 0);
126
- assert.equal((node as any).v, 1);
127
- });
128
-
129
- it('inflight dedup: concurrent gets produce single tRPC call', async () => {
130
- const data = new Map<string, NodeData>();
131
- data.set('/a', { $path: '/a', $type: 'test', v: 99 } as NodeData);
132
- const mock = createMockTrpc(data);
133
- const store = withCache(createRemoteTree(mock as any));
134
-
135
- const results = await Promise.all(
136
- Array.from({ length: 5 }, () => store.get('/a')),
137
- );
138
-
139
- assert.equal(mock.getCalls, 1);
140
- for (const r of results) assert.equal((r as any).v, 99);
141
- });
142
- });