@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,122 @@
|
|
|
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("LiteralPrimitive", () => {
|
|
8
|
+
describe("proxy", () => {
|
|
9
|
+
it("set() generates correct operation with string literal", () => {
|
|
10
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
11
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
12
|
+
|
|
13
|
+
const literalPrimitive = Primitive.Literal("active" as const);
|
|
14
|
+
const proxy = literalPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
15
|
+
|
|
16
|
+
proxy.set("active");
|
|
17
|
+
|
|
18
|
+
expect(operations).toHaveLength(1);
|
|
19
|
+
expect(operations[0]!.kind).toBe("literal.set");
|
|
20
|
+
expect(operations[0]!.payload).toBe("active");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("set() generates correct operation with number literal", () => {
|
|
24
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
25
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
26
|
+
|
|
27
|
+
const literalPrimitive = Primitive.Literal(42 as const);
|
|
28
|
+
const proxy = literalPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
29
|
+
|
|
30
|
+
proxy.set(42);
|
|
31
|
+
|
|
32
|
+
expect(operations[0]!.payload).toBe(42);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("set() generates correct operation with boolean literal", () => {
|
|
36
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
37
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
38
|
+
|
|
39
|
+
const literalPrimitive = Primitive.Literal(true as const);
|
|
40
|
+
const proxy = literalPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
41
|
+
|
|
42
|
+
proxy.set(true);
|
|
43
|
+
|
|
44
|
+
expect(operations[0]!.payload).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("set() generates correct operation with null literal", () => {
|
|
48
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
49
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
50
|
+
|
|
51
|
+
const literalPrimitive = Primitive.Literal(null);
|
|
52
|
+
const proxy = literalPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
53
|
+
|
|
54
|
+
proxy.set(null);
|
|
55
|
+
|
|
56
|
+
expect(operations[0]!.payload).toBe(null);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("applyOperation", () => {
|
|
61
|
+
it("returns the literal value from literal.set operation", () => {
|
|
62
|
+
const literalPrimitive = Primitive.Literal("foo" as const);
|
|
63
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
64
|
+
kind: "literal.set",
|
|
65
|
+
path: OperationPath.make(""),
|
|
66
|
+
payload: "foo",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = literalPrimitive._internal.applyOperation(undefined, operation);
|
|
70
|
+
expect(result).toBe("foo");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws ValidationError for wrong literal value", () => {
|
|
74
|
+
const literalPrimitive = Primitive.Literal("foo" as const);
|
|
75
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
76
|
+
kind: "literal.set",
|
|
77
|
+
path: OperationPath.make(""),
|
|
78
|
+
payload: "bar",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
expect(() => literalPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
82
|
+
Primitive.ValidationError
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("throws ValidationError for wrong operation kind", () => {
|
|
87
|
+
const literalPrimitive = Primitive.Literal("foo" as const);
|
|
88
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
89
|
+
kind: "string.set",
|
|
90
|
+
path: OperationPath.make(""),
|
|
91
|
+
payload: "foo",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
expect(() => literalPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
95
|
+
Primitive.ValidationError
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("getInitialState", () => {
|
|
101
|
+
it("returns undefined when no default is set", () => {
|
|
102
|
+
const literalPrimitive = Primitive.Literal("test" as const);
|
|
103
|
+
expect(literalPrimitive._internal.getInitialState()).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns the default value when set", () => {
|
|
107
|
+
const literalPrimitive = Primitive.Literal("active" as const).default("active");
|
|
108
|
+
expect(literalPrimitive._internal.getInitialState()).toBe("active");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("literal accessor", () => {
|
|
113
|
+
it("returns the literal value", () => {
|
|
114
|
+
const literalPrimitive = Primitive.Literal("myLiteral" as const);
|
|
115
|
+
expect(literalPrimitive.literal).toBe("myLiteral");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// =============================================================================
|
|
121
|
+
// Struct Primitive Tests
|
|
122
|
+
// =============================================================================
|
|
@@ -0,0 +1,133 @@
|
|
|
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("NumberPrimitive", () => {
|
|
8
|
+
describe("proxy", () => {
|
|
9
|
+
it("set() generates correct operation with path and payload", () => {
|
|
10
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
11
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
12
|
+
|
|
13
|
+
const numberPrimitive = Primitive.Number().required();
|
|
14
|
+
const proxy = numberPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
15
|
+
|
|
16
|
+
proxy.set(42);
|
|
17
|
+
|
|
18
|
+
expect(operations).toHaveLength(1);
|
|
19
|
+
expect(operations[0]!.kind).toBe("number.set");
|
|
20
|
+
expect(operations[0]!.payload).toBe(42);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("set() works with decimal numbers", () => {
|
|
24
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
25
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
26
|
+
|
|
27
|
+
const numberPrimitive = Primitive.Number();
|
|
28
|
+
const proxy = numberPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
29
|
+
|
|
30
|
+
proxy.set(3.14159);
|
|
31
|
+
|
|
32
|
+
expect(operations[0]!.payload).toBe(3.14159);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("set() works with negative numbers", () => {
|
|
36
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
37
|
+
const env = ProxyEnvironment.make((op) => operations.push(op));
|
|
38
|
+
|
|
39
|
+
const numberPrimitive = Primitive.Number();
|
|
40
|
+
const proxy = numberPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
41
|
+
|
|
42
|
+
proxy.set(-100);
|
|
43
|
+
|
|
44
|
+
expect(operations[0]!.payload).toBe(-100);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("applyOperation", () => {
|
|
49
|
+
it("returns the new number value from number.set operation", () => {
|
|
50
|
+
const numberPrimitive = Primitive.Number();
|
|
51
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
52
|
+
kind: "number.set",
|
|
53
|
+
path: OperationPath.make(""),
|
|
54
|
+
payload: 123,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = numberPrimitive._internal.applyOperation(undefined, operation);
|
|
58
|
+
expect(result).toBe(123);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("replaces existing state with new value", () => {
|
|
62
|
+
const numberPrimitive = Primitive.Number();
|
|
63
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
64
|
+
kind: "number.set",
|
|
65
|
+
path: OperationPath.make(""),
|
|
66
|
+
payload: 999,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const result = numberPrimitive._internal.applyOperation(100, operation);
|
|
70
|
+
expect(result).toBe(999);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("throws ValidationError for non-number payload", () => {
|
|
74
|
+
const numberPrimitive = Primitive.Number();
|
|
75
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
76
|
+
kind: "number.set",
|
|
77
|
+
path: OperationPath.make(""),
|
|
78
|
+
payload: "42",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
expect(() => numberPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
82
|
+
Primitive.ValidationError
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("throws ValidationError for wrong operation kind", () => {
|
|
87
|
+
const numberPrimitive = Primitive.Number();
|
|
88
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
89
|
+
kind: "string.set",
|
|
90
|
+
path: OperationPath.make(""),
|
|
91
|
+
payload: 42,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
expect(() => numberPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
95
|
+
Primitive.ValidationError
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("getInitialState", () => {
|
|
101
|
+
it("returns undefined when no default is set", () => {
|
|
102
|
+
const numberPrimitive = Primitive.Number();
|
|
103
|
+
expect(numberPrimitive._internal.getInitialState()).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns the default value when set", () => {
|
|
107
|
+
const numberPrimitive = Primitive.Number().default(0);
|
|
108
|
+
expect(numberPrimitive._internal.getInitialState()).toBe(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("schema modifiers", () => {
|
|
113
|
+
it("required() returns a new NumberPrimitive", () => {
|
|
114
|
+
const original = Primitive.Number();
|
|
115
|
+
const required = original.required();
|
|
116
|
+
|
|
117
|
+
expect(required).toBeInstanceOf(Primitive.NumberPrimitive);
|
|
118
|
+
expect(required).not.toBe(original);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("default() returns a new NumberPrimitive with default value", () => {
|
|
122
|
+
const original = Primitive.Number();
|
|
123
|
+
const withDefault = original.default(100);
|
|
124
|
+
|
|
125
|
+
expect(withDefault).toBeInstanceOf(Primitive.NumberPrimitive);
|
|
126
|
+
expect(withDefault._internal.getInitialState()).toBe(100);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Literal Primitive Tests
|
|
133
|
+
// =============================================================================
|
|
@@ -0,0 +1,128 @@
|
|
|
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("StringPrimitive", () => {
|
|
8
|
+
describe("proxy", () => {
|
|
9
|
+
it("set() generates correct operation with path and payload", () => {
|
|
10
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
11
|
+
const env = ProxyEnvironment.make((op) => {
|
|
12
|
+
operations.push(op);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const stringPrimitive = Primitive.String().required();
|
|
16
|
+
const proxy = stringPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
17
|
+
|
|
18
|
+
proxy.set("Hello World");
|
|
19
|
+
|
|
20
|
+
expect(operations).toHaveLength(1);
|
|
21
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
22
|
+
expect(operations[0]!.payload).toBe("Hello World");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("set() includes the correct path in operation", () => {
|
|
26
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
27
|
+
const env = ProxyEnvironment.make((op) => {
|
|
28
|
+
operations.push(op);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const stringPrimitive = Primitive.String();
|
|
32
|
+
const proxy = stringPrimitive._internal.createProxy(env, OperationPath.make("users/0/name"));
|
|
33
|
+
|
|
34
|
+
proxy.set("John");
|
|
35
|
+
|
|
36
|
+
expect(operations).toHaveLength(1);
|
|
37
|
+
expect(operations[0]!.path.toTokens()).toEqual(["users", "0", "name"]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("applyOperation", () => {
|
|
42
|
+
it("returns the new string value from string.set operation", () => {
|
|
43
|
+
const stringPrimitive = Primitive.String();
|
|
44
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
45
|
+
kind: "string.set",
|
|
46
|
+
path: OperationPath.make(""),
|
|
47
|
+
payload: "New Value",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const result = stringPrimitive._internal.applyOperation(undefined, operation);
|
|
51
|
+
|
|
52
|
+
expect(result).toBe("New Value");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("replaces existing state with new value", () => {
|
|
56
|
+
const stringPrimitive = Primitive.String();
|
|
57
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
58
|
+
kind: "string.set",
|
|
59
|
+
path: OperationPath.make(""),
|
|
60
|
+
payload: "Updated",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const result = stringPrimitive._internal.applyOperation("Original", operation);
|
|
64
|
+
|
|
65
|
+
expect(result).toBe("Updated");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("throws ValidationError for non-string payload", () => {
|
|
69
|
+
const stringPrimitive = Primitive.String();
|
|
70
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
71
|
+
kind: "string.set",
|
|
72
|
+
path: OperationPath.make(""),
|
|
73
|
+
payload: 123,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
expect(() => stringPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
77
|
+
Primitive.ValidationError
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("throws ValidationError for wrong operation kind", () => {
|
|
82
|
+
const stringPrimitive = Primitive.String();
|
|
83
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
84
|
+
kind: "wrong.kind",
|
|
85
|
+
path: OperationPath.make(""),
|
|
86
|
+
payload: "value",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
expect(() => stringPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
90
|
+
Primitive.ValidationError
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("getInitialState", () => {
|
|
96
|
+
it("returns undefined when no default is set", () => {
|
|
97
|
+
const stringPrimitive = Primitive.String();
|
|
98
|
+
expect(stringPrimitive._internal.getInitialState()).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns the default value when set", () => {
|
|
102
|
+
const stringPrimitive = Primitive.String().default("Default Text");
|
|
103
|
+
expect(stringPrimitive._internal.getInitialState()).toBe("Default Text");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("schema modifiers", () => {
|
|
108
|
+
it("required() returns a new StringPrimitive", () => {
|
|
109
|
+
const original = Primitive.String();
|
|
110
|
+
const required = original.required();
|
|
111
|
+
|
|
112
|
+
expect(required).toBeInstanceOf(Primitive.StringPrimitive);
|
|
113
|
+
expect(required).not.toBe(original);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("default() returns a new StringPrimitive with default value", () => {
|
|
117
|
+
const original = Primitive.String();
|
|
118
|
+
const withDefault = original.default("test");
|
|
119
|
+
|
|
120
|
+
expect(withDefault).toBeInstanceOf(Primitive.StringPrimitive);
|
|
121
|
+
expect(withDefault._internal.getInitialState()).toBe("test");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Boolean Primitive Tests
|
|
128
|
+
// =============================================================================
|
|
@@ -0,0 +1,311 @@
|
|
|
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("StructPrimitive", () => {
|
|
8
|
+
describe("proxy", () => {
|
|
9
|
+
it("nested field access returns field primitive proxy", () => {
|
|
10
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
11
|
+
const env = ProxyEnvironment.make((op) => {
|
|
12
|
+
operations.push(op);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const structPrimitive = Primitive.Struct({
|
|
16
|
+
name: Primitive.String().required(),
|
|
17
|
+
title: Primitive.String(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
21
|
+
|
|
22
|
+
// Access nested field and call set
|
|
23
|
+
proxy.name.set("John Doe");
|
|
24
|
+
|
|
25
|
+
expect(operations).toHaveLength(1);
|
|
26
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
27
|
+
expect(operations[0]!.payload).toBe("John Doe");
|
|
28
|
+
expect(operations[0]!.path.toTokens()).toEqual(["name"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("nested field paths are constructed correctly", () => {
|
|
32
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
33
|
+
const env = ProxyEnvironment.make((op) => {
|
|
34
|
+
operations.push(op);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const structPrimitive = Primitive.Struct({
|
|
38
|
+
name: Primitive.String(),
|
|
39
|
+
email: Primitive.String(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make("users/0"));
|
|
43
|
+
|
|
44
|
+
proxy.email.set("test@example.com");
|
|
45
|
+
|
|
46
|
+
expect(operations[0]!.path.toTokens()).toEqual(["users", "0", "email"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("set() on struct generates struct.set operation", () => {
|
|
50
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
51
|
+
const env = ProxyEnvironment.make((op) => {
|
|
52
|
+
operations.push(op);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const structPrimitive = Primitive.Struct({
|
|
56
|
+
name: Primitive.String(),
|
|
57
|
+
age: Primitive.String(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
61
|
+
|
|
62
|
+
proxy.set({ name: "Alice", age: "30" });
|
|
63
|
+
|
|
64
|
+
expect(operations).toHaveLength(1);
|
|
65
|
+
expect(operations[0]!.kind).toBe("struct.set");
|
|
66
|
+
expect(operations[0]!.payload).toEqual({ name: "Alice", age: "30" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("multiple field sets generate separate operations", () => {
|
|
70
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
71
|
+
const env = ProxyEnvironment.make((op) => {
|
|
72
|
+
operations.push(op);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const structPrimitive = Primitive.Struct({
|
|
76
|
+
firstName: Primitive.String(),
|
|
77
|
+
lastName: Primitive.String(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const proxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
81
|
+
|
|
82
|
+
proxy.firstName.set("John");
|
|
83
|
+
proxy.lastName.set("Doe");
|
|
84
|
+
|
|
85
|
+
expect(operations).toHaveLength(2);
|
|
86
|
+
expect(operations[0]!.payload).toBe("John");
|
|
87
|
+
expect(operations[1]!.payload).toBe("Doe");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("applyOperation", () => {
|
|
92
|
+
it("struct.set replaces entire struct state", () => {
|
|
93
|
+
const structPrimitive = Primitive.Struct({
|
|
94
|
+
name: Primitive.String(),
|
|
95
|
+
email: Primitive.String(),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
99
|
+
kind: "struct.set",
|
|
100
|
+
path: OperationPath.make(""),
|
|
101
|
+
payload: { name: "Bob", email: "bob@test.com" },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = structPrimitive._internal.applyOperation(undefined, operation);
|
|
105
|
+
|
|
106
|
+
expect(result).toEqual({ name: "Bob", email: "bob@test.com" });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("delegates field operations to nested primitives", () => {
|
|
110
|
+
const structPrimitive = Primitive.Struct({
|
|
111
|
+
name: Primitive.String(),
|
|
112
|
+
title: Primitive.String(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
116
|
+
kind: "string.set",
|
|
117
|
+
path: OperationPath.make("name"),
|
|
118
|
+
payload: "Updated Name",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const currentState = { name: "Original", title: "Mr" };
|
|
122
|
+
const result = structPrimitive._internal.applyOperation(currentState, operation);
|
|
123
|
+
|
|
124
|
+
expect(result).toEqual({ name: "Updated Name", title: "Mr" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("creates state from undefined when applying field operation", () => {
|
|
128
|
+
const structPrimitive = Primitive.Struct({
|
|
129
|
+
name: Primitive.String(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
133
|
+
kind: "string.set",
|
|
134
|
+
path: OperationPath.make("name"),
|
|
135
|
+
payload: "New Name",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const result = structPrimitive._internal.applyOperation(undefined, operation);
|
|
139
|
+
|
|
140
|
+
expect(result).toEqual({ name: "New Name" });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("throws ValidationError for unknown field", () => {
|
|
144
|
+
const structPrimitive = Primitive.Struct({
|
|
145
|
+
name: Primitive.String(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
149
|
+
kind: "string.set",
|
|
150
|
+
path: OperationPath.make("unknownField"),
|
|
151
|
+
payload: "value",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
expect(() => structPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
155
|
+
Primitive.ValidationError
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("throws ValidationError for non-object payload on struct.set", () => {
|
|
160
|
+
const structPrimitive = Primitive.Struct({
|
|
161
|
+
name: Primitive.String(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
165
|
+
kind: "struct.set",
|
|
166
|
+
path: OperationPath.make(""),
|
|
167
|
+
payload: "not an object",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
expect(() => structPrimitive._internal.applyOperation(undefined, operation)).toThrow(
|
|
171
|
+
Primitive.ValidationError
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("getInitialState", () => {
|
|
177
|
+
it("returns undefined when no field has defaults", () => {
|
|
178
|
+
const structPrimitive = Primitive.Struct({
|
|
179
|
+
name: Primitive.String(),
|
|
180
|
+
email: Primitive.String(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(structPrimitive._internal.getInitialState()).toBeUndefined();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("returns partial state from field defaults", () => {
|
|
187
|
+
const structPrimitive = Primitive.Struct({
|
|
188
|
+
name: Primitive.String().default("Anonymous"),
|
|
189
|
+
email: Primitive.String(),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const initialState = structPrimitive._internal.getInitialState();
|
|
193
|
+
|
|
194
|
+
expect(initialState).toEqual({ name: "Anonymous" });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("returns complete state when all fields have defaults", () => {
|
|
198
|
+
const structPrimitive = Primitive.Struct({
|
|
199
|
+
name: Primitive.String().default("Guest"),
|
|
200
|
+
role: Primitive.String().default("user"),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const initialState = structPrimitive._internal.getInitialState();
|
|
204
|
+
|
|
205
|
+
expect(initialState).toEqual({ name: "Guest", role: "user" });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("uses struct default value over field defaults", () => {
|
|
209
|
+
const structPrimitive = Primitive.Struct({
|
|
210
|
+
name: Primitive.String().default("Field Default"),
|
|
211
|
+
}).default({ name: "Struct Default" });
|
|
212
|
+
|
|
213
|
+
const initialState = structPrimitive._internal.getInitialState();
|
|
214
|
+
|
|
215
|
+
expect(initialState).toEqual({ name: "Struct Default" });
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("nested structs", () => {
|
|
220
|
+
it("supports nested struct primitives", () => {
|
|
221
|
+
const operations: Operation.Operation<any, any, any>[] = [];
|
|
222
|
+
const env = ProxyEnvironment.make((op) => {
|
|
223
|
+
operations.push(op);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const addressPrimitive = Primitive.Struct({
|
|
227
|
+
street: Primitive.String(),
|
|
228
|
+
city: Primitive.String(),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const personPrimitive = Primitive.Struct({
|
|
232
|
+
name: Primitive.String(),
|
|
233
|
+
address: addressPrimitive,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const proxy = personPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
237
|
+
|
|
238
|
+
proxy.address.city.set("New York");
|
|
239
|
+
|
|
240
|
+
expect(operations).toHaveLength(1);
|
|
241
|
+
expect(operations[0]!.kind).toBe("string.set");
|
|
242
|
+
expect(operations[0]!.payload).toBe("New York");
|
|
243
|
+
expect(operations[0]!.path.toTokens()).toEqual(["address", "city"]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("applies operations to nested structs", () => {
|
|
247
|
+
const addressPrimitive = Primitive.Struct({
|
|
248
|
+
street: Primitive.String(),
|
|
249
|
+
city: Primitive.String(),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const personPrimitive = Primitive.Struct({
|
|
253
|
+
name: Primitive.String(),
|
|
254
|
+
address: addressPrimitive,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const operation: Operation.Operation<any, any, any> = {
|
|
258
|
+
kind: "string.set",
|
|
259
|
+
path: OperationPath.make("address/city"),
|
|
260
|
+
payload: "Los Angeles",
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const currentState = {
|
|
264
|
+
name: "John",
|
|
265
|
+
address: { street: "123 Main St", city: "San Francisco" },
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const result = personPrimitive._internal.applyOperation(currentState, operation);
|
|
269
|
+
|
|
270
|
+
expect(result).toEqual({
|
|
271
|
+
name: "John",
|
|
272
|
+
address: { street: "123 Main St", city: "Los Angeles" },
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("type inference", () => {
|
|
278
|
+
it("infers correct state type from struct definition", () => {
|
|
279
|
+
const structPrimitive = Primitive.Struct({
|
|
280
|
+
name: Primitive.String(),
|
|
281
|
+
email: Primitive.String(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// This is a compile-time check - the type should be inferred correctly
|
|
285
|
+
type ExpectedState = Primitive.InferState<typeof structPrimitive>;
|
|
286
|
+
const state: ExpectedState = { name: "test", email: "test@test.com" };
|
|
287
|
+
|
|
288
|
+
expect(state).toEqual({ name: "test", email: "test@test.com" });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("infers correct proxy type with field access", () => {
|
|
292
|
+
const structPrimitive = Primitive.Struct({
|
|
293
|
+
name: Primitive.String(),
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
type ExpectedProxy = Primitive.InferProxy<typeof structPrimitive>;
|
|
297
|
+
|
|
298
|
+
// This would fail at compile time if types are wrong
|
|
299
|
+
const env = ProxyEnvironment.make(() => {});
|
|
300
|
+
const proxy: ExpectedProxy = structPrimitive._internal.createProxy(env, OperationPath.make(""));
|
|
301
|
+
|
|
302
|
+
// Verify the proxy has the expected shape
|
|
303
|
+
expect(typeof proxy.name.set).toBe("function");
|
|
304
|
+
expect(typeof proxy.set).toBe("function");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// =============================================================================
|
|
310
|
+
// Array Primitive Tests (Ordered with ID + Fractional Index)
|
|
311
|
+
// =============================================================================
|