@voidhash/mimic 0.0.1-alpha.1
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 +17 -0
- package/package.json +33 -0
- package/src/Document.ts +256 -0
- package/src/FractionalIndex.ts +1249 -0
- package/src/Operation.ts +59 -0
- package/src/OperationDefinition.ts +23 -0
- package/src/OperationPath.ts +197 -0
- package/src/Presence.ts +142 -0
- package/src/Primitive.ts +32 -0
- package/src/Proxy.ts +8 -0
- package/src/ProxyEnvironment.ts +52 -0
- package/src/Transaction.ts +72 -0
- package/src/Transform.ts +13 -0
- package/src/client/ClientDocument.ts +1163 -0
- package/src/client/Rebase.ts +309 -0
- package/src/client/StateMonitor.ts +307 -0
- package/src/client/Transport.ts +318 -0
- package/src/client/WebSocketTransport.ts +572 -0
- package/src/client/errors.ts +145 -0
- package/src/client/index.ts +61 -0
- package/src/index.ts +12 -0
- package/src/primitives/Array.ts +457 -0
- package/src/primitives/Boolean.ts +128 -0
- package/src/primitives/Lazy.ts +89 -0
- package/src/primitives/Literal.ts +128 -0
- package/src/primitives/Number.ts +169 -0
- package/src/primitives/String.ts +189 -0
- package/src/primitives/Struct.ts +348 -0
- package/src/primitives/Tree.ts +1120 -0
- package/src/primitives/TreeNode.ts +113 -0
- package/src/primitives/Union.ts +329 -0
- package/src/primitives/shared.ts +122 -0
- package/src/server/ServerDocument.ts +267 -0
- package/src/server/errors.ts +90 -0
- package/src/server/index.ts +40 -0
- package/tests/Document.test.ts +556 -0
- package/tests/FractionalIndex.test.ts +377 -0
- package/tests/OperationPath.test.ts +151 -0
- package/tests/Presence.test.ts +321 -0
- package/tests/Primitive.test.ts +381 -0
- package/tests/client/ClientDocument.test.ts +1398 -0
- package/tests/client/WebSocketTransport.test.ts +992 -0
- package/tests/primitives/Array.test.ts +418 -0
- package/tests/primitives/Boolean.test.ts +126 -0
- package/tests/primitives/Lazy.test.ts +143 -0
- package/tests/primitives/Literal.test.ts +122 -0
- package/tests/primitives/Number.test.ts +133 -0
- package/tests/primitives/String.test.ts +128 -0
- package/tests/primitives/Struct.test.ts +311 -0
- package/tests/primitives/Tree.test.ts +467 -0
- package/tests/primitives/TreeNode.test.ts +50 -0
- package/tests/primitives/Union.test.ts +210 -0
- package/tests/server/ServerDocument.test.ts +528 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
+
import * as Primitive from "../../src/Primitive";
|
|
3
|
+
import * as ProxyEnvironment from "../../src/ProxyEnvironment";
|
|
4
|
+
import * as OperationPath from "../../src/OperationPath";
|
|
5
|
+
import * as Operation from "../../src/Operation";
|
|
6
|
+
|
|
7
|
+
describe("TreePrimitive", () => {
|
|
8
|
+
// Define node types using the new TreeNode API
|
|
9
|
+
const FileNode = Primitive.TreeNode("file", {
|
|
10
|
+
data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number() }),
|
|
11
|
+
children: [] as const,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const FolderNode = Primitive.TreeNode("folder", {
|
|
15
|
+
data: Primitive.Struct({ name: Primitive.String() }),
|
|
16
|
+
children: (): readonly Primitive.AnyTreeNodePrimitive[] => [FolderNode, FileNode],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const fileSystemTree = Primitive.Tree({
|
|
20
|
+
root: FolderNode,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Helper to create a mock environment with state access
|
|
24
|
+
const createEnvWithState = (
|
|
25
|
+
state: Primitive.TreeState<typeof FolderNode> = []
|
|
26
|
+
): { env: ReturnType<typeof ProxyEnvironment.make>; operations: Operation.Operation<any, any, any>[] } => {
|
|
27
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
28
|
+
let currentState = [...state] as Primitive.TreeState<typeof FolderNode>;
|
|
29
|
+
let idCounter = 0;
|
|
30
|
+
|
|
31
|
+
const env = ProxyEnvironment.make({
|
|
32
|
+
onOperation: (op) => {
|
|
33
|
+
operations.push(op);
|
|
34
|
+
// Apply operation to keep state in sync
|
|
35
|
+
currentState = fileSystemTree._internal.applyOperation(currentState, op);
|
|
36
|
+
},
|
|
37
|
+
getState: () => currentState,
|
|
38
|
+
generateId: () => `node-${++idCounter}`,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return { env, operations };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe("schema", () => {
|
|
45
|
+
it("exposes root node type", () => {
|
|
46
|
+
expect(fileSystemTree.root).toBe(FolderNode);
|
|
47
|
+
expect(fileSystemTree.root.type).toBe("folder");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("required() returns a new TreePrimitive", () => {
|
|
51
|
+
const required = fileSystemTree.required();
|
|
52
|
+
expect(required).toBeInstanceOf(Primitive.TreePrimitive);
|
|
53
|
+
expect(required).not.toBe(fileSystemTree);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("default() returns a new TreePrimitive with default value", () => {
|
|
57
|
+
const defaultState: Primitive.TreeState<typeof FolderNode> = [
|
|
58
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
59
|
+
];
|
|
60
|
+
const withDefault = fileSystemTree.default(defaultState);
|
|
61
|
+
expect(withDefault._internal.getInitialState()).toEqual(defaultState);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("proxy - basic operations", () => {
|
|
66
|
+
it("get() returns empty array for initial state", () => {
|
|
67
|
+
const { env } = createEnvWithState();
|
|
68
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
69
|
+
|
|
70
|
+
expect(proxy.get()).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("set() generates tree.set operation", () => {
|
|
74
|
+
const { env, operations } = createEnvWithState();
|
|
75
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
76
|
+
|
|
77
|
+
const nodes: Primitive.TreeState<typeof FolderNode> = [
|
|
78
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
79
|
+
];
|
|
80
|
+
proxy.set(nodes);
|
|
81
|
+
|
|
82
|
+
expect(operations).toHaveLength(1);
|
|
83
|
+
expect(operations[0]!.kind).toBe("tree.set");
|
|
84
|
+
expect(operations[0]!.payload).toEqual(nodes);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("root() returns the root node", () => {
|
|
88
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
89
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
90
|
+
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
91
|
+
];
|
|
92
|
+
const { env } = createEnvWithState(initialState);
|
|
93
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
94
|
+
|
|
95
|
+
const root = proxy.root();
|
|
96
|
+
expect(root).toBeDefined();
|
|
97
|
+
expect(root!.id).toBe("root");
|
|
98
|
+
expect(root!.parentId).toBe(null);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("node() returns a node proxy by ID", () => {
|
|
102
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
103
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
104
|
+
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
105
|
+
];
|
|
106
|
+
const { env } = createEnvWithState(initialState);
|
|
107
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
108
|
+
|
|
109
|
+
const node = proxy.node("child1");
|
|
110
|
+
expect(node).toBeDefined();
|
|
111
|
+
expect(node!.id).toBe("child1");
|
|
112
|
+
expect(node!.type).toBe("file");
|
|
113
|
+
expect(node!.get().data).toEqual({ name: "File1", size: 100 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("children() returns ordered children", () => {
|
|
117
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
118
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
119
|
+
{ id: "child2", type: "file", parentId: "root", pos: "a1", data: { name: "File2", size: 200 } },
|
|
120
|
+
{ id: "child1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
121
|
+
];
|
|
122
|
+
const { env } = createEnvWithState(initialState);
|
|
123
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
124
|
+
|
|
125
|
+
const children = proxy.children("root");
|
|
126
|
+
expect(children).toHaveLength(2);
|
|
127
|
+
expect(children[0]!.id).toBe("child1"); // a0 comes first
|
|
128
|
+
expect(children[1]!.id).toBe("child2"); // a1 comes second
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("proxy - type narrowing with is() and as()", () => {
|
|
133
|
+
it("is() returns true for matching node type", () => {
|
|
134
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
135
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
136
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
137
|
+
];
|
|
138
|
+
const { env } = createEnvWithState(initialState);
|
|
139
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
140
|
+
|
|
141
|
+
const fileProxy = proxy.node("file1");
|
|
142
|
+
expect(fileProxy!.is(FileNode)).toBe(true);
|
|
143
|
+
expect(fileProxy!.is(FolderNode)).toBe(false);
|
|
144
|
+
|
|
145
|
+
const folderProxy = proxy.node("root");
|
|
146
|
+
expect(folderProxy!.is(FolderNode)).toBe(true);
|
|
147
|
+
expect(folderProxy!.is(FileNode)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("as() returns typed proxy for correct type", () => {
|
|
151
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
152
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
153
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
154
|
+
];
|
|
155
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
156
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
157
|
+
|
|
158
|
+
const fileProxy = proxy.node("file1")!.as(FileNode);
|
|
159
|
+
expect(fileProxy.id).toBe("file1");
|
|
160
|
+
expect(fileProxy.type).toBe("file");
|
|
161
|
+
|
|
162
|
+
// Type-safe data access
|
|
163
|
+
fileProxy.data.name.set("UpdatedName");
|
|
164
|
+
expect(operations).toHaveLength(1);
|
|
165
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("as() throws for wrong type", () => {
|
|
169
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
170
|
+
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
171
|
+
];
|
|
172
|
+
const { env } = createEnvWithState(initialState);
|
|
173
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
174
|
+
|
|
175
|
+
const nodeProxy = proxy.node("file1");
|
|
176
|
+
expect(() => nodeProxy!.as(FolderNode)).toThrow(Primitive.ValidationError);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("proxy - insert operations with TreeNode types", () => {
|
|
181
|
+
it("insertFirst() creates node at beginning of children", () => {
|
|
182
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
183
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
184
|
+
{ id: "existing", type: "file", parentId: "root", pos: "a1", data: { name: "Existing", size: 100 } },
|
|
185
|
+
];
|
|
186
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
187
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
188
|
+
|
|
189
|
+
const newId = proxy.insertFirst("root", FileNode, { name: "First", size: 50 });
|
|
190
|
+
|
|
191
|
+
expect(operations).toHaveLength(1);
|
|
192
|
+
expect(operations[0]!.kind).toBe("tree.insert");
|
|
193
|
+
expect(newId).toBe("node-1");
|
|
194
|
+
|
|
195
|
+
const payload = operations[0]!.payload as { id: string; pos: string; type: string };
|
|
196
|
+
expect(payload.type).toBe("file");
|
|
197
|
+
expect(payload.pos < "a1").toBe(true); // Should be before existing
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("insertLast() creates node at end of children", () => {
|
|
201
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
202
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
203
|
+
{ id: "existing", type: "file", parentId: "root", pos: "a0", data: { name: "Existing", size: 100 } },
|
|
204
|
+
];
|
|
205
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
206
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
207
|
+
|
|
208
|
+
proxy.insertLast("root", FileNode, { name: "Last", size: 50 });
|
|
209
|
+
|
|
210
|
+
const payload = operations[0]!.payload as { pos: string };
|
|
211
|
+
expect(payload.pos > "a0").toBe(true); // Should be after existing
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("insertFirst() with null parentId creates root node", () => {
|
|
215
|
+
const { env, operations } = createEnvWithState();
|
|
216
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
217
|
+
|
|
218
|
+
proxy.insertFirst(null, FolderNode, { name: "Root" });
|
|
219
|
+
|
|
220
|
+
expect(operations).toHaveLength(1);
|
|
221
|
+
const payload = operations[0]!.payload as { parentId: string | null; type: string };
|
|
222
|
+
expect(payload.parentId).toBe(null);
|
|
223
|
+
expect(payload.type).toBe("folder");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("proxy - validation", () => {
|
|
228
|
+
it("throws when inserting invalid child type", () => {
|
|
229
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
230
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
231
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
232
|
+
];
|
|
233
|
+
const { env } = createEnvWithState(initialState);
|
|
234
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
235
|
+
|
|
236
|
+
// Files cannot have children
|
|
237
|
+
expect(() => proxy.insertFirst("file1", FileNode, { name: "Child", size: 50 })).toThrow(
|
|
238
|
+
Primitive.ValidationError
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("throws when inserting non-root type at root level", () => {
|
|
243
|
+
const { env } = createEnvWithState();
|
|
244
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
245
|
+
|
|
246
|
+
expect(() => proxy.insertFirst(null, FileNode, { name: "File", size: 50 })).toThrow(
|
|
247
|
+
Primitive.ValidationError
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("throws when inserting second root", () => {
|
|
252
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
253
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
254
|
+
];
|
|
255
|
+
const { env } = createEnvWithState(initialState);
|
|
256
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
257
|
+
|
|
258
|
+
expect(() => proxy.insertFirst(null, FolderNode, { name: "SecondRoot" })).toThrow(
|
|
259
|
+
Primitive.ValidationError
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("proxy - toSnapshot()", () => {
|
|
265
|
+
it("returns undefined for empty tree", () => {
|
|
266
|
+
const { env } = createEnvWithState();
|
|
267
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
268
|
+
|
|
269
|
+
expect(proxy.toSnapshot()).toBeUndefined();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("returns nested snapshot with spread data", () => {
|
|
273
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
274
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
275
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
276
|
+
{ id: "folder1", type: "folder", parentId: "root", pos: "a1", data: { name: "Subfolder" } },
|
|
277
|
+
{ id: "file2", type: "file", parentId: "folder1", pos: "a0", data: { name: "File2", size: 200 } },
|
|
278
|
+
];
|
|
279
|
+
const { env } = createEnvWithState(initialState);
|
|
280
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
281
|
+
|
|
282
|
+
const snapshot = proxy.toSnapshot();
|
|
283
|
+
expect(snapshot).toBeDefined();
|
|
284
|
+
expect(snapshot!.id).toBe("root");
|
|
285
|
+
expect(snapshot!.type).toBe("folder");
|
|
286
|
+
expect(snapshot!.name).toBe("Root"); // Data spread at node level
|
|
287
|
+
expect(snapshot!.children).toHaveLength(2);
|
|
288
|
+
|
|
289
|
+
const file1Snapshot = snapshot!.children[0]!;
|
|
290
|
+
expect(file1Snapshot.id).toBe("file1");
|
|
291
|
+
expect(file1Snapshot.name).toBe("File1");
|
|
292
|
+
expect(file1Snapshot.children).toEqual([]);
|
|
293
|
+
|
|
294
|
+
const folder1Snapshot = snapshot!.children[1]!;
|
|
295
|
+
expect(folder1Snapshot.id).toBe("folder1");
|
|
296
|
+
expect(folder1Snapshot.children).toHaveLength(1);
|
|
297
|
+
expect(folder1Snapshot.children[0]!.name).toBe("File2");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("proxy - at() with typed node", () => {
|
|
302
|
+
it("at() returns typed proxy for node data", () => {
|
|
303
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
304
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
305
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
306
|
+
];
|
|
307
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
308
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
309
|
+
|
|
310
|
+
proxy.at("file1", FileNode).name.set("UpdatedName");
|
|
311
|
+
|
|
312
|
+
expect(operations).toHaveLength(1);
|
|
313
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
314
|
+
expect(operations[0]!.path.toTokens()).toEqual(["file1", "name"]);
|
|
315
|
+
expect(operations[0]!.payload).toBe("UpdatedName");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("at() throws when node type mismatch", () => {
|
|
319
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
320
|
+
{ id: "file1", type: "file", parentId: null, pos: "a0", data: { name: "File1", size: 100 } },
|
|
321
|
+
];
|
|
322
|
+
const { env } = createEnvWithState(initialState);
|
|
323
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
324
|
+
|
|
325
|
+
expect(() => proxy.at("file1", FolderNode)).toThrow(Primitive.ValidationError);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("proxy - move operations", () => {
|
|
330
|
+
it("move() changes parent and position", () => {
|
|
331
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
332
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
333
|
+
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
334
|
+
{ id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
|
|
335
|
+
{ id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
|
|
336
|
+
];
|
|
337
|
+
const { env, operations } = createEnvWithState(initialState);
|
|
338
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
339
|
+
|
|
340
|
+
// Move file1 to folder2
|
|
341
|
+
proxy.move("file1", "folder2", 0);
|
|
342
|
+
|
|
343
|
+
expect(operations).toHaveLength(1);
|
|
344
|
+
expect(operations[0]!.kind).toBe("tree.move");
|
|
345
|
+
const payload = operations[0]!.payload as { id: string; parentId: string };
|
|
346
|
+
expect(payload.id).toBe("file1");
|
|
347
|
+
expect(payload.parentId).toBe("folder2");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("throws when moving node to its descendant (cycle prevention)", () => {
|
|
351
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
352
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
353
|
+
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
354
|
+
{ id: "folder2", type: "folder", parentId: "folder1", pos: "a0", data: { name: "Folder2" } },
|
|
355
|
+
];
|
|
356
|
+
const { env } = createEnvWithState(initialState);
|
|
357
|
+
const proxy = fileSystemTree._internal.createProxy(env, OperationPath.make(""));
|
|
358
|
+
|
|
359
|
+
expect(() => proxy.move("folder1", "folder2", 0)).toThrow(Primitive.ValidationError);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("applyOperation", () => {
|
|
364
|
+
it("tree.set replaces entire tree", () => {
|
|
365
|
+
const newNodes: Primitive.TreeState<typeof FolderNode> = [
|
|
366
|
+
{ id: "new-root", type: "folder", parentId: null, pos: "a0", data: { name: "NewRoot" } },
|
|
367
|
+
];
|
|
368
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
369
|
+
kind: "tree.set",
|
|
370
|
+
path: OperationPath.make(""),
|
|
371
|
+
payload: newNodes,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = fileSystemTree._internal.applyOperation([], operation);
|
|
375
|
+
expect(result).toEqual(newNodes);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("tree.insert adds a new node", () => {
|
|
379
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
380
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
381
|
+
];
|
|
382
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
383
|
+
kind: "tree.insert",
|
|
384
|
+
path: OperationPath.make(""),
|
|
385
|
+
payload: {
|
|
386
|
+
id: "file1",
|
|
387
|
+
type: "file",
|
|
388
|
+
parentId: "root",
|
|
389
|
+
pos: "a0",
|
|
390
|
+
data: { name: "File1", size: 100 },
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
395
|
+
expect(result).toHaveLength(2);
|
|
396
|
+
expect(result[1]).toEqual({
|
|
397
|
+
id: "file1",
|
|
398
|
+
type: "file",
|
|
399
|
+
parentId: "root",
|
|
400
|
+
pos: "a0",
|
|
401
|
+
data: { name: "File1", size: 100 },
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("tree.remove removes node and descendants", () => {
|
|
406
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
407
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
408
|
+
{ id: "folder1", type: "folder", parentId: "root", pos: "a0", data: { name: "Folder1" } },
|
|
409
|
+
{ id: "file1", type: "file", parentId: "folder1", pos: "a0", data: { name: "File1", size: 100 } },
|
|
410
|
+
{ id: "folder2", type: "folder", parentId: "root", pos: "a1", data: { name: "Folder2" } },
|
|
411
|
+
];
|
|
412
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
413
|
+
kind: "tree.remove",
|
|
414
|
+
path: OperationPath.make(""),
|
|
415
|
+
payload: { id: "folder1" },
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
419
|
+
expect(result).toHaveLength(2);
|
|
420
|
+
expect(result.map(n => n.id)).toEqual(["root", "folder2"]);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("delegates node data operations", () => {
|
|
424
|
+
const initialState: Primitive.TreeState<typeof FolderNode> = [
|
|
425
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
426
|
+
{ id: "file1", type: "file", parentId: "root", pos: "a0", data: { name: "File1", size: 100 } },
|
|
427
|
+
];
|
|
428
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
429
|
+
kind: "string.set",
|
|
430
|
+
path: OperationPath.make("file1/name"),
|
|
431
|
+
payload: "UpdatedName",
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const result = fileSystemTree._internal.applyOperation(initialState, operation);
|
|
435
|
+
const updatedNode = result.find(n => n.id === "file1");
|
|
436
|
+
expect(updatedNode!.data).toEqual({ name: "UpdatedName", size: 100 });
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
describe("getInitialState", () => {
|
|
441
|
+
it("automatically creates a root node when no default is set", () => {
|
|
442
|
+
const initialState = fileSystemTree._internal.getInitialState();
|
|
443
|
+
expect(initialState).toBeDefined();
|
|
444
|
+
expect(initialState).toHaveLength(1);
|
|
445
|
+
expect(initialState![0]).toMatchObject({
|
|
446
|
+
type: "folder",
|
|
447
|
+
parentId: null,
|
|
448
|
+
data: {},
|
|
449
|
+
});
|
|
450
|
+
// Verify ID and pos are generated
|
|
451
|
+
expect(typeof initialState![0]!.id).toBe("string");
|
|
452
|
+
expect(typeof initialState![0]!.pos).toBe("string");
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("returns the default value when set", () => {
|
|
456
|
+
const defaultState: Primitive.TreeState<typeof FolderNode> = [
|
|
457
|
+
{ id: "root", type: "folder", parentId: null, pos: "a0", data: { name: "Root" } },
|
|
458
|
+
];
|
|
459
|
+
const withDefault = fileSystemTree.default(defaultState);
|
|
460
|
+
expect(withDefault._internal.getInitialState()).toEqual(defaultState);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// =============================================================================
|
|
466
|
+
// Integration Tests - Tree with Complex Structures
|
|
467
|
+
// =============================================================================
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
+
import * as Primitive from "../../src/Primitive";
|
|
3
|
+
import * as ProxyEnvironment from "../../src/ProxyEnvironment";
|
|
4
|
+
import * as OperationPath from "../../src/OperationPath";
|
|
5
|
+
import * as Operation from "../../src/Operation";
|
|
6
|
+
|
|
7
|
+
describe("TreeNodePrimitive", () => {
|
|
8
|
+
it("creates a TreeNode with type, data, and empty children", () => {
|
|
9
|
+
const FileNode = Primitive.TreeNode("file", {
|
|
10
|
+
data: Primitive.Struct({ name: Primitive.String(), size: Primitive.Number() }),
|
|
11
|
+
children: [],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(FileNode.type).toBe("file");
|
|
15
|
+
expect(FileNode.data).toBeInstanceOf(Primitive.StructPrimitive);
|
|
16
|
+
expect(FileNode.children).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("creates a TreeNode with lazy children for self-reference", () => {
|
|
20
|
+
const FolderNode: Primitive.AnyTreeNodePrimitive = Primitive.TreeNode("folder", {
|
|
21
|
+
data: Primitive.Struct({ name: Primitive.String() }),
|
|
22
|
+
children: (): readonly Primitive.AnyTreeNodePrimitive[] => [FolderNode],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
expect(FolderNode.type).toBe("folder");
|
|
26
|
+
expect(FolderNode.children).toHaveLength(1);
|
|
27
|
+
expect(FolderNode.children[0]).toBe(FolderNode);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("isChildAllowed returns true for allowed child types", () => {
|
|
31
|
+
const FileNode = Primitive.TreeNode("file", {
|
|
32
|
+
data: Primitive.Struct({ name: Primitive.String() }),
|
|
33
|
+
children: [],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const FolderNode: Primitive.AnyTreeNodePrimitive = Primitive.TreeNode("folder", {
|
|
37
|
+
data: Primitive.Struct({ name: Primitive.String() }),
|
|
38
|
+
children: (): readonly Primitive.AnyTreeNodePrimitive[] => [FolderNode, FileNode],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(FolderNode.isChildAllowed("folder")).toBe(true);
|
|
42
|
+
expect(FolderNode.isChildAllowed("file")).toBe(true);
|
|
43
|
+
expect(FolderNode.isChildAllowed("unknown")).toBe(false);
|
|
44
|
+
expect(FileNode.isChildAllowed("file")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Tree Primitive Tests
|
|
50
|
+
// =============================================================================
|