@voidhash/mimic-effect 1.0.0-beta.1 → 1.0.0-beta.10
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/.turbo/turbo-build.log +116 -74
- package/dist/ColdStorage.cjs +9 -5
- package/dist/ColdStorage.d.cts.map +1 -1
- package/dist/ColdStorage.d.mts.map +1 -1
- package/dist/ColdStorage.mjs +9 -5
- package/dist/ColdStorage.mjs.map +1 -1
- package/dist/DocumentInstance.cjs +263 -0
- package/dist/DocumentInstance.d.cts +78 -0
- package/dist/DocumentInstance.d.cts.map +1 -0
- package/dist/DocumentInstance.d.mts +78 -0
- package/dist/DocumentInstance.d.mts.map +1 -0
- package/dist/DocumentInstance.mjs +264 -0
- package/dist/DocumentInstance.mjs.map +1 -0
- package/dist/Errors.cjs +10 -1
- package/dist/Errors.d.cts +18 -3
- package/dist/Errors.d.cts.map +1 -1
- package/dist/Errors.d.mts +18 -3
- package/dist/Errors.d.mts.map +1 -1
- package/dist/Errors.mjs +9 -1
- package/dist/Errors.mjs.map +1 -1
- package/dist/HotStorage.cjs +39 -12
- package/dist/HotStorage.d.cts +17 -1
- package/dist/HotStorage.d.cts.map +1 -1
- package/dist/HotStorage.d.mts +17 -1
- package/dist/HotStorage.d.mts.map +1 -1
- package/dist/HotStorage.mjs +39 -12
- package/dist/HotStorage.mjs.map +1 -1
- package/dist/Metrics.cjs +29 -1
- package/dist/Metrics.d.cts +5 -0
- package/dist/Metrics.d.cts.map +1 -1
- package/dist/Metrics.d.mts +5 -0
- package/dist/Metrics.d.mts.map +1 -1
- package/dist/Metrics.mjs +26 -1
- package/dist/Metrics.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +44 -139
- package/dist/MimicClusterServerEngine.d.cts.map +1 -1
- package/dist/MimicClusterServerEngine.d.mts +1 -1
- package/dist/MimicClusterServerEngine.d.mts.map +1 -1
- package/dist/MimicClusterServerEngine.mjs +46 -141
- package/dist/MimicClusterServerEngine.mjs.map +1 -1
- package/dist/MimicServer.cjs +20 -20
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +20 -20
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +92 -11
- package/dist/MimicServerEngine.d.cts +12 -4
- package/dist/MimicServerEngine.d.cts.map +1 -1
- package/dist/MimicServerEngine.d.mts +12 -4
- package/dist/MimicServerEngine.d.mts.map +1 -1
- package/dist/MimicServerEngine.mjs +94 -13
- package/dist/MimicServerEngine.mjs.map +1 -1
- package/dist/PresenceManager.cjs +5 -5
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +5 -5
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.d.cts +1 -1
- package/dist/Protocol.d.mts +1 -1
- package/dist/Types.d.cts +9 -2
- package/dist/Types.d.cts.map +1 -1
- package/dist/Types.d.mts +9 -2
- package/dist/Types.d.mts.map +1 -1
- package/dist/index.cjs +5 -6
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- package/dist/testing/ColdStorageTestSuite.cjs +508 -0
- package/dist/testing/ColdStorageTestSuite.d.cts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.d.mts +36 -0
- package/dist/testing/ColdStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/ColdStorageTestSuite.mjs +508 -0
- package/dist/testing/ColdStorageTestSuite.mjs.map +1 -0
- package/dist/testing/FailingStorage.cjs +162 -0
- package/dist/testing/FailingStorage.d.cts +43 -0
- package/dist/testing/FailingStorage.d.cts.map +1 -0
- package/dist/testing/FailingStorage.d.mts +43 -0
- package/dist/testing/FailingStorage.d.mts.map +1 -0
- package/dist/testing/FailingStorage.mjs +163 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +820 -0
- package/dist/testing/HotStorageTestSuite.d.cts +42 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +42 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +820 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +117 -0
- package/dist/testing/assertions.mjs +112 -0
- package/dist/testing/assertions.mjs.map +1 -0
- package/dist/testing/index.cjs +14 -0
- package/dist/testing/index.d.cts +6 -0
- package/dist/testing/index.d.mts +6 -0
- package/dist/testing/index.mjs +7 -0
- package/dist/testing/types.cjs +15 -0
- package/dist/testing/types.d.cts +90 -0
- package/dist/testing/types.d.cts.map +1 -0
- package/dist/testing/types.d.mts +90 -0
- package/dist/testing/types.d.mts.map +1 -0
- package/dist/testing/types.mjs +16 -0
- package/dist/testing/types.mjs.map +1 -0
- package/package.json +8 -3
- package/src/ColdStorage.ts +21 -12
- package/src/DocumentInstance.ts +527 -0
- package/src/Errors.ts +15 -1
- package/src/HotStorage.ts +115 -24
- package/src/Metrics.ts +30 -0
- package/src/MimicClusterServerEngine.ts +120 -275
- package/src/MimicServer.ts +83 -75
- package/src/MimicServerEngine.ts +230 -30
- package/src/PresenceManager.ts +44 -34
- package/src/Types.ts +9 -2
- package/src/index.ts +5 -35
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +338 -0
- package/src/testing/HotStorageTestSuite.ts +1105 -0
- package/src/testing/StorageIntegrationTestSuite.ts +736 -0
- package/src/testing/assertions.ts +188 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +8 -120
- package/tests/DocumentInstance.test.ts +669 -0
- package/tests/HotStorage.test.ts +7 -126
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsdown.config.ts +1 -1
- package/dist/DocumentManager.cjs +0 -229
- package/dist/DocumentManager.d.cts +0 -59
- package/dist/DocumentManager.d.cts.map +0 -1
- package/dist/DocumentManager.d.mts +0 -59
- package/dist/DocumentManager.d.mts.map +0 -1
- package/dist/DocumentManager.mjs +0 -227
- package/dist/DocumentManager.mjs.map +0 -1
- package/src/DocumentManager.ts +0 -506
- package/tests/DocumentManager.test.ts +0 -335
package/tests/HotStorage.test.ts
CHANGED
|
@@ -1,138 +1,19 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { Effect } from "effect";
|
|
3
3
|
import { HotStorage, HotStorageTag } from "../src/HotStorage";
|
|
4
|
-
import
|
|
5
|
-
import { Transaction } from "@voidhash/mimic";
|
|
4
|
+
import { HotStorageTestSuite } from "../src/testing";
|
|
6
5
|
|
|
7
6
|
describe("HotStorage", () => {
|
|
8
7
|
describe("InMemory", () => {
|
|
8
|
+
// Use the test suite utilities for comprehensive testing
|
|
9
9
|
const layer = HotStorage.InMemory.make();
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("should return empty array for missing document", async () => {
|
|
18
|
-
const result = await Effect.runPromise(
|
|
19
|
-
Effect.gen(function* () {
|
|
20
|
-
const storage = yield* HotStorageTag;
|
|
21
|
-
return yield* storage.getEntries("non-existent", 0);
|
|
22
|
-
}).pipe(Effect.provide(layer))
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
expect(result).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("should append and retrieve entries", async () => {
|
|
29
|
-
const entry1 = makeEntry(1);
|
|
30
|
-
const entry2 = makeEntry(2);
|
|
31
|
-
|
|
32
|
-
const result = await Effect.runPromise(
|
|
33
|
-
Effect.gen(function* () {
|
|
34
|
-
const storage = yield* HotStorageTag;
|
|
35
|
-
yield* storage.append("doc-1", entry1);
|
|
36
|
-
yield* storage.append("doc-1", entry2);
|
|
37
|
-
return yield* storage.getEntries("doc-1", 0);
|
|
38
|
-
}).pipe(Effect.provide(layer))
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(result.length).toBe(2);
|
|
42
|
-
expect(result[0]!.version).toBe(1);
|
|
43
|
-
expect(result[1]!.version).toBe(2);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("should filter entries by sinceVersion", async () => {
|
|
47
|
-
const entries = [makeEntry(1), makeEntry(2), makeEntry(3), makeEntry(4)];
|
|
48
|
-
|
|
49
|
-
const result = await Effect.runPromise(
|
|
50
|
-
Effect.gen(function* () {
|
|
51
|
-
const storage = yield* HotStorageTag;
|
|
52
|
-
for (const entry of entries) {
|
|
53
|
-
yield* storage.append("doc-1", entry);
|
|
54
|
-
}
|
|
55
|
-
return yield* storage.getEntries("doc-1", 2);
|
|
56
|
-
}).pipe(Effect.provide(layer))
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
expect(result.length).toBe(2);
|
|
60
|
-
expect(result[0]!.version).toBe(3);
|
|
61
|
-
expect(result[1]!.version).toBe(4);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("should truncate entries up to version", async () => {
|
|
65
|
-
const entries = [makeEntry(1), makeEntry(2), makeEntry(3), makeEntry(4)];
|
|
66
|
-
|
|
67
|
-
const result = await Effect.runPromise(
|
|
68
|
-
Effect.gen(function* () {
|
|
69
|
-
const storage = yield* HotStorageTag;
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
yield* storage.append("doc-1", entry);
|
|
72
|
-
}
|
|
73
|
-
yield* storage.truncate("doc-1", 2);
|
|
74
|
-
return yield* storage.getEntries("doc-1", 0);
|
|
75
|
-
}).pipe(Effect.provide(layer))
|
|
11
|
+
// Run all test suite tests
|
|
12
|
+
for (const test of HotStorageTestSuite.makeTests()) {
|
|
13
|
+
it(`[${test.category}] ${test.name}`, () =>
|
|
14
|
+
Effect.runPromise(test.run.pipe(Effect.provide(layer)))
|
|
76
15
|
);
|
|
77
|
-
|
|
78
|
-
expect(result.length).toBe(2);
|
|
79
|
-
expect(result[0]!.version).toBe(3);
|
|
80
|
-
expect(result[1]!.version).toBe(4);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("should maintain order after append", async () => {
|
|
84
|
-
const entries = [makeEntry(3), makeEntry(1), makeEntry(2)];
|
|
85
|
-
|
|
86
|
-
const result = await Effect.runPromise(
|
|
87
|
-
Effect.gen(function* () {
|
|
88
|
-
const storage = yield* HotStorageTag;
|
|
89
|
-
for (const entry of entries) {
|
|
90
|
-
yield* storage.append("doc-1", entry);
|
|
91
|
-
}
|
|
92
|
-
return yield* storage.getEntries("doc-1", 0);
|
|
93
|
-
}).pipe(Effect.provide(layer))
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
// Should be sorted by version
|
|
97
|
-
expect(result.length).toBe(3);
|
|
98
|
-
expect(result[0]!.version).toBe(1);
|
|
99
|
-
expect(result[1]!.version).toBe(2);
|
|
100
|
-
expect(result[2]!.version).toBe(3);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("should isolate documents", async () => {
|
|
104
|
-
const entry1 = makeEntry(1);
|
|
105
|
-
const entry2 = makeEntry(2);
|
|
106
|
-
|
|
107
|
-
const result = await Effect.runPromise(
|
|
108
|
-
Effect.gen(function* () {
|
|
109
|
-
const storage = yield* HotStorageTag;
|
|
110
|
-
yield* storage.append("doc-1", entry1);
|
|
111
|
-
yield* storage.append("doc-2", entry2);
|
|
112
|
-
|
|
113
|
-
const entries1 = yield* storage.getEntries("doc-1", 0);
|
|
114
|
-
const entries2 = yield* storage.getEntries("doc-2", 0);
|
|
115
|
-
|
|
116
|
-
return { entries1, entries2 };
|
|
117
|
-
}).pipe(Effect.provide(layer))
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
expect(result.entries1.length).toBe(1);
|
|
121
|
-
expect(result.entries1[0]!.version).toBe(1);
|
|
122
|
-
expect(result.entries2.length).toBe(1);
|
|
123
|
-
expect(result.entries2[0]!.version).toBe(2);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("should not error when truncating non-existent document", async () => {
|
|
127
|
-
await expect(
|
|
128
|
-
Effect.runPromise(
|
|
129
|
-
Effect.gen(function* () {
|
|
130
|
-
const storage = yield* HotStorageTag;
|
|
131
|
-
yield* storage.truncate("non-existent", 5);
|
|
132
|
-
}).pipe(Effect.provide(layer))
|
|
133
|
-
)
|
|
134
|
-
).resolves.toBeUndefined();
|
|
135
|
-
});
|
|
16
|
+
}
|
|
136
17
|
});
|
|
137
18
|
|
|
138
19
|
describe("Tag", () => {
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Effect, Layer } from "effect";
|
|
3
|
+
import { StorageIntegrationTestSuite } from "../src/testing/StorageIntegrationTestSuite";
|
|
4
|
+
import { FailingStorage } from "../src/testing/FailingStorage";
|
|
5
|
+
import { ColdStorage, ColdStorageTag } from "../src/ColdStorage";
|
|
6
|
+
import { HotStorage, HotStorageTag } from "../src/HotStorage";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Storage Integration Tests
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
describe("Storage Integration", () => {
|
|
13
|
+
const layer = Layer.mergeAll(
|
|
14
|
+
ColdStorage.InMemory.make(),
|
|
15
|
+
HotStorage.InMemory.make()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
for (const test of StorageIntegrationTestSuite.makeTests()) {
|
|
19
|
+
it(test.name, () =>
|
|
20
|
+
Effect.runPromise(test.run.pipe(Effect.provide(layer)))
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// =============================================================================
|
|
26
|
+
// Failure Scenario Tests
|
|
27
|
+
// =============================================================================
|
|
28
|
+
|
|
29
|
+
describe("Storage Failure Scenarios", () => {
|
|
30
|
+
describe("ColdStorage Failures", () => {
|
|
31
|
+
it("load failure propagates error", async () => {
|
|
32
|
+
const failingLayer = Layer.mergeAll(
|
|
33
|
+
FailingStorage.makeColdStorage({ failLoad: true }),
|
|
34
|
+
HotStorage.InMemory.make()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const result = await Effect.runPromise(
|
|
38
|
+
Effect.gen(function* () {
|
|
39
|
+
const cold = yield* ColdStorageTag;
|
|
40
|
+
return yield* Effect.either(cold.load("test-doc"));
|
|
41
|
+
}).pipe(Effect.provide(failingLayer))
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(result._tag).toBe("Left");
|
|
45
|
+
if (result._tag === "Left") {
|
|
46
|
+
expect(result.left._tag).toBe("ColdStorageError");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("save failure propagates error", async () => {
|
|
51
|
+
const failingLayer = Layer.mergeAll(
|
|
52
|
+
FailingStorage.makeColdStorage({ failSave: true }),
|
|
53
|
+
HotStorage.InMemory.make()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const result = await Effect.runPromise(
|
|
57
|
+
Effect.gen(function* () {
|
|
58
|
+
const cold = yield* ColdStorageTag;
|
|
59
|
+
return yield* Effect.either(
|
|
60
|
+
cold.save("test-doc", {
|
|
61
|
+
state: { data: "test" },
|
|
62
|
+
version: 1,
|
|
63
|
+
schemaVersion: 1,
|
|
64
|
+
savedAt: Date.now(),
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
}).pipe(Effect.provide(failingLayer))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(result._tag).toBe("Left");
|
|
71
|
+
if (result._tag === "Left") {
|
|
72
|
+
expect(result.left._tag).toBe("ColdStorageError");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("failAfterN allows first N operations then fails", async () => {
|
|
77
|
+
const failingLayer = Layer.mergeAll(
|
|
78
|
+
FailingStorage.makeColdStorage({ failAfterN: 2, failLoad: true }),
|
|
79
|
+
HotStorage.InMemory.make()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const results = await Effect.runPromise(
|
|
83
|
+
Effect.gen(function* () {
|
|
84
|
+
const cold = yield* ColdStorageTag;
|
|
85
|
+
|
|
86
|
+
// First 2 operations succeed
|
|
87
|
+
const r1 = yield* Effect.either(cold.load("doc-1"));
|
|
88
|
+
const r2 = yield* Effect.either(cold.load("doc-2"));
|
|
89
|
+
|
|
90
|
+
// Third operation fails
|
|
91
|
+
const r3 = yield* Effect.either(cold.load("doc-3"));
|
|
92
|
+
|
|
93
|
+
return { r1, r2, r3 };
|
|
94
|
+
}).pipe(Effect.provide(failingLayer))
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(results.r1._tag).toBe("Right");
|
|
98
|
+
expect(results.r2._tag).toBe("Right");
|
|
99
|
+
expect(results.r3._tag).toBe("Left");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("HotStorage Failures", () => {
|
|
104
|
+
it("append failure propagates error", async () => {
|
|
105
|
+
const failingLayer = Layer.mergeAll(
|
|
106
|
+
ColdStorage.InMemory.make(),
|
|
107
|
+
FailingStorage.makeHotStorage({ failAppend: true })
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const result = await Effect.runPromise(
|
|
111
|
+
Effect.gen(function* () {
|
|
112
|
+
const hot = yield* HotStorageTag;
|
|
113
|
+
return yield* Effect.either(
|
|
114
|
+
hot.append("test-doc", {
|
|
115
|
+
transaction: { id: "tx-1", ops: [], timestamp: Date.now() },
|
|
116
|
+
version: 1,
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
}).pipe(Effect.provide(failingLayer))
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(result._tag).toBe("Left");
|
|
124
|
+
if (result._tag === "Left") {
|
|
125
|
+
expect(result.left._tag).toBe("HotStorageError");
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("getEntries failure propagates error", async () => {
|
|
130
|
+
const failingLayer = Layer.mergeAll(
|
|
131
|
+
ColdStorage.InMemory.make(),
|
|
132
|
+
FailingStorage.makeHotStorage({ failGetEntries: true })
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = await Effect.runPromise(
|
|
136
|
+
Effect.gen(function* () {
|
|
137
|
+
const hot = yield* HotStorageTag;
|
|
138
|
+
return yield* Effect.either(hot.getEntries("test-doc", 0));
|
|
139
|
+
}).pipe(Effect.provide(failingLayer))
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result._tag).toBe("Left");
|
|
143
|
+
if (result._tag === "Left") {
|
|
144
|
+
expect(result.left._tag).toBe("HotStorageError");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("truncate failure propagates error", async () => {
|
|
149
|
+
const failingLayer = Layer.mergeAll(
|
|
150
|
+
ColdStorage.InMemory.make(),
|
|
151
|
+
FailingStorage.makeHotStorage({ failTruncate: true })
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const result = await Effect.runPromise(
|
|
155
|
+
Effect.gen(function* () {
|
|
156
|
+
const hot = yield* HotStorageTag;
|
|
157
|
+
return yield* Effect.either(hot.truncate("test-doc", 5));
|
|
158
|
+
}).pipe(Effect.provide(failingLayer))
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(result._tag).toBe("Left");
|
|
162
|
+
if (result._tag === "Left") {
|
|
163
|
+
expect(result.left._tag).toBe("HotStorageError");
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("failAfterN allows first N operations then fails", async () => {
|
|
168
|
+
const failingLayer = Layer.mergeAll(
|
|
169
|
+
ColdStorage.InMemory.make(),
|
|
170
|
+
FailingStorage.makeHotStorage({ failAfterN: 3, failAppend: true })
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const results = await Effect.runPromise(
|
|
174
|
+
Effect.gen(function* () {
|
|
175
|
+
const hot = yield* HotStorageTag;
|
|
176
|
+
|
|
177
|
+
const makeEntry = (v: number) => ({
|
|
178
|
+
transaction: { id: `tx-${v}`, ops: [], timestamp: Date.now() },
|
|
179
|
+
version: v,
|
|
180
|
+
timestamp: Date.now(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// First 3 appends succeed
|
|
184
|
+
const r1 = yield* Effect.either(hot.append("doc", makeEntry(1)));
|
|
185
|
+
const r2 = yield* Effect.either(hot.append("doc", makeEntry(2)));
|
|
186
|
+
const r3 = yield* Effect.either(hot.append("doc", makeEntry(3)));
|
|
187
|
+
|
|
188
|
+
// Fourth append fails
|
|
189
|
+
const r4 = yield* Effect.either(hot.append("doc", makeEntry(4)));
|
|
190
|
+
|
|
191
|
+
return { r1, r2, r3, r4 };
|
|
192
|
+
}).pipe(Effect.provide(failingLayer))
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(results.r1._tag).toBe("Right");
|
|
196
|
+
expect(results.r2._tag).toBe("Right");
|
|
197
|
+
expect(results.r3._tag).toBe("Right");
|
|
198
|
+
expect(results.r4._tag).toBe("Left");
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe("Custom Error Messages", () => {
|
|
203
|
+
it("ColdStorage uses custom error message", async () => {
|
|
204
|
+
const failingLayer = Layer.mergeAll(
|
|
205
|
+
FailingStorage.makeColdStorage({
|
|
206
|
+
failLoad: true,
|
|
207
|
+
errorMessage: "Database connection timeout",
|
|
208
|
+
}),
|
|
209
|
+
HotStorage.InMemory.make()
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const result = await Effect.runPromise(
|
|
213
|
+
Effect.gen(function* () {
|
|
214
|
+
const cold = yield* ColdStorageTag;
|
|
215
|
+
return yield* Effect.either(cold.load("test-doc"));
|
|
216
|
+
}).pipe(Effect.provide(failingLayer))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result._tag).toBe("Left");
|
|
220
|
+
if (result._tag === "Left") {
|
|
221
|
+
expect(result.left.cause).toBeInstanceOf(Error);
|
|
222
|
+
expect((result.left.cause as Error).message).toBe(
|
|
223
|
+
"Database connection timeout"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("HotStorage uses custom error message", async () => {
|
|
229
|
+
const failingLayer = Layer.mergeAll(
|
|
230
|
+
ColdStorage.InMemory.make(),
|
|
231
|
+
FailingStorage.makeHotStorage({
|
|
232
|
+
failAppend: true,
|
|
233
|
+
errorMessage: "Redis cluster unavailable",
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const result = await Effect.runPromise(
|
|
238
|
+
Effect.gen(function* () {
|
|
239
|
+
const hot = yield* HotStorageTag;
|
|
240
|
+
return yield* Effect.either(
|
|
241
|
+
hot.append("test-doc", {
|
|
242
|
+
transaction: { id: "tx", ops: [], timestamp: Date.now() },
|
|
243
|
+
version: 1,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
}).pipe(Effect.provide(failingLayer))
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(result._tag).toBe("Left");
|
|
251
|
+
if (result._tag === "Left") {
|
|
252
|
+
expect(result.left.cause).toBeInstanceOf(Error);
|
|
253
|
+
expect((result.left.cause as Error).message).toBe(
|
|
254
|
+
"Redis cluster unavailable"
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
});
|
package/tsdown.config.ts
CHANGED
package/dist/DocumentManager.cjs
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
const require_ColdStorage = require('./ColdStorage.cjs');
|
|
2
|
-
const require_HotStorage = require('./HotStorage.cjs');
|
|
3
|
-
const require_Metrics = require('./Metrics.cjs');
|
|
4
|
-
let effect = require("effect");
|
|
5
|
-
let _voidhash_mimic_server = require("@voidhash/mimic/server");
|
|
6
|
-
|
|
7
|
-
//#region src/DocumentManager.ts
|
|
8
|
-
/**
|
|
9
|
-
* @voidhash/mimic-effect - DocumentManager
|
|
10
|
-
*
|
|
11
|
-
* Internal service for managing document lifecycle, including:
|
|
12
|
-
* - Document creation and restoration
|
|
13
|
-
* - Transaction processing
|
|
14
|
-
* - WAL management
|
|
15
|
-
* - Snapshot scheduling
|
|
16
|
-
* - Idle document GC
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Context tag for DocumentManager service
|
|
20
|
-
*/
|
|
21
|
-
var DocumentManagerTag = class extends effect.Context.Tag("@voidhash/mimic-effect/DocumentManager")() {};
|
|
22
|
-
/**
|
|
23
|
-
* Context tag for DocumentManager configuration
|
|
24
|
-
*/
|
|
25
|
-
var DocumentManagerConfigTag = class extends effect.Context.Tag("@voidhash/mimic-effect/DocumentManagerConfig")() {};
|
|
26
|
-
/**
|
|
27
|
-
* Create the DocumentManager layer.
|
|
28
|
-
* Requires ColdStorage, HotStorage, and DocumentManagerConfig.
|
|
29
|
-
*/
|
|
30
|
-
const layer = effect.Layer.scoped(DocumentManagerTag, effect.Effect.gen(function* () {
|
|
31
|
-
const coldStorage = yield* require_ColdStorage.ColdStorageTag;
|
|
32
|
-
const hotStorage = yield* require_HotStorage.HotStorageTag;
|
|
33
|
-
const config = yield* DocumentManagerConfigTag;
|
|
34
|
-
const store = yield* effect.Ref.make(effect.HashMap.empty());
|
|
35
|
-
const SCHEMA_VERSION = 1;
|
|
36
|
-
/**
|
|
37
|
-
* Compute initial state for a new document
|
|
38
|
-
*/
|
|
39
|
-
const computeInitialState = (documentId) => {
|
|
40
|
-
if (config.initial === void 0) return effect.Effect.succeed(void 0);
|
|
41
|
-
if (typeof config.initial === "function") return config.initial({ documentId });
|
|
42
|
-
return effect.Effect.succeed(config.initial);
|
|
43
|
-
};
|
|
44
|
-
/**
|
|
45
|
-
* Restore a document from storage
|
|
46
|
-
*/
|
|
47
|
-
const restoreDocument = (documentId) => effect.Effect.gen(function* () {
|
|
48
|
-
const storedDoc = yield* effect.Effect.catchAll(coldStorage.load(documentId), () => effect.Effect.succeed(void 0));
|
|
49
|
-
let initialState;
|
|
50
|
-
let initialVersion = 0;
|
|
51
|
-
if (storedDoc) {
|
|
52
|
-
initialState = storedDoc.state;
|
|
53
|
-
initialVersion = storedDoc.version;
|
|
54
|
-
} else initialState = yield* computeInitialState(documentId);
|
|
55
|
-
const pubsub = yield* effect.PubSub.unbounded();
|
|
56
|
-
const lastSnapshotVersion = yield* effect.Ref.make(initialVersion);
|
|
57
|
-
const lastSnapshotTime = yield* effect.Ref.make(Date.now());
|
|
58
|
-
const transactionsSinceSnapshot = yield* effect.Ref.make(0);
|
|
59
|
-
const lastActivityTime = yield* effect.Ref.make(Date.now());
|
|
60
|
-
const document = _voidhash_mimic_server.ServerDocument.make({
|
|
61
|
-
schema: config.schema,
|
|
62
|
-
initialState,
|
|
63
|
-
initialVersion,
|
|
64
|
-
maxTransactionHistory: config.maxTransactionHistory,
|
|
65
|
-
onBroadcast: (message) => {
|
|
66
|
-
effect.Effect.runSync(effect.PubSub.publish(pubsub, {
|
|
67
|
-
type: "transaction",
|
|
68
|
-
transaction: message.transaction,
|
|
69
|
-
version: message.version
|
|
70
|
-
}));
|
|
71
|
-
},
|
|
72
|
-
onRejection: (transactionId, reason) => {
|
|
73
|
-
effect.Effect.runSync(effect.PubSub.publish(pubsub, {
|
|
74
|
-
type: "error",
|
|
75
|
-
transactionId,
|
|
76
|
-
reason
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
const walEntries = yield* effect.Effect.catchAll(hotStorage.getEntries(documentId, initialVersion), () => effect.Effect.succeed([]));
|
|
81
|
-
for (const entry of walEntries) {
|
|
82
|
-
const result = document.submit(entry.transaction);
|
|
83
|
-
if (!result.success) yield* effect.Effect.logWarning("Skipping corrupted WAL entry", {
|
|
84
|
-
documentId,
|
|
85
|
-
version: entry.version,
|
|
86
|
-
reason: result.reason
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const instance = {
|
|
90
|
-
document,
|
|
91
|
-
pubsub,
|
|
92
|
-
lastSnapshotVersion,
|
|
93
|
-
lastSnapshotTime,
|
|
94
|
-
transactionsSinceSnapshot,
|
|
95
|
-
lastActivityTime
|
|
96
|
-
};
|
|
97
|
-
if (storedDoc) yield* effect.Metric.increment(require_Metrics.documentsRestored);
|
|
98
|
-
else yield* effect.Metric.increment(require_Metrics.documentsCreated);
|
|
99
|
-
yield* effect.Metric.incrementBy(require_Metrics.documentsActive, 1);
|
|
100
|
-
return instance;
|
|
101
|
-
});
|
|
102
|
-
/**
|
|
103
|
-
* Get or create a document instance
|
|
104
|
-
*/
|
|
105
|
-
const getOrCreateDocument = (documentId) => effect.Effect.gen(function* () {
|
|
106
|
-
const current = yield* effect.Ref.get(store);
|
|
107
|
-
const existing = effect.HashMap.get(current, documentId);
|
|
108
|
-
if (existing._tag === "Some") {
|
|
109
|
-
yield* effect.Ref.set(existing.value.lastActivityTime, Date.now());
|
|
110
|
-
return existing.value;
|
|
111
|
-
}
|
|
112
|
-
const instance = yield* restoreDocument(documentId);
|
|
113
|
-
yield* effect.Ref.update(store, (map) => effect.HashMap.set(map, documentId, instance));
|
|
114
|
-
return instance;
|
|
115
|
-
});
|
|
116
|
-
/**
|
|
117
|
-
* Save a snapshot to ColdStorage and truncate WAL
|
|
118
|
-
*/
|
|
119
|
-
const saveSnapshot = (documentId, instance) => effect.Effect.gen(function* () {
|
|
120
|
-
const state = instance.document.get();
|
|
121
|
-
const version = instance.document.getVersion();
|
|
122
|
-
if (state === void 0) return;
|
|
123
|
-
const storedDoc = {
|
|
124
|
-
state,
|
|
125
|
-
version,
|
|
126
|
-
schemaVersion: SCHEMA_VERSION,
|
|
127
|
-
savedAt: Date.now()
|
|
128
|
-
};
|
|
129
|
-
const snapshotStartTime = Date.now();
|
|
130
|
-
yield* effect.Effect.catchAll(coldStorage.save(documentId, storedDoc), (e) => effect.Effect.logError("Failed to save snapshot", {
|
|
131
|
-
documentId,
|
|
132
|
-
error: e
|
|
133
|
-
}));
|
|
134
|
-
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
135
|
-
yield* effect.Metric.increment(require_Metrics.storageSnapshots);
|
|
136
|
-
yield* effect.Metric.update(require_Metrics.storageSnapshotLatency, snapshotDuration);
|
|
137
|
-
yield* effect.Effect.catchAll(hotStorage.truncate(documentId, version), (e) => effect.Effect.logError("Failed to truncate WAL", {
|
|
138
|
-
documentId,
|
|
139
|
-
error: e
|
|
140
|
-
}));
|
|
141
|
-
yield* effect.Ref.set(instance.lastSnapshotVersion, version);
|
|
142
|
-
yield* effect.Ref.set(instance.lastSnapshotTime, Date.now());
|
|
143
|
-
yield* effect.Ref.set(instance.transactionsSinceSnapshot, 0);
|
|
144
|
-
});
|
|
145
|
-
/**
|
|
146
|
-
* Check if snapshot should be triggered
|
|
147
|
-
*/
|
|
148
|
-
const checkSnapshotTriggers = (documentId, instance) => effect.Effect.gen(function* () {
|
|
149
|
-
const txCount = yield* effect.Ref.get(instance.transactionsSinceSnapshot);
|
|
150
|
-
const lastTime = yield* effect.Ref.get(instance.lastSnapshotTime);
|
|
151
|
-
const now = Date.now();
|
|
152
|
-
const intervalMs = effect.Duration.toMillis(config.snapshot.interval);
|
|
153
|
-
if (txCount >= config.snapshot.transactionThreshold) {
|
|
154
|
-
yield* saveSnapshot(documentId, instance);
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
if (now - lastTime >= intervalMs) {
|
|
158
|
-
yield* saveSnapshot(documentId, instance);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
yield* effect.Effect.gen(function* () {
|
|
163
|
-
yield* effect.Effect.gen(function* () {
|
|
164
|
-
const current = yield* effect.Ref.get(store);
|
|
165
|
-
const now = Date.now();
|
|
166
|
-
const maxIdleMs = effect.Duration.toMillis(config.maxIdleTime);
|
|
167
|
-
for (const [documentId, instance] of current) if (now - (yield* effect.Ref.get(instance.lastActivityTime)) >= maxIdleMs) {
|
|
168
|
-
yield* saveSnapshot(documentId, instance);
|
|
169
|
-
yield* effect.Ref.update(store, (map) => effect.HashMap.remove(map, documentId));
|
|
170
|
-
yield* effect.Metric.increment(require_Metrics.documentsEvicted);
|
|
171
|
-
yield* effect.Metric.incrementBy(require_Metrics.documentsActive, -1);
|
|
172
|
-
yield* effect.Effect.logInfo("Document evicted due to idle timeout", { documentId });
|
|
173
|
-
}
|
|
174
|
-
}).pipe(effect.Effect.repeat(effect.Schedule.spaced("1 minute")), effect.Effect.fork);
|
|
175
|
-
});
|
|
176
|
-
yield* effect.Effect.addFinalizer(() => effect.Effect.gen(function* () {
|
|
177
|
-
const current = yield* effect.Ref.get(store);
|
|
178
|
-
for (const [documentId, instance] of current) yield* saveSnapshot(documentId, instance);
|
|
179
|
-
yield* effect.Effect.logInfo("DocumentManager shutdown complete");
|
|
180
|
-
}));
|
|
181
|
-
return {
|
|
182
|
-
submit: (documentId, transaction) => effect.Effect.gen(function* () {
|
|
183
|
-
const instance = yield* getOrCreateDocument(documentId);
|
|
184
|
-
const submitStartTime = Date.now();
|
|
185
|
-
const result = instance.document.submit(transaction);
|
|
186
|
-
const latency = Date.now() - submitStartTime;
|
|
187
|
-
yield* effect.Metric.update(require_Metrics.transactionsLatency, latency);
|
|
188
|
-
if (result.success) {
|
|
189
|
-
yield* effect.Metric.increment(require_Metrics.transactionsProcessed);
|
|
190
|
-
const walEntry = {
|
|
191
|
-
transaction,
|
|
192
|
-
version: result.version,
|
|
193
|
-
timestamp: Date.now()
|
|
194
|
-
};
|
|
195
|
-
yield* effect.Effect.catchAll(hotStorage.append(documentId, walEntry), (e) => effect.Effect.logError("Failed to append to WAL", {
|
|
196
|
-
documentId,
|
|
197
|
-
error: e
|
|
198
|
-
}));
|
|
199
|
-
yield* effect.Metric.increment(require_Metrics.storageWalAppends);
|
|
200
|
-
yield* effect.Ref.update(instance.transactionsSinceSnapshot, (n) => n + 1);
|
|
201
|
-
yield* checkSnapshotTriggers(documentId, instance);
|
|
202
|
-
} else yield* effect.Metric.increment(require_Metrics.transactionsRejected);
|
|
203
|
-
return result;
|
|
204
|
-
}),
|
|
205
|
-
getSnapshot: (documentId) => effect.Effect.gen(function* () {
|
|
206
|
-
return (yield* getOrCreateDocument(documentId)).document.getSnapshot();
|
|
207
|
-
}),
|
|
208
|
-
subscribe: (documentId) => effect.Effect.gen(function* () {
|
|
209
|
-
const instance = yield* getOrCreateDocument(documentId);
|
|
210
|
-
return effect.Stream.fromPubSub(instance.pubsub);
|
|
211
|
-
}),
|
|
212
|
-
touch: (documentId) => effect.Effect.gen(function* () {
|
|
213
|
-
const current = yield* effect.Ref.get(store);
|
|
214
|
-
const existing = effect.HashMap.get(current, documentId);
|
|
215
|
-
if (existing._tag === "Some") yield* effect.Ref.set(existing.value.lastActivityTime, Date.now());
|
|
216
|
-
})
|
|
217
|
-
};
|
|
218
|
-
}));
|
|
219
|
-
const DocumentManager = {
|
|
220
|
-
Tag: DocumentManagerTag,
|
|
221
|
-
ConfigTag: DocumentManagerConfigTag,
|
|
222
|
-
layer
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
//#endregion
|
|
226
|
-
exports.DocumentManager = DocumentManager;
|
|
227
|
-
exports.DocumentManagerConfigTag = DocumentManagerConfigTag;
|
|
228
|
-
exports.DocumentManagerTag = DocumentManagerTag;
|
|
229
|
-
exports.layer = layer;
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { ResolvedConfig } from "./Types.cjs";
|
|
2
|
-
import { ServerBroadcast, SnapshotMessage } from "./Protocol.cjs";
|
|
3
|
-
import { ColdStorageTag } from "./ColdStorage.cjs";
|
|
4
|
-
import { HotStorageTag } from "./HotStorage.cjs";
|
|
5
|
-
import { Context, Effect, Layer, Scope, Stream } from "effect";
|
|
6
|
-
import { Transaction } from "@voidhash/mimic";
|
|
7
|
-
|
|
8
|
-
//#region src/DocumentManager.d.ts
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Result of submitting a transaction
|
|
12
|
-
*/
|
|
13
|
-
type SubmitResult = {
|
|
14
|
-
readonly success: true;
|
|
15
|
-
readonly version: number;
|
|
16
|
-
} | {
|
|
17
|
-
readonly success: false;
|
|
18
|
-
readonly reason: string;
|
|
19
|
-
};
|
|
20
|
-
/**
|
|
21
|
-
* Internal service for managing document lifecycle.
|
|
22
|
-
*/
|
|
23
|
-
interface DocumentManager {
|
|
24
|
-
/**
|
|
25
|
-
* Submit a transaction to a document.
|
|
26
|
-
*/
|
|
27
|
-
readonly submit: (documentId: string, transaction: Transaction.Transaction) => Effect.Effect<SubmitResult>;
|
|
28
|
-
/**
|
|
29
|
-
* Get a snapshot of a document.
|
|
30
|
-
*/
|
|
31
|
-
readonly getSnapshot: (documentId: string) => Effect.Effect<SnapshotMessage>;
|
|
32
|
-
/**
|
|
33
|
-
* Subscribe to broadcasts for a document.
|
|
34
|
-
*/
|
|
35
|
-
readonly subscribe: (documentId: string) => Effect.Effect<Stream.Stream<ServerBroadcast>, never, Scope.Scope>;
|
|
36
|
-
/**
|
|
37
|
-
* Touch a document to update its last activity time.
|
|
38
|
-
* Call this on any client activity to prevent idle GC.
|
|
39
|
-
*/
|
|
40
|
-
readonly touch: (documentId: string) => Effect.Effect<void>;
|
|
41
|
-
}
|
|
42
|
-
declare const DocumentManagerTag_base: Context.TagClass<DocumentManagerTag, "@voidhash/mimic-effect/DocumentManager", DocumentManager>;
|
|
43
|
-
/**
|
|
44
|
-
* Context tag for DocumentManager service
|
|
45
|
-
*/
|
|
46
|
-
declare class DocumentManagerTag extends DocumentManagerTag_base {}
|
|
47
|
-
declare const DocumentManagerConfigTag_base: Context.TagClass<DocumentManagerConfigTag, "@voidhash/mimic-effect/DocumentManagerConfig", ResolvedConfig<Primitive.AnyPrimitive>>;
|
|
48
|
-
/**
|
|
49
|
-
* Context tag for DocumentManager configuration
|
|
50
|
-
*/
|
|
51
|
-
declare class DocumentManagerConfigTag extends DocumentManagerConfigTag_base {}
|
|
52
|
-
declare const DocumentManager: {
|
|
53
|
-
Tag: typeof DocumentManagerTag;
|
|
54
|
-
ConfigTag: typeof DocumentManagerConfigTag;
|
|
55
|
-
layer: Layer.Layer<DocumentManagerTag, never, ColdStorageTag | HotStorageTag | DocumentManagerConfigTag>;
|
|
56
|
-
};
|
|
57
|
-
//#endregion
|
|
58
|
-
export { DocumentManager, DocumentManagerConfigTag, DocumentManagerTag, SubmitResult };
|
|
59
|
-
//# sourceMappingURL=DocumentManager.d.cts.map
|