@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +136 -90
- package/README.md +385 -0
- package/dist/ColdStorage.cjs +60 -0
- package/dist/ColdStorage.d.cts +53 -0
- package/dist/ColdStorage.d.cts.map +1 -0
- package/dist/ColdStorage.d.mts +53 -0
- package/dist/ColdStorage.d.mts.map +1 -0
- package/dist/ColdStorage.mjs +60 -0
- package/dist/ColdStorage.mjs.map +1 -0
- package/dist/DocumentManager.cjs +263 -82
- package/dist/DocumentManager.d.cts +44 -22
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts +44 -22
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +259 -67
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/Errors.cjs +54 -0
- package/dist/Errors.d.cts +96 -0
- package/dist/Errors.d.cts.map +1 -0
- package/dist/Errors.d.mts +96 -0
- package/dist/Errors.d.mts.map +1 -0
- package/dist/Errors.mjs +48 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/HotStorage.cjs +100 -0
- package/dist/HotStorage.d.cts +70 -0
- package/dist/HotStorage.d.cts.map +1 -0
- package/dist/HotStorage.d.mts +70 -0
- package/dist/HotStorage.d.mts.map +1 -0
- package/dist/HotStorage.mjs +100 -0
- package/dist/HotStorage.mjs.map +1 -0
- package/dist/Metrics.cjs +143 -0
- package/dist/Metrics.d.cts +31 -0
- package/dist/Metrics.d.cts.map +1 -0
- package/dist/Metrics.d.mts +31 -0
- package/dist/Metrics.d.mts.map +1 -0
- package/dist/Metrics.mjs +126 -0
- package/dist/Metrics.mjs.map +1 -0
- package/dist/MimicAuthService.cjs +61 -45
- package/dist/MimicAuthService.d.cts +61 -48
- package/dist/MimicAuthService.d.cts.map +1 -1
- package/dist/MimicAuthService.d.mts +61 -48
- package/dist/MimicAuthService.d.mts.map +1 -1
- package/dist/MimicAuthService.mjs +60 -36
- package/dist/MimicAuthService.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +521 -0
- package/dist/MimicClusterServerEngine.d.cts +17 -0
- package/dist/MimicClusterServerEngine.d.cts.map +1 -0
- package/dist/MimicClusterServerEngine.d.mts +17 -0
- package/dist/MimicClusterServerEngine.d.mts.map +1 -0
- package/dist/MimicClusterServerEngine.mjs +523 -0
- package/dist/MimicClusterServerEngine.mjs.map +1 -0
- package/dist/MimicServer.cjs +205 -96
- package/dist/MimicServer.d.cts +9 -110
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts +9 -110
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +206 -90
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +97 -0
- package/dist/MimicServerEngine.d.cts +78 -0
- package/dist/MimicServerEngine.d.cts.map +1 -0
- package/dist/MimicServerEngine.d.mts +78 -0
- package/dist/MimicServerEngine.d.mts.map +1 -0
- package/dist/MimicServerEngine.mjs +97 -0
- package/dist/MimicServerEngine.mjs.map +1 -0
- package/dist/PresenceManager.cjs +75 -91
- package/dist/PresenceManager.d.cts +17 -66
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts +17 -66
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +74 -78
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.cjs +146 -0
- package/dist/Protocol.d.cts +203 -0
- package/dist/Protocol.d.cts.map +1 -0
- package/dist/Protocol.d.mts +203 -0
- package/dist/Protocol.d.mts.map +1 -0
- package/dist/Protocol.mjs +132 -0
- package/dist/Protocol.mjs.map +1 -0
- package/dist/Types.d.cts +172 -0
- package/dist/Types.d.cts.map +1 -0
- package/dist/Types.d.mts +172 -0
- package/dist/Types.d.mts.map +1 -0
- package/dist/_virtual/rolldown_runtime.cjs +1 -25
- package/dist/_virtual/rolldown_runtime.mjs +4 -1
- package/dist/index.cjs +37 -75
- package/dist/index.d.cts +13 -12
- package/dist/index.d.mts +13 -12
- package/dist/index.mjs +12 -12
- 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 +135 -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 +136 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +585 -0
- package/dist/testing/HotStorageTestSuite.d.cts +40 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +40 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +585 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +35 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +349 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +114 -0
- package/dist/testing/assertions.mjs +109 -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 +18 -3
- package/src/ColdStorage.ts +136 -0
- package/src/DocumentManager.ts +550 -190
- package/src/Errors.ts +114 -0
- package/src/HotStorage.ts +239 -0
- package/src/Metrics.ts +187 -0
- package/src/MimicAuthService.ts +126 -64
- package/src/MimicClusterServerEngine.ts +946 -0
- package/src/MimicServer.ts +448 -195
- package/src/MimicServerEngine.ts +276 -0
- package/src/PresenceManager.ts +169 -240
- package/src/Protocol.ts +350 -0
- package/src/Types.ts +231 -0
- package/src/index.ts +57 -23
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +286 -0
- package/src/testing/HotStorageTestSuite.ts +762 -0
- package/src/testing/StorageIntegrationTestSuite.ts +504 -0
- package/src/testing/assertions.ts +181 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +24 -0
- package/tests/DocumentManager.test.ts +158 -287
- package/tests/HotStorage.test.ts +24 -0
- package/tests/MimicAuthService.test.ts +102 -134
- package/tests/MimicClusterServerEngine.test.ts +587 -0
- package/tests/MimicServer.test.ts +90 -226
- package/tests/MimicServerEngine.test.ts +521 -0
- package/tests/PresenceManager.test.ts +22 -63
- package/tests/Protocol.test.ts +190 -0
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsconfig.json +1 -1
- package/tsdown.config.ts +1 -1
- package/dist/DocumentProtocol.cjs +0 -94
- package/dist/DocumentProtocol.d.cts +0 -113
- package/dist/DocumentProtocol.d.cts.map +0 -1
- package/dist/DocumentProtocol.d.mts +0 -113
- package/dist/DocumentProtocol.d.mts.map +0 -1
- package/dist/DocumentProtocol.mjs +0 -89
- package/dist/DocumentProtocol.mjs.map +0 -1
- package/dist/MimicConfig.cjs +0 -60
- package/dist/MimicConfig.d.cts +0 -141
- package/dist/MimicConfig.d.cts.map +0 -1
- package/dist/MimicConfig.d.mts +0 -141
- package/dist/MimicConfig.d.mts.map +0 -1
- package/dist/MimicConfig.mjs +0 -50
- package/dist/MimicConfig.mjs.map +0 -1
- package/dist/MimicDataStorage.cjs +0 -83
- package/dist/MimicDataStorage.d.cts +0 -113
- package/dist/MimicDataStorage.d.cts.map +0 -1
- package/dist/MimicDataStorage.d.mts +0 -113
- package/dist/MimicDataStorage.d.mts.map +0 -1
- package/dist/MimicDataStorage.mjs +0 -74
- package/dist/MimicDataStorage.mjs.map +0 -1
- package/dist/WebSocketHandler.cjs +0 -365
- package/dist/WebSocketHandler.d.cts +0 -34
- package/dist/WebSocketHandler.d.cts.map +0 -1
- package/dist/WebSocketHandler.d.mts +0 -34
- package/dist/WebSocketHandler.d.mts.map +0 -1
- package/dist/WebSocketHandler.mjs +0 -355
- package/dist/WebSocketHandler.mjs.map +0 -1
- package/dist/auth/NoAuth.cjs +0 -43
- package/dist/auth/NoAuth.d.cts +0 -22
- package/dist/auth/NoAuth.d.cts.map +0 -1
- package/dist/auth/NoAuth.d.mts +0 -22
- package/dist/auth/NoAuth.d.mts.map +0 -1
- package/dist/auth/NoAuth.mjs +0 -36
- package/dist/auth/NoAuth.mjs.map +0 -1
- package/dist/errors.cjs +0 -74
- package/dist/errors.d.cts +0 -89
- package/dist/errors.d.cts.map +0 -1
- package/dist/errors.d.mts +0 -89
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs +0 -67
- package/dist/errors.mjs.map +0 -1
- package/dist/storage/InMemoryDataStorage.cjs +0 -57
- package/dist/storage/InMemoryDataStorage.d.cts +0 -19
- package/dist/storage/InMemoryDataStorage.d.cts.map +0 -1
- package/dist/storage/InMemoryDataStorage.d.mts +0 -19
- package/dist/storage/InMemoryDataStorage.d.mts.map +0 -1
- package/dist/storage/InMemoryDataStorage.mjs +0 -48
- package/dist/storage/InMemoryDataStorage.mjs.map +0 -1
- package/src/DocumentProtocol.ts +0 -112
- package/src/MimicConfig.ts +0 -211
- package/src/MimicDataStorage.ts +0 -157
- package/src/WebSocketHandler.ts +0 -735
- package/src/auth/NoAuth.ts +0 -46
- package/src/errors.ts +0 -113
- package/src/storage/InMemoryDataStorage.ts +0 -66
- package/tests/DocumentProtocol.test.ts +0 -113
- package/tests/InMemoryDataStorage.test.ts +0 -190
- package/tests/MimicConfig.test.ts +0 -290
- package/tests/MimicDataStorage.test.ts +0 -190
- package/tests/NoAuth.test.ts +0 -94
- package/tests/WebSocketHandler.test.ts +0 -321
- package/tests/errors.test.ts +0 -77
package/src/Errors.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @voidhash/mimic-effect - Error Types
|
|
3
|
+
*
|
|
4
|
+
* All error types used throughout the mimic-effect package.
|
|
5
|
+
*/
|
|
6
|
+
import { Data } from "effect";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Storage Errors
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Error when ColdStorage (snapshot storage) operations fail
|
|
14
|
+
*/
|
|
15
|
+
export class ColdStorageError extends Data.TaggedError("ColdStorageError")<{
|
|
16
|
+
readonly documentId: string;
|
|
17
|
+
readonly operation: "load" | "save" | "delete";
|
|
18
|
+
readonly cause: unknown;
|
|
19
|
+
}> {}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Error when HotStorage (WAL storage) operations fail
|
|
23
|
+
*/
|
|
24
|
+
export class HotStorageError extends Data.TaggedError("HotStorageError")<{
|
|
25
|
+
readonly documentId: string;
|
|
26
|
+
readonly operation: "append" | "getEntries" | "truncate" | "appendWithCheck";
|
|
27
|
+
readonly cause: unknown;
|
|
28
|
+
}> {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Error when WAL append detects a version gap.
|
|
32
|
+
* This indicates either:
|
|
33
|
+
* - A bug in the application (skipped a version)
|
|
34
|
+
* - Concurrent writes to the same document
|
|
35
|
+
* - Data corruption in WAL storage
|
|
36
|
+
*/
|
|
37
|
+
export class WalVersionGapError extends Data.TaggedError("WalVersionGapError")<{
|
|
38
|
+
readonly documentId: string;
|
|
39
|
+
readonly expectedVersion: number;
|
|
40
|
+
readonly actualPreviousVersion: number | undefined;
|
|
41
|
+
}> {}
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Auth Errors
|
|
45
|
+
// =============================================================================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error when authentication fails (invalid token, expired, etc.)
|
|
49
|
+
*/
|
|
50
|
+
export class AuthenticationError extends Data.TaggedError(
|
|
51
|
+
"AuthenticationError"
|
|
52
|
+
)<{
|
|
53
|
+
readonly reason: string;
|
|
54
|
+
}> {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Error when authorization fails (user doesn't have required permission)
|
|
58
|
+
*/
|
|
59
|
+
export class AuthorizationError extends Data.TaggedError("AuthorizationError")<{
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
readonly required: "read" | "write";
|
|
62
|
+
readonly actual: "read" | "write";
|
|
63
|
+
}> {}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Connection Errors
|
|
67
|
+
// =============================================================================
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Error when document ID is missing from WebSocket request path
|
|
71
|
+
*/
|
|
72
|
+
export class MissingDocumentIdError extends Data.TaggedError(
|
|
73
|
+
"MissingDocumentIdError"
|
|
74
|
+
)<{
|
|
75
|
+
readonly path: string;
|
|
76
|
+
}> {}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Error when WebSocket message cannot be parsed
|
|
80
|
+
*/
|
|
81
|
+
export class MessageParseError extends Data.TaggedError("MessageParseError")<{
|
|
82
|
+
readonly cause: unknown;
|
|
83
|
+
}> {}
|
|
84
|
+
|
|
85
|
+
// =============================================================================
|
|
86
|
+
// Transaction Errors
|
|
87
|
+
// =============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Error when a transaction is rejected by the document
|
|
91
|
+
*/
|
|
92
|
+
export class TransactionRejectedError extends Data.TaggedError(
|
|
93
|
+
"TransactionRejectedError"
|
|
94
|
+
)<{
|
|
95
|
+
readonly transactionId: string;
|
|
96
|
+
readonly reason: string;
|
|
97
|
+
}> {}
|
|
98
|
+
|
|
99
|
+
// =============================================================================
|
|
100
|
+
// Union Type
|
|
101
|
+
// =============================================================================
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Union of all mimic-effect errors
|
|
105
|
+
*/
|
|
106
|
+
export type MimicError =
|
|
107
|
+
| ColdStorageError
|
|
108
|
+
| HotStorageError
|
|
109
|
+
| WalVersionGapError
|
|
110
|
+
| AuthenticationError
|
|
111
|
+
| AuthorizationError
|
|
112
|
+
| MissingDocumentIdError
|
|
113
|
+
| MessageParseError
|
|
114
|
+
| TransactionRejectedError;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @voidhash/mimic-effect - HotStorage
|
|
3
|
+
*
|
|
4
|
+
* Interface and implementations for Write-Ahead Log (WAL) storage.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Effect, HashMap, Layer, Ref } from "effect";
|
|
7
|
+
import type { WalEntry } from "./Types";
|
|
8
|
+
import { HotStorageError, WalVersionGapError } from "./Errors";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// HotStorage Interface
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* HotStorage interface for storing Write-Ahead Log entries.
|
|
16
|
+
*
|
|
17
|
+
* This is the "hot" tier of the two-tier storage system.
|
|
18
|
+
* It stores every transaction as a WAL entry for durability between snapshots.
|
|
19
|
+
* WAL entries are small (just the transaction) and writes are append-only.
|
|
20
|
+
*/
|
|
21
|
+
export interface HotStorage {
|
|
22
|
+
/**
|
|
23
|
+
* Append a WAL entry for a document.
|
|
24
|
+
*/
|
|
25
|
+
readonly append: (
|
|
26
|
+
documentId: string,
|
|
27
|
+
entry: WalEntry
|
|
28
|
+
) => Effect.Effect<void, HotStorageError>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Append a WAL entry with version gap checking.
|
|
32
|
+
*
|
|
33
|
+
* This is an atomic operation that:
|
|
34
|
+
* 1. Verifies the previous entry has version = expectedVersion - 1
|
|
35
|
+
* (or this is the first entry if expectedVersion === 1)
|
|
36
|
+
* 2. Appends the entry if check passes
|
|
37
|
+
*
|
|
38
|
+
* Use this for two-phase commit to guarantee WAL ordering at write time.
|
|
39
|
+
*
|
|
40
|
+
* @param documentId - Document ID
|
|
41
|
+
* @param entry - WAL entry to append
|
|
42
|
+
* @param expectedVersion - The version this entry should have (entry.version)
|
|
43
|
+
* @returns Effect that fails with WalVersionGapError if gap detected
|
|
44
|
+
*/
|
|
45
|
+
readonly appendWithCheck: (
|
|
46
|
+
documentId: string,
|
|
47
|
+
entry: WalEntry,
|
|
48
|
+
expectedVersion: number
|
|
49
|
+
) => Effect.Effect<void, HotStorageError | WalVersionGapError>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all WAL entries for a document since a given version.
|
|
53
|
+
* Returns entries with version > sinceVersion, ordered by version.
|
|
54
|
+
*/
|
|
55
|
+
readonly getEntries: (
|
|
56
|
+
documentId: string,
|
|
57
|
+
sinceVersion: number
|
|
58
|
+
) => Effect.Effect<WalEntry[], HotStorageError>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Truncate WAL entries up to (and including) a given version.
|
|
62
|
+
* Called after a snapshot is saved to remove entries that are now in the snapshot.
|
|
63
|
+
*/
|
|
64
|
+
readonly truncate: (
|
|
65
|
+
documentId: string,
|
|
66
|
+
upToVersion: number
|
|
67
|
+
) => Effect.Effect<void, HotStorageError>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Context Tag
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Context tag for HotStorage service
|
|
76
|
+
*/
|
|
77
|
+
export class HotStorageTag extends Context.Tag("@voidhash/mimic-effect/HotStorage")<
|
|
78
|
+
HotStorageTag,
|
|
79
|
+
HotStorage
|
|
80
|
+
>() {}
|
|
81
|
+
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// Factory
|
|
84
|
+
// =============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create a HotStorage layer from an Effect that produces a HotStorage service.
|
|
88
|
+
*
|
|
89
|
+
* This allows you to access other Effect services when implementing custom storage.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const Hot = HotStorage.make(
|
|
94
|
+
* Effect.gen(function*() {
|
|
95
|
+
* const redis = yield* RedisService
|
|
96
|
+
*
|
|
97
|
+
* return {
|
|
98
|
+
* append: (documentId, entry) =>
|
|
99
|
+
* redis.rpush(`wal:${documentId}`, JSON.stringify(entry)),
|
|
100
|
+
* getEntries: (documentId, sinceVersion) =>
|
|
101
|
+
* redis.lrange(`wal:${documentId}`, 0, -1).pipe(
|
|
102
|
+
* Effect.map(entries =>
|
|
103
|
+
* entries
|
|
104
|
+
* .map(e => JSON.parse(e))
|
|
105
|
+
* .filter(e => e.version > sinceVersion)
|
|
106
|
+
* .sort((a, b) => a.version - b.version)
|
|
107
|
+
* )
|
|
108
|
+
* ),
|
|
109
|
+
* truncate: (documentId, upToVersion) =>
|
|
110
|
+
* // Implementation depends on Redis data structure
|
|
111
|
+
* Effect.void,
|
|
112
|
+
* }
|
|
113
|
+
* })
|
|
114
|
+
* )
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export const make = <E, R>(
|
|
118
|
+
effect: Effect.Effect<HotStorage, E, R>
|
|
119
|
+
): Layer.Layer<HotStorageTag, E, R> =>
|
|
120
|
+
Layer.effect(HotStorageTag, effect);
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// InMemory Implementation
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* In-memory HotStorage implementation.
|
|
128
|
+
*
|
|
129
|
+
* Useful for testing and development. Not suitable for production
|
|
130
|
+
* as data is lost when the process restarts.
|
|
131
|
+
*/
|
|
132
|
+
export namespace InMemory {
|
|
133
|
+
/**
|
|
134
|
+
* Create an in-memory HotStorage layer.
|
|
135
|
+
*/
|
|
136
|
+
export const make = (): Layer.Layer<HotStorageTag> =>
|
|
137
|
+
Layer.effect(
|
|
138
|
+
HotStorageTag,
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
const store = yield* Ref.make(HashMap.empty<string, WalEntry[]>());
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
append: (documentId, entry) =>
|
|
144
|
+
Ref.update(store, (map) => {
|
|
145
|
+
const existing = HashMap.get(map, documentId);
|
|
146
|
+
const entries =
|
|
147
|
+
existing._tag === "Some" ? existing.value : [];
|
|
148
|
+
return HashMap.set(map, documentId, [...entries, entry]);
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
appendWithCheck: (documentId, entry, expectedVersion) =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
type CheckResult =
|
|
154
|
+
| { type: "ok" }
|
|
155
|
+
| { type: "gap"; lastVersion: number | undefined };
|
|
156
|
+
|
|
157
|
+
// Use Ref.modify for atomic check + update
|
|
158
|
+
const result: CheckResult = yield* Ref.modify(store, (map): [CheckResult, HashMap.HashMap<string, WalEntry[]>] => {
|
|
159
|
+
const existing = HashMap.get(map, documentId);
|
|
160
|
+
const entries = existing._tag === "Some" ? existing.value : [];
|
|
161
|
+
|
|
162
|
+
// Find the highest version in existing entries
|
|
163
|
+
const lastVersion = entries.length > 0
|
|
164
|
+
? Math.max(...entries.map((e) => e.version))
|
|
165
|
+
: 0;
|
|
166
|
+
|
|
167
|
+
// Gap check
|
|
168
|
+
if (expectedVersion === 1) {
|
|
169
|
+
// First entry: should have no entries with version >= 1
|
|
170
|
+
if (lastVersion >= 1) {
|
|
171
|
+
return [
|
|
172
|
+
{ type: "gap", lastVersion },
|
|
173
|
+
map,
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Not first: last entry should have version = expectedVersion - 1
|
|
178
|
+
if (lastVersion !== expectedVersion - 1) {
|
|
179
|
+
return [
|
|
180
|
+
{ type: "gap", lastVersion: lastVersion > 0 ? lastVersion : undefined },
|
|
181
|
+
map,
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// No gap: append and return success
|
|
187
|
+
return [
|
|
188
|
+
{ type: "ok" },
|
|
189
|
+
HashMap.set(map, documentId, [...entries, entry]),
|
|
190
|
+
];
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (result.type === "gap") {
|
|
194
|
+
return yield* Effect.fail(
|
|
195
|
+
new WalVersionGapError({
|
|
196
|
+
documentId,
|
|
197
|
+
expectedVersion,
|
|
198
|
+
actualPreviousVersion: result.lastVersion,
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}),
|
|
203
|
+
|
|
204
|
+
getEntries: (documentId, sinceVersion) =>
|
|
205
|
+
Effect.gen(function* () {
|
|
206
|
+
const current = yield* Ref.get(store);
|
|
207
|
+
const existing = HashMap.get(current, documentId);
|
|
208
|
+
const entries =
|
|
209
|
+
existing._tag === "Some" ? existing.value : [];
|
|
210
|
+
return entries
|
|
211
|
+
.filter((e) => e.version > sinceVersion)
|
|
212
|
+
.sort((a, b) => a.version - b.version);
|
|
213
|
+
}),
|
|
214
|
+
|
|
215
|
+
truncate: (documentId, upToVersion) =>
|
|
216
|
+
Ref.update(store, (map) => {
|
|
217
|
+
const existing = HashMap.get(map, documentId);
|
|
218
|
+
if (existing._tag === "None") {
|
|
219
|
+
return map;
|
|
220
|
+
}
|
|
221
|
+
const filtered = existing.value.filter(
|
|
222
|
+
(e) => e.version > upToVersion
|
|
223
|
+
);
|
|
224
|
+
return HashMap.set(map, documentId, filtered);
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// Re-export namespace
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
export const HotStorage = {
|
|
236
|
+
Tag: HotStorageTag,
|
|
237
|
+
make,
|
|
238
|
+
InMemory,
|
|
239
|
+
};
|
package/src/Metrics.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @voidhash/mimic-effect - Metrics
|
|
3
|
+
*
|
|
4
|
+
* Observability metrics using Effect's Metric API.
|
|
5
|
+
*/
|
|
6
|
+
import { Metric, MetricBoundaries } from "effect";
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Connection Metrics
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Current active WebSocket connections
|
|
14
|
+
*/
|
|
15
|
+
export const connectionsActive = Metric.gauge("mimic.connections.active");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Total connections over lifetime
|
|
19
|
+
*/
|
|
20
|
+
export const connectionsTotal = Metric.counter("mimic.connections.total");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Connection duration histogram (milliseconds)
|
|
24
|
+
*/
|
|
25
|
+
export const connectionsDuration = Metric.histogram(
|
|
26
|
+
"mimic.connections.duration_ms",
|
|
27
|
+
MetricBoundaries.exponential({
|
|
28
|
+
start: 100,
|
|
29
|
+
factor: 2,
|
|
30
|
+
count: 15, // Up to ~3.2 million ms (~53 minutes)
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Connection errors (auth failures, etc.)
|
|
36
|
+
*/
|
|
37
|
+
export const connectionsErrors = Metric.counter("mimic.connections.errors");
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// Document Metrics
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Documents currently in memory
|
|
45
|
+
*/
|
|
46
|
+
export const documentsActive = Metric.gauge("mimic.documents.active");
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* New documents created
|
|
50
|
+
*/
|
|
51
|
+
export const documentsCreated = Metric.counter("mimic.documents.created");
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Documents restored from storage
|
|
55
|
+
*/
|
|
56
|
+
export const documentsRestored = Metric.counter("mimic.documents.restored");
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Documents garbage collected (evicted)
|
|
60
|
+
*/
|
|
61
|
+
export const documentsEvicted = Metric.counter("mimic.documents.evicted");
|
|
62
|
+
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// Transaction Metrics
|
|
65
|
+
// =============================================================================
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Successfully processed transactions
|
|
69
|
+
*/
|
|
70
|
+
export const transactionsProcessed = Metric.counter(
|
|
71
|
+
"mimic.transactions.processed"
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Rejected transactions
|
|
76
|
+
*/
|
|
77
|
+
export const transactionsRejected = Metric.counter(
|
|
78
|
+
"mimic.transactions.rejected"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Transaction processing latency histogram (milliseconds)
|
|
83
|
+
*/
|
|
84
|
+
export const transactionsLatency = Metric.histogram(
|
|
85
|
+
"mimic.transactions.latency_ms",
|
|
86
|
+
MetricBoundaries.exponential({
|
|
87
|
+
start: 0.1,
|
|
88
|
+
factor: 2,
|
|
89
|
+
count: 15, // Up to ~1638 ms
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// =============================================================================
|
|
94
|
+
// Storage Metrics
|
|
95
|
+
// =============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Snapshots saved to ColdStorage
|
|
99
|
+
*/
|
|
100
|
+
export const storageSnapshots = Metric.counter("mimic.storage.snapshots");
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Snapshot save duration histogram (milliseconds)
|
|
104
|
+
*/
|
|
105
|
+
export const storageSnapshotLatency = Metric.histogram(
|
|
106
|
+
"mimic.storage.snapshot_latency_ms",
|
|
107
|
+
MetricBoundaries.exponential({
|
|
108
|
+
start: 1,
|
|
109
|
+
factor: 2,
|
|
110
|
+
count: 12, // Up to ~4 seconds
|
|
111
|
+
})
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* WAL entries written to HotStorage
|
|
116
|
+
*/
|
|
117
|
+
export const storageWalAppends = Metric.counter("mimic.storage.wal_appends");
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Version gaps detected during WAL replay
|
|
121
|
+
*/
|
|
122
|
+
export const storageVersionGaps = Metric.counter("mimic.storage.version_gaps");
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Failed WAL appends causing transaction rollback
|
|
126
|
+
*/
|
|
127
|
+
export const walAppendFailures = Metric.counter("mimic.storage.wal_append_failures");
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* ColdStorage load failures during document restore
|
|
131
|
+
*/
|
|
132
|
+
export const coldStorageLoadFailures = Metric.counter("mimic.storage.cold_load_failures");
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* HotStorage getEntries failures during document restore
|
|
136
|
+
*/
|
|
137
|
+
export const hotStorageLoadFailures = Metric.counter("mimic.storage.hot_load_failures");
|
|
138
|
+
|
|
139
|
+
// =============================================================================
|
|
140
|
+
// Presence Metrics
|
|
141
|
+
// =============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Presence set operations
|
|
145
|
+
*/
|
|
146
|
+
export const presenceUpdates = Metric.counter("mimic.presence.updates");
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Active presence entries
|
|
150
|
+
*/
|
|
151
|
+
export const presenceActive = Metric.gauge("mimic.presence.active");
|
|
152
|
+
|
|
153
|
+
// =============================================================================
|
|
154
|
+
// Export namespace
|
|
155
|
+
// =============================================================================
|
|
156
|
+
|
|
157
|
+
export const MimicMetrics = {
|
|
158
|
+
// Connection
|
|
159
|
+
connectionsActive,
|
|
160
|
+
connectionsTotal,
|
|
161
|
+
connectionsDuration,
|
|
162
|
+
connectionsErrors,
|
|
163
|
+
|
|
164
|
+
// Document
|
|
165
|
+
documentsActive,
|
|
166
|
+
documentsCreated,
|
|
167
|
+
documentsRestored,
|
|
168
|
+
documentsEvicted,
|
|
169
|
+
|
|
170
|
+
// Transaction
|
|
171
|
+
transactionsProcessed,
|
|
172
|
+
transactionsRejected,
|
|
173
|
+
transactionsLatency,
|
|
174
|
+
|
|
175
|
+
// Storage
|
|
176
|
+
storageSnapshots,
|
|
177
|
+
storageSnapshotLatency,
|
|
178
|
+
storageWalAppends,
|
|
179
|
+
storageVersionGaps,
|
|
180
|
+
walAppendFailures,
|
|
181
|
+
coldStorageLoadFailures,
|
|
182
|
+
hotStorageLoadFailures,
|
|
183
|
+
|
|
184
|
+
// Presence
|
|
185
|
+
presenceUpdates,
|
|
186
|
+
presenceActive,
|
|
187
|
+
};
|