@voidhash/mimic-effect 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.
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as Schema from "effect/Schema";
3
+ import * as Protocol from "../src/DocumentProtocol";
4
+
5
+ // =============================================================================
6
+ // Schema Tests
7
+ // =============================================================================
8
+
9
+ describe("DocumentProtocol", () => {
10
+ describe("TransactionSchema", () => {
11
+ it("should validate a valid transaction", () => {
12
+ const transaction = {
13
+ id: "tx-123",
14
+ ops: [
15
+ { kind: "string.set", path: { segments: ["title"] }, payload: "Hello" },
16
+ ],
17
+ timestamp: Date.now(),
18
+ };
19
+
20
+ const result = Schema.decodeUnknownSync(Protocol.TransactionSchema)(transaction);
21
+ expect(result.id).toBe("tx-123");
22
+ expect(result.ops).toHaveLength(1);
23
+ });
24
+
25
+ it("should reject invalid transaction", () => {
26
+ const invalid = {
27
+ id: 123, // should be string
28
+ ops: [],
29
+ timestamp: Date.now(),
30
+ };
31
+
32
+ expect(() =>
33
+ Schema.decodeUnknownSync(Protocol.TransactionSchema)(invalid)
34
+ ).toThrow();
35
+ });
36
+ });
37
+
38
+ describe("TransactionMessageSchema", () => {
39
+ it("should validate a transaction message", () => {
40
+ const message = {
41
+ type: "transaction",
42
+ transaction: {
43
+ id: "tx-456",
44
+ ops: [],
45
+ timestamp: Date.now(),
46
+ },
47
+ version: 1,
48
+ };
49
+
50
+ const result = Schema.decodeUnknownSync(Protocol.TransactionMessageSchema)(message);
51
+ expect(result.type).toBe("transaction");
52
+ expect(result.version).toBe(1);
53
+ });
54
+ });
55
+
56
+ describe("SnapshotMessageSchema", () => {
57
+ it("should validate a snapshot message", () => {
58
+ const message = {
59
+ type: "snapshot",
60
+ state: { title: "Test", count: 42 },
61
+ version: 5,
62
+ };
63
+
64
+ const result = Schema.decodeUnknownSync(Protocol.SnapshotMessageSchema)(message);
65
+ expect(result.type).toBe("snapshot");
66
+ expect(result.state).toEqual({ title: "Test", count: 42 });
67
+ expect(result.version).toBe(5);
68
+ });
69
+ });
70
+
71
+ describe("ErrorMessageSchema", () => {
72
+ it("should validate an error message", () => {
73
+ const message = {
74
+ type: "error",
75
+ transactionId: "tx-789",
76
+ reason: "Transaction is empty",
77
+ };
78
+
79
+ const result = Schema.decodeUnknownSync(Protocol.ErrorMessageSchema)(message);
80
+ expect(result.type).toBe("error");
81
+ expect(result.transactionId).toBe("tx-789");
82
+ expect(result.reason).toBe("Transaction is empty");
83
+ });
84
+ });
85
+
86
+ describe("SubmitResultSchema", () => {
87
+ it("should validate a success result", () => {
88
+ const result = {
89
+ success: true,
90
+ version: 10,
91
+ };
92
+
93
+ const decoded = Schema.decodeUnknownSync(Protocol.SubmitResultSchema)(result);
94
+ expect(decoded.success).toBe(true);
95
+ if (decoded.success) {
96
+ expect(decoded.version).toBe(10);
97
+ }
98
+ });
99
+
100
+ it("should validate a failure result", () => {
101
+ const result = {
102
+ success: false,
103
+ reason: "Invalid operation",
104
+ };
105
+
106
+ const decoded = Schema.decodeUnknownSync(Protocol.SubmitResultSchema)(result);
107
+ expect(decoded.success).toBe(false);
108
+ if (!decoded.success) {
109
+ expect(decoded.reason).toBe("Invalid operation");
110
+ }
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as InMemoryDataStorage from "../src/storage/InMemoryDataStorage";
4
+ import { MimicDataStorageTag } from "../src/MimicDataStorage";
5
+
6
+ // =============================================================================
7
+ // InMemoryDataStorage Tests
8
+ // =============================================================================
9
+
10
+ describe("InMemoryDataStorage", () => {
11
+ describe("load", () => {
12
+ it("should return undefined for non-existent documents", async () => {
13
+ const result = await Effect.runPromise(
14
+ Effect.gen(function* () {
15
+ const storage = yield* MimicDataStorageTag;
16
+ return yield* storage.load("non-existent-doc");
17
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
18
+ );
19
+
20
+ expect(result).toBeUndefined();
21
+ });
22
+ });
23
+
24
+ describe("save and load", () => {
25
+ it("should save and load document state", async () => {
26
+ const testState = { title: "Test Document", count: 42 };
27
+
28
+ const result = await Effect.runPromise(
29
+ Effect.gen(function* () {
30
+ const storage = yield* MimicDataStorageTag;
31
+ yield* storage.save("doc-1", testState);
32
+ return yield* storage.load("doc-1");
33
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
34
+ );
35
+
36
+ expect(result).toEqual(testState);
37
+ });
38
+
39
+ it("should update existing document", async () => {
40
+ const initialState = { title: "Initial", count: 0 };
41
+ const updatedState = { title: "Updated", count: 100 };
42
+
43
+ const result = await Effect.runPromise(
44
+ Effect.gen(function* () {
45
+ const storage = yield* MimicDataStorageTag;
46
+ yield* storage.save("doc-1", initialState);
47
+ yield* storage.save("doc-1", updatedState);
48
+ return yield* storage.load("doc-1");
49
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
50
+ );
51
+
52
+ expect(result).toEqual(updatedState);
53
+ });
54
+
55
+ it("should store multiple documents independently", async () => {
56
+ const state1 = { title: "Doc 1" };
57
+ const state2 = { title: "Doc 2" };
58
+ const state3 = { title: "Doc 3" };
59
+
60
+ const result = await Effect.runPromise(
61
+ Effect.gen(function* () {
62
+ const storage = yield* MimicDataStorageTag;
63
+ yield* storage.save("doc-1", state1);
64
+ yield* storage.save("doc-2", state2);
65
+ yield* storage.save("doc-3", state3);
66
+ return {
67
+ doc1: yield* storage.load("doc-1"),
68
+ doc2: yield* storage.load("doc-2"),
69
+ doc3: yield* storage.load("doc-3"),
70
+ };
71
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
72
+ );
73
+
74
+ expect(result.doc1).toEqual(state1);
75
+ expect(result.doc2).toEqual(state2);
76
+ expect(result.doc3).toEqual(state3);
77
+ });
78
+ });
79
+
80
+ describe("delete", () => {
81
+ it("should delete existing document", async () => {
82
+ const testState = { title: "To be deleted" };
83
+
84
+ const result = await Effect.runPromise(
85
+ Effect.gen(function* () {
86
+ const storage = yield* MimicDataStorageTag;
87
+ yield* storage.save("doc-1", testState);
88
+ const beforeDelete = yield* storage.load("doc-1");
89
+ yield* storage.delete("doc-1");
90
+ const afterDelete = yield* storage.load("doc-1");
91
+ return { beforeDelete, afterDelete };
92
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
93
+ );
94
+
95
+ expect(result.beforeDelete).toEqual(testState);
96
+ expect(result.afterDelete).toBeUndefined();
97
+ });
98
+
99
+ it("should handle deleting non-existent document gracefully", async () => {
100
+ await Effect.runPromise(
101
+ Effect.gen(function* () {
102
+ const storage = yield* MimicDataStorageTag;
103
+ yield* storage.delete("non-existent-doc");
104
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
105
+ );
106
+ // Should not throw
107
+ });
108
+
109
+ it("should not affect other documents when deleting", async () => {
110
+ const state1 = { title: "Doc 1" };
111
+ const state2 = { title: "Doc 2" };
112
+
113
+ const result = await Effect.runPromise(
114
+ Effect.gen(function* () {
115
+ const storage = yield* MimicDataStorageTag;
116
+ yield* storage.save("doc-1", state1);
117
+ yield* storage.save("doc-2", state2);
118
+ yield* storage.delete("doc-1");
119
+ return {
120
+ doc1: yield* storage.load("doc-1"),
121
+ doc2: yield* storage.load("doc-2"),
122
+ };
123
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
124
+ );
125
+
126
+ expect(result.doc1).toBeUndefined();
127
+ expect(result.doc2).toEqual(state2);
128
+ });
129
+ });
130
+
131
+ describe("onLoad", () => {
132
+ it("should pass through state unchanged", async () => {
133
+ const testState = { title: "Test", nested: { value: 123 } };
134
+
135
+ const result = await Effect.runPromise(
136
+ Effect.gen(function* () {
137
+ const storage = yield* MimicDataStorageTag;
138
+ return yield* storage.onLoad(testState);
139
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
140
+ );
141
+
142
+ expect(result).toBe(testState);
143
+ });
144
+ });
145
+
146
+ describe("onSave", () => {
147
+ it("should pass through state unchanged", async () => {
148
+ const testState = { title: "Test", nested: { value: 456 } };
149
+
150
+ const result = await Effect.runPromise(
151
+ Effect.gen(function* () {
152
+ const storage = yield* MimicDataStorageTag;
153
+ return yield* storage.onSave(testState);
154
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
155
+ );
156
+
157
+ expect(result).toBe(testState);
158
+ });
159
+ });
160
+
161
+ describe("layer aliases", () => {
162
+ it("should have layerDefault as an alias for layer", () => {
163
+ expect(InMemoryDataStorage.layerDefault).toBe(InMemoryDataStorage.layer);
164
+ });
165
+ });
166
+
167
+ describe("isolation", () => {
168
+ it("should have independent storage per layer instance", async () => {
169
+ const testState = { title: "Isolated" };
170
+
171
+ // Save in one layer
172
+ await Effect.runPromise(
173
+ Effect.gen(function* () {
174
+ const storage = yield* MimicDataStorageTag;
175
+ yield* storage.save("doc-1", testState);
176
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
177
+ );
178
+
179
+ // Load in a new layer instance - should not find the document
180
+ const result = await Effect.runPromise(
181
+ Effect.gen(function* () {
182
+ const storage = yield* MimicDataStorageTag;
183
+ return yield* storage.load("doc-1");
184
+ }).pipe(Effect.provide(InMemoryDataStorage.layer))
185
+ );
186
+
187
+ expect(result).toBeUndefined();
188
+ });
189
+ });
190
+ });
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as MimicAuthService from "../src/MimicAuthService";
4
+
5
+ // =============================================================================
6
+ // MimicAuthService Tests
7
+ // =============================================================================
8
+
9
+ describe("MimicAuthService", () => {
10
+ describe("make", () => {
11
+ it("should create auth service with sync handler", async () => {
12
+ const authService = MimicAuthService.make((token) => ({
13
+ success: true,
14
+ userId: `user-${token}`,
15
+ }));
16
+
17
+ const result = await Effect.runPromise(
18
+ authService.authenticate("test-token")
19
+ );
20
+
21
+ expect(result.success).toBe(true);
22
+ if (result.success) {
23
+ expect(result.userId).toBe("user-test-token");
24
+ }
25
+ });
26
+
27
+ it("should create auth service with async handler", async () => {
28
+ const authService = MimicAuthService.make(async (token) => {
29
+ await new Promise((resolve) => setTimeout(resolve, 10));
30
+ return { success: true, userId: `async-${token}` };
31
+ });
32
+
33
+ const result = await Effect.runPromise(
34
+ authService.authenticate("async-token")
35
+ );
36
+
37
+ expect(result.success).toBe(true);
38
+ if (result.success) {
39
+ expect(result.userId).toBe("async-async-token");
40
+ }
41
+ });
42
+
43
+ it("should handle auth failure", async () => {
44
+ const authService = MimicAuthService.make((_token) => ({
45
+ success: false,
46
+ error: "Invalid token",
47
+ }));
48
+
49
+ const result = await Effect.runPromise(
50
+ authService.authenticate("bad-token")
51
+ );
52
+
53
+ expect(result.success).toBe(false);
54
+ if (!result.success) {
55
+ expect(result.error).toBe("Invalid token");
56
+ }
57
+ });
58
+
59
+ it("should handle success without userId", async () => {
60
+ const authService = MimicAuthService.make((_token) => ({
61
+ success: true,
62
+ }));
63
+
64
+ const result = await Effect.runPromise(
65
+ authService.authenticate("token")
66
+ );
67
+
68
+ expect(result.success).toBe(true);
69
+ if (result.success) {
70
+ expect(result.userId).toBeUndefined();
71
+ }
72
+ });
73
+ });
74
+
75
+ describe("makeEffect", () => {
76
+ it("should create auth service from Effect-based authenticate function", async () => {
77
+ const authService = MimicAuthService.makeEffect((token) =>
78
+ Effect.succeed({ success: true as const, userId: `effect-${token}` })
79
+ );
80
+
81
+ const result = await Effect.runPromise(
82
+ authService.authenticate("effect-token")
83
+ );
84
+
85
+ expect(result.success).toBe(true);
86
+ if (result.success) {
87
+ expect(result.userId).toBe("effect-effect-token");
88
+ }
89
+ });
90
+
91
+ it("should support Effect operations in authenticate", async () => {
92
+ const authService = MimicAuthService.makeEffect((token) =>
93
+ Effect.gen(function* () {
94
+ yield* Effect.sleep(10);
95
+ return { success: true as const, userId: `delayed-${token}` };
96
+ })
97
+ );
98
+
99
+ const result = await Effect.runPromise(
100
+ authService.authenticate("delay-token")
101
+ );
102
+
103
+ expect(result.success).toBe(true);
104
+ if (result.success) {
105
+ expect(result.userId).toBe("delayed-delay-token");
106
+ }
107
+ });
108
+ });
109
+
110
+ describe("layer", () => {
111
+ it("should create a layer from auth handler", async () => {
112
+ const testLayer = MimicAuthService.layer({
113
+ authHandler: (token) => ({ success: true, userId: `layer-${token}` }),
114
+ });
115
+
116
+ const result = await Effect.runPromise(
117
+ Effect.gen(function* () {
118
+ const authService = yield* MimicAuthService.MimicAuthServiceTag;
119
+ return yield* authService.authenticate("layer-token");
120
+ }).pipe(Effect.provide(testLayer))
121
+ );
122
+
123
+ expect(result.success).toBe(true);
124
+ if (result.success) {
125
+ expect(result.userId).toBe("layer-layer-token");
126
+ }
127
+ });
128
+ });
129
+
130
+ describe("layerService", () => {
131
+ it("should create a layer from service implementation", async () => {
132
+ const service = MimicAuthService.make((token) => ({
133
+ success: true,
134
+ userId: `service-${token}`,
135
+ }));
136
+
137
+ const testLayer = MimicAuthService.layerService(service);
138
+
139
+ const result = await Effect.runPromise(
140
+ Effect.gen(function* () {
141
+ const authService = yield* MimicAuthService.MimicAuthServiceTag;
142
+ return yield* authService.authenticate("service-token");
143
+ }).pipe(Effect.provide(testLayer))
144
+ );
145
+
146
+ expect(result.success).toBe(true);
147
+ if (result.success) {
148
+ expect(result.userId).toBe("service-service-token");
149
+ }
150
+ });
151
+ });
152
+
153
+ describe("layerEffect", () => {
154
+ it("should create a layer from an Effect", async () => {
155
+ const testLayer = MimicAuthService.layerEffect(
156
+ Effect.succeed(
157
+ MimicAuthService.make((token) => ({
158
+ success: true,
159
+ userId: `effect-layer-${token}`,
160
+ }))
161
+ )
162
+ );
163
+
164
+ const result = await Effect.runPromise(
165
+ Effect.gen(function* () {
166
+ const authService = yield* MimicAuthService.MimicAuthServiceTag;
167
+ return yield* authService.authenticate("effect-layer-token");
168
+ }).pipe(Effect.provide(testLayer))
169
+ );
170
+
171
+ expect(result.success).toBe(true);
172
+ if (result.success) {
173
+ expect(result.userId).toBe("effect-layer-effect-layer-token");
174
+ }
175
+ });
176
+ });
177
+
178
+ describe("MimicAuthServiceTag", () => {
179
+ it("should have the correct tag identifier", () => {
180
+ expect(MimicAuthService.MimicAuthServiceTag.key).toBe(
181
+ "@voidhash/mimic-server-effect/MimicAuthService"
182
+ );
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Duration from "effect/Duration";
4
+ import * as Schema from "effect/Schema";
5
+ import { Primitive, Presence } from "@voidhash/mimic";
6
+ import * as MimicConfig from "../src/MimicConfig";
7
+
8
+ // =============================================================================
9
+ // Test Schema
10
+ // =============================================================================
11
+
12
+ const TestSchema = Primitive.Struct({
13
+ title: Primitive.String().default(""),
14
+ count: Primitive.Number().default(0),
15
+ });
16
+
17
+ // =============================================================================
18
+ // MimicConfig Tests
19
+ // =============================================================================
20
+
21
+ describe("MimicConfig", () => {
22
+ describe("make", () => {
23
+ it("should create config with default values", () => {
24
+ const config = MimicConfig.make({
25
+ schema: TestSchema,
26
+ });
27
+
28
+ expect(config.schema).toBe(TestSchema);
29
+ expect(Duration.toMillis(config.maxIdleTime)).toBe(5 * 60 * 1000); // 5 minutes
30
+ expect(config.maxTransactionHistory).toBe(1000);
31
+ expect(Duration.toMillis(config.heartbeatInterval)).toBe(30 * 1000); // 30 seconds
32
+ expect(Duration.toMillis(config.heartbeatTimeout)).toBe(10 * 1000); // 10 seconds
33
+ });
34
+
35
+ it("should accept custom maxIdleTime", () => {
36
+ const config = MimicConfig.make({
37
+ schema: TestSchema,
38
+ maxIdleTime: "10 minutes",
39
+ });
40
+
41
+ expect(Duration.toMillis(config.maxIdleTime)).toBe(10 * 60 * 1000);
42
+ });
43
+
44
+ it("should accept custom maxTransactionHistory", () => {
45
+ const config = MimicConfig.make({
46
+ schema: TestSchema,
47
+ maxTransactionHistory: 500,
48
+ });
49
+
50
+ expect(config.maxTransactionHistory).toBe(500);
51
+ });
52
+
53
+ it("should accept custom heartbeatInterval", () => {
54
+ const config = MimicConfig.make({
55
+ schema: TestSchema,
56
+ heartbeatInterval: "1 minute",
57
+ });
58
+
59
+ expect(Duration.toMillis(config.heartbeatInterval)).toBe(60 * 1000);
60
+ });
61
+
62
+ it("should accept custom heartbeatTimeout", () => {
63
+ const config = MimicConfig.make({
64
+ schema: TestSchema,
65
+ heartbeatTimeout: "30 seconds",
66
+ });
67
+
68
+ expect(Duration.toMillis(config.heartbeatTimeout)).toBe(30 * 1000);
69
+ });
70
+
71
+ it("should accept all custom values", () => {
72
+ const config = MimicConfig.make({
73
+ schema: TestSchema,
74
+ maxIdleTime: "15 minutes",
75
+ maxTransactionHistory: 2000,
76
+ heartbeatInterval: "45 seconds",
77
+ heartbeatTimeout: "15 seconds",
78
+ });
79
+
80
+ expect(config.schema).toBe(TestSchema);
81
+ expect(Duration.toMillis(config.maxIdleTime)).toBe(15 * 60 * 1000);
82
+ expect(config.maxTransactionHistory).toBe(2000);
83
+ expect(Duration.toMillis(config.heartbeatInterval)).toBe(45 * 1000);
84
+ expect(Duration.toMillis(config.heartbeatTimeout)).toBe(15 * 1000);
85
+ });
86
+ });
87
+
88
+ describe("layer", () => {
89
+ it("should create a layer that provides MimicServerConfigTag", async () => {
90
+ const testLayer = MimicConfig.layer({
91
+ schema: TestSchema,
92
+ maxTransactionHistory: 100,
93
+ });
94
+
95
+ const result = await Effect.runPromise(
96
+ Effect.gen(function* () {
97
+ const config = yield* MimicConfig.MimicServerConfigTag;
98
+ return config;
99
+ }).pipe(Effect.provide(testLayer))
100
+ );
101
+
102
+ expect(result.schema).toBe(TestSchema);
103
+ expect(result.maxTransactionHistory).toBe(100);
104
+ });
105
+ });
106
+
107
+ describe("MimicServerConfigTag", () => {
108
+ it("should have the correct tag identifier", () => {
109
+ expect(MimicConfig.MimicServerConfigTag.key).toBe(
110
+ "@voidhash/mimic-server-effect/MimicServerConfig"
111
+ );
112
+ });
113
+ });
114
+
115
+ describe("presence configuration", () => {
116
+ const CursorPresence = Presence.make({
117
+ schema: Schema.Struct({
118
+ x: Schema.Number,
119
+ y: Schema.Number,
120
+ name: Schema.optional(Schema.String),
121
+ }),
122
+ });
123
+
124
+ it("should have undefined presence by default", () => {
125
+ const config = MimicConfig.make({
126
+ schema: TestSchema,
127
+ });
128
+
129
+ expect(config.presence).toBeUndefined();
130
+ });
131
+
132
+ it("should accept presence schema option", () => {
133
+ const config = MimicConfig.make({
134
+ schema: TestSchema,
135
+ presence: CursorPresence,
136
+ });
137
+
138
+ expect(config.presence).toBe(CursorPresence);
139
+ });
140
+
141
+ it("should provide presence through layer", async () => {
142
+ const testLayer = MimicConfig.layer({
143
+ schema: TestSchema,
144
+ presence: CursorPresence,
145
+ });
146
+
147
+ const result = await Effect.runPromise(
148
+ Effect.gen(function* () {
149
+ const config = yield* MimicConfig.MimicServerConfigTag;
150
+ return config.presence;
151
+ }).pipe(Effect.provide(testLayer))
152
+ );
153
+
154
+ expect(result).toBe(CursorPresence);
155
+ });
156
+
157
+ it("should work with all options including presence", () => {
158
+ const config = MimicConfig.make({
159
+ schema: TestSchema,
160
+ maxIdleTime: "10 minutes",
161
+ maxTransactionHistory: 500,
162
+ heartbeatInterval: "1 minute",
163
+ heartbeatTimeout: "20 seconds",
164
+ presence: CursorPresence,
165
+ });
166
+
167
+ expect(config.schema).toBe(TestSchema);
168
+ expect(Duration.toMillis(config.maxIdleTime)).toBe(10 * 60 * 1000);
169
+ expect(config.maxTransactionHistory).toBe(500);
170
+ expect(Duration.toMillis(config.heartbeatInterval)).toBe(60 * 1000);
171
+ expect(Duration.toMillis(config.heartbeatTimeout)).toBe(20 * 1000);
172
+ expect(config.presence).toBe(CursorPresence);
173
+ });
174
+ });
175
+ });