@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/DocumentManager.ts
CHANGED
|
@@ -1,254 +1,614 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @
|
|
3
|
-
*
|
|
2
|
+
* @voidhash/mimic-effect - DocumentManager
|
|
3
|
+
*
|
|
4
|
+
* Internal service for managing document lifecycle, including:
|
|
5
|
+
* - Document creation and restoration
|
|
6
|
+
* - Transaction processing
|
|
7
|
+
* - WAL management
|
|
8
|
+
* - Snapshot scheduling
|
|
9
|
+
* - Idle document GC
|
|
4
10
|
*/
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
Context,
|
|
13
|
+
Duration,
|
|
14
|
+
Effect,
|
|
15
|
+
HashMap,
|
|
16
|
+
Layer,
|
|
17
|
+
Metric,
|
|
18
|
+
PubSub,
|
|
19
|
+
Ref,
|
|
20
|
+
Schedule,
|
|
21
|
+
Scope,
|
|
22
|
+
Stream,
|
|
23
|
+
} from "effect";
|
|
24
|
+
import { Document, Primitive, Transaction } from "@voidhash/mimic";
|
|
14
25
|
import { ServerDocument } from "@voidhash/mimic/server";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
import type {
|
|
27
|
+
ResolvedConfig,
|
|
28
|
+
StoredDocument,
|
|
29
|
+
WalEntry,
|
|
30
|
+
} from "./Types";
|
|
31
|
+
import type { SnapshotMessage, ServerBroadcast } from "./Protocol";
|
|
32
|
+
import { ColdStorageTag } from "./ColdStorage";
|
|
33
|
+
import { HotStorageTag } from "./HotStorage";
|
|
34
|
+
import { ColdStorageError, HotStorageError } from "./Errors";
|
|
35
|
+
import * as Metrics from "./Metrics";
|
|
20
36
|
|
|
21
37
|
// =============================================================================
|
|
22
|
-
//
|
|
38
|
+
// Submit Result Types
|
|
23
39
|
// =============================================================================
|
|
24
40
|
|
|
25
41
|
/**
|
|
26
|
-
*
|
|
42
|
+
* Result of submitting a transaction
|
|
27
43
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readonly
|
|
31
|
-
/** PubSub for broadcasting messages to subscribers */
|
|
32
|
-
readonly pubsub: PubSub.PubSub<Protocol.ServerBroadcast>;
|
|
33
|
-
/** Reference count for cleanup */
|
|
34
|
-
readonly refCount: Ref.Ref<number>;
|
|
35
|
-
}
|
|
44
|
+
export type SubmitResult =
|
|
45
|
+
| { readonly success: true; readonly version: number }
|
|
46
|
+
| { readonly success: false; readonly reason: string };
|
|
36
47
|
|
|
37
48
|
// =============================================================================
|
|
38
|
-
//
|
|
49
|
+
// DocumentManager Interface
|
|
39
50
|
// =============================================================================
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
|
-
*
|
|
53
|
+
* Error type for DocumentManager operations
|
|
54
|
+
*/
|
|
55
|
+
export type DocumentManagerError = ColdStorageError | HotStorageError;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Internal service for managing document lifecycle.
|
|
43
59
|
*/
|
|
44
60
|
export interface DocumentManager {
|
|
45
61
|
/**
|
|
46
62
|
* Submit a transaction to a document.
|
|
63
|
+
* May fail with ColdStorageError or HotStorageError if storage is unavailable.
|
|
47
64
|
*/
|
|
48
65
|
readonly submit: (
|
|
49
66
|
documentId: string,
|
|
50
67
|
transaction: Transaction.Transaction
|
|
51
|
-
) => Effect.Effect<
|
|
68
|
+
) => Effect.Effect<SubmitResult, DocumentManagerError>;
|
|
52
69
|
|
|
53
70
|
/**
|
|
54
71
|
* Get a snapshot of a document.
|
|
72
|
+
* May fail with ColdStorageError or HotStorageError if storage is unavailable.
|
|
55
73
|
*/
|
|
56
|
-
readonly getSnapshot: (
|
|
57
|
-
documentId: string
|
|
58
|
-
) => Effect.Effect<Protocol.SnapshotMessage>;
|
|
74
|
+
readonly getSnapshot: (documentId: string) => Effect.Effect<SnapshotMessage, DocumentManagerError>;
|
|
59
75
|
|
|
60
76
|
/**
|
|
61
77
|
* Subscribe to broadcasts for a document.
|
|
62
|
-
*
|
|
78
|
+
* May fail with ColdStorageError or HotStorageError if storage is unavailable.
|
|
63
79
|
*/
|
|
64
80
|
readonly subscribe: (
|
|
65
81
|
documentId: string
|
|
66
|
-
) => Effect.Effect<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
82
|
+
) => Effect.Effect<Stream.Stream<ServerBroadcast>, DocumentManagerError, Scope.Scope>;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Touch a document to update its last activity time.
|
|
86
|
+
* Call this on any client activity to prevent idle GC.
|
|
87
|
+
*/
|
|
88
|
+
readonly touch: (documentId: string) => Effect.Effect<void>;
|
|
71
89
|
}
|
|
72
90
|
|
|
91
|
+
// =============================================================================
|
|
92
|
+
// Context Tag
|
|
93
|
+
// =============================================================================
|
|
94
|
+
|
|
73
95
|
/**
|
|
74
|
-
* Context tag for DocumentManager
|
|
96
|
+
* Context tag for DocumentManager service
|
|
75
97
|
*/
|
|
76
98
|
export class DocumentManagerTag extends Context.Tag(
|
|
77
|
-
"@voidhash/mimic-
|
|
99
|
+
"@voidhash/mimic-effect/DocumentManager"
|
|
78
100
|
)<DocumentManagerTag, DocumentManager>() {}
|
|
79
101
|
|
|
80
102
|
// =============================================================================
|
|
81
|
-
//
|
|
103
|
+
// Internal Types
|
|
82
104
|
// =============================================================================
|
|
83
105
|
|
|
84
106
|
/**
|
|
85
|
-
*
|
|
107
|
+
* Document instance state
|
|
86
108
|
*/
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
interface DocumentInstance<TSchema extends Primitive.AnyPrimitive> {
|
|
110
|
+
/** The underlying ServerDocument */
|
|
111
|
+
readonly document: ServerDocument.ServerDocument<TSchema>;
|
|
112
|
+
/** PubSub for broadcasting messages */
|
|
113
|
+
readonly pubsub: PubSub.PubSub<ServerBroadcast>;
|
|
114
|
+
/** Version at last snapshot */
|
|
115
|
+
readonly lastSnapshotVersion: Ref.Ref<number>;
|
|
116
|
+
/** Timestamp of last snapshot (ms) */
|
|
117
|
+
readonly lastSnapshotTime: Ref.Ref<number>;
|
|
118
|
+
/** Transactions since last snapshot */
|
|
119
|
+
readonly transactionsSinceSnapshot: Ref.Ref<number>;
|
|
120
|
+
/** Last activity timestamp (ms) */
|
|
121
|
+
readonly lastActivityTime: Ref.Ref<number>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// Config Context Tag
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Context tag for DocumentManager configuration
|
|
130
|
+
*/
|
|
131
|
+
export class DocumentManagerConfigTag extends Context.Tag(
|
|
132
|
+
"@voidhash/mimic-effect/DocumentManagerConfig"
|
|
133
|
+
)<DocumentManagerConfigTag, ResolvedConfig<Primitive.AnyPrimitive>>() {}
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Layer Implementation
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create the DocumentManager layer.
|
|
141
|
+
* Requires ColdStorage, HotStorage, and DocumentManagerConfig.
|
|
142
|
+
*/
|
|
143
|
+
export const layer = Layer.scoped(
|
|
144
|
+
DocumentManagerTag,
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const coldStorage = yield* ColdStorageTag;
|
|
147
|
+
const hotStorage = yield* HotStorageTag;
|
|
148
|
+
const config = yield* DocumentManagerConfigTag;
|
|
149
|
+
|
|
150
|
+
// Store: documentId -> DocumentInstance
|
|
151
|
+
const store = yield* Ref.make(
|
|
152
|
+
HashMap.empty<string, DocumentInstance<Primitive.AnyPrimitive>>()
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Current schema version (hard-coded to 1 for now)
|
|
156
|
+
const SCHEMA_VERSION = 1;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Compute initial state for a new document
|
|
160
|
+
*/
|
|
161
|
+
const computeInitialState = (
|
|
162
|
+
documentId: string
|
|
163
|
+
): Effect.Effect<Primitive.InferSetInput<typeof config.schema> | undefined> => {
|
|
164
|
+
if (config.initial === undefined) {
|
|
165
|
+
return Effect.succeed(undefined);
|
|
108
166
|
}
|
|
109
167
|
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
);
|
|
168
|
+
// Check if it's a function or static value
|
|
169
|
+
if (typeof config.initial === "function") {
|
|
170
|
+
return (config.initial as (ctx: { documentId: string }) => Effect.Effect<Primitive.InferSetInput<typeof config.schema>>)({ documentId });
|
|
171
|
+
}
|
|
115
172
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
? yield* storage.onLoad(rawState)
|
|
119
|
-
: config.initial !== undefined
|
|
120
|
-
? yield* config.initial({ documentId })
|
|
121
|
-
: undefined;
|
|
122
|
-
|
|
123
|
-
// Create PubSub for broadcasting
|
|
124
|
-
const pubsub = yield* PubSub.unbounded<Protocol.ServerBroadcast>();
|
|
125
|
-
|
|
126
|
-
// Create ServerDocument with broadcast callback
|
|
127
|
-
const serverDocument = ServerDocument.make({
|
|
128
|
-
schema: config.schema,
|
|
129
|
-
initialState: initialState as Primitive.InferSetInput<typeof config.schema> | undefined,
|
|
130
|
-
maxTransactionHistory: config.maxTransactionHistory,
|
|
131
|
-
onBroadcast: (transactionMessage) => {
|
|
132
|
-
// Get current state and save to storage
|
|
133
|
-
const currentState = serverDocument.get();
|
|
134
|
-
|
|
135
|
-
// Run save in background (fire-and-forget with error logging)
|
|
136
|
-
Effect.runFork(
|
|
137
|
-
Effect.gen(function* () {
|
|
138
|
-
if (currentState !== undefined) {
|
|
139
|
-
const transformedState = yield* storage.onSave(currentState);
|
|
140
|
-
yield* Effect.catchAll(
|
|
141
|
-
storage.save(documentId, transformedState),
|
|
142
|
-
(error) => Effect.logError("Failed to save document", error)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
})
|
|
146
|
-
);
|
|
173
|
+
return Effect.succeed(config.initial as Primitive.InferSetInput<typeof config.schema>);
|
|
174
|
+
};
|
|
147
175
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
176
|
+
/**
|
|
177
|
+
* Restore a document from storage
|
|
178
|
+
*/
|
|
179
|
+
const restoreDocument = (
|
|
180
|
+
documentId: string
|
|
181
|
+
): Effect.Effect<DocumentInstance<typeof config.schema>, ColdStorageError | HotStorageError> =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
// 1. Load snapshot from ColdStorage (errors propagate - do not silently fallback)
|
|
184
|
+
const storedDoc = yield* coldStorage.load(documentId);
|
|
185
|
+
|
|
186
|
+
let initialState: Primitive.InferSetInput<typeof config.schema> | undefined;
|
|
187
|
+
let initialVersion = 0;
|
|
188
|
+
|
|
189
|
+
if (storedDoc) {
|
|
190
|
+
// Use stored state
|
|
191
|
+
initialState = storedDoc.state as Primitive.InferSetInput<typeof config.schema>;
|
|
192
|
+
initialVersion = storedDoc.version;
|
|
193
|
+
} else {
|
|
194
|
+
// Compute initial state
|
|
195
|
+
initialState = yield* computeInitialState(documentId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 2. Create PubSub for broadcasting
|
|
199
|
+
const pubsub = yield* PubSub.unbounded<ServerBroadcast>();
|
|
200
|
+
|
|
201
|
+
// 3. Create refs for tracking
|
|
202
|
+
const lastSnapshotVersion = yield* Ref.make(initialVersion);
|
|
203
|
+
const lastSnapshotTime = yield* Ref.make(Date.now());
|
|
204
|
+
const transactionsSinceSnapshot = yield* Ref.make(0);
|
|
205
|
+
const lastActivityTime = yield* Ref.make(Date.now());
|
|
206
|
+
|
|
207
|
+
// 4. Create ServerDocument with callbacks
|
|
208
|
+
const document = ServerDocument.make({
|
|
209
|
+
schema: config.schema,
|
|
210
|
+
initialState,
|
|
211
|
+
initialVersion,
|
|
212
|
+
maxTransactionHistory: config.maxTransactionHistory,
|
|
213
|
+
onBroadcast: (message: ServerDocument.TransactionMessage) => {
|
|
214
|
+
// This is called synchronously by ServerDocument
|
|
215
|
+
// We need to publish to PubSub
|
|
216
|
+
Effect.runSync(
|
|
217
|
+
PubSub.publish(pubsub, {
|
|
218
|
+
type: "transaction",
|
|
219
|
+
transaction: message.transaction,
|
|
220
|
+
version: message.version,
|
|
221
|
+
})
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
onRejection: (transactionId: string, reason: string) => {
|
|
225
|
+
Effect.runSync(
|
|
226
|
+
PubSub.publish(pubsub, {
|
|
227
|
+
type: "error",
|
|
228
|
+
transactionId,
|
|
229
|
+
reason,
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 5. Load WAL entries (errors propagate - do not silently fallback)
|
|
236
|
+
const walEntries = yield* hotStorage.getEntries(documentId, initialVersion);
|
|
237
|
+
|
|
238
|
+
// 6. Verify WAL continuity (warning only, non-blocking)
|
|
239
|
+
if (walEntries.length > 0) {
|
|
240
|
+
const firstWalVersion = walEntries[0]!.version;
|
|
241
|
+
const expectedFirst = initialVersion + 1;
|
|
242
|
+
|
|
243
|
+
if (firstWalVersion !== expectedFirst) {
|
|
244
|
+
yield* Effect.logWarning("WAL version gap detected", {
|
|
245
|
+
documentId,
|
|
246
|
+
snapshotVersion: initialVersion,
|
|
247
|
+
firstWalVersion,
|
|
248
|
+
expectedFirst,
|
|
249
|
+
});
|
|
250
|
+
yield* Metric.increment(Metrics.storageVersionGaps);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check internal gaps
|
|
254
|
+
for (let i = 1; i < walEntries.length; i++) {
|
|
255
|
+
const prev = walEntries[i - 1]!.version;
|
|
256
|
+
const curr = walEntries[i]!.version;
|
|
257
|
+
if (curr !== prev + 1) {
|
|
258
|
+
yield* Effect.logWarning("WAL internal gap detected", {
|
|
259
|
+
documentId,
|
|
260
|
+
previousVersion: prev,
|
|
261
|
+
currentVersion: curr,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 7. Replay WAL entries
|
|
268
|
+
for (const entry of walEntries) {
|
|
269
|
+
const result = document.submit(entry.transaction);
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
yield* Effect.logWarning("Skipping corrupted WAL entry", {
|
|
272
|
+
documentId,
|
|
273
|
+
version: entry.version,
|
|
274
|
+
reason: result.reason,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const instance: DocumentInstance<typeof config.schema> = {
|
|
280
|
+
document,
|
|
281
|
+
pubsub,
|
|
282
|
+
lastSnapshotVersion,
|
|
283
|
+
lastSnapshotTime,
|
|
284
|
+
transactionsSinceSnapshot,
|
|
285
|
+
lastActivityTime,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Track metrics - determine if restored or created
|
|
289
|
+
if (storedDoc) {
|
|
290
|
+
yield* Metric.increment(Metrics.documentsRestored);
|
|
291
|
+
} else {
|
|
292
|
+
yield* Metric.increment(Metrics.documentsCreated);
|
|
293
|
+
}
|
|
294
|
+
yield* Metric.incrementBy(Metrics.documentsActive, 1);
|
|
295
|
+
|
|
296
|
+
return instance;
|
|
166
297
|
});
|
|
167
298
|
|
|
168
|
-
|
|
299
|
+
/**
|
|
300
|
+
* Get or create a document instance
|
|
301
|
+
*/
|
|
302
|
+
const getOrCreateDocument = (
|
|
303
|
+
documentId: string
|
|
304
|
+
): Effect.Effect<DocumentInstance<typeof config.schema>, ColdStorageError | HotStorageError> =>
|
|
305
|
+
Effect.gen(function* () {
|
|
306
|
+
const current = yield* Ref.get(store);
|
|
307
|
+
const existing = HashMap.get(current, documentId);
|
|
169
308
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
309
|
+
if (existing._tag === "Some") {
|
|
310
|
+
// Update activity time
|
|
311
|
+
yield* Ref.set(existing.value.lastActivityTime, Date.now());
|
|
312
|
+
return existing.value as DocumentInstance<typeof config.schema>;
|
|
313
|
+
}
|
|
175
314
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
HashMap.set(map, documentId, instance)
|
|
179
|
-
);
|
|
315
|
+
// Restore document
|
|
316
|
+
const instance = yield* restoreDocument(documentId);
|
|
180
317
|
|
|
181
|
-
|
|
182
|
-
|
|
318
|
+
// Store it
|
|
319
|
+
yield* Ref.update(store, (map) =>
|
|
320
|
+
HashMap.set(map, documentId, instance)
|
|
321
|
+
);
|
|
183
322
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
documentId: string,
|
|
187
|
-
transaction: Transaction.Transaction
|
|
188
|
-
): Effect.Effect<Protocol.SubmitResult> =>
|
|
189
|
-
Effect.gen(function* () {
|
|
190
|
-
const instance = yield* getOrCreateDocument(documentId);
|
|
191
|
-
const result = instance.document.submit(transaction);
|
|
192
|
-
return result;
|
|
193
|
-
});
|
|
323
|
+
return instance;
|
|
324
|
+
});
|
|
194
325
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Save a snapshot to ColdStorage derived from WAL entries.
|
|
328
|
+
* This ensures snapshots are always based on durable WAL data.
|
|
329
|
+
* Idempotent: skips save if already snapshotted at target version.
|
|
330
|
+
* Truncate failures are non-fatal and will be retried on next snapshot.
|
|
331
|
+
*/
|
|
332
|
+
const saveSnapshot = (
|
|
333
|
+
documentId: string,
|
|
334
|
+
instance: DocumentInstance<typeof config.schema>,
|
|
335
|
+
targetVersion: number
|
|
336
|
+
): Effect.Effect<void, ColdStorageError | HotStorageError> =>
|
|
337
|
+
Effect.gen(function* () {
|
|
338
|
+
const lastSnapshotVersion = yield* Ref.get(instance.lastSnapshotVersion);
|
|
339
|
+
|
|
340
|
+
// Idempotency check: skip if already snapshotted at this version
|
|
341
|
+
if (targetVersion <= lastSnapshotVersion) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const snapshotStartTime = Date.now();
|
|
346
|
+
|
|
347
|
+
// Load base snapshot from cold storage
|
|
348
|
+
const baseSnapshot = yield* coldStorage.load(documentId);
|
|
349
|
+
const baseVersion = baseSnapshot?.version ?? 0;
|
|
350
|
+
const baseState = baseSnapshot?.state as Primitive.InferState<typeof config.schema> | undefined;
|
|
351
|
+
|
|
352
|
+
// Load WAL entries from base to target
|
|
353
|
+
const walEntries = yield* hotStorage.getEntries(documentId, baseVersion);
|
|
354
|
+
const relevantEntries = walEntries.filter(e => e.version <= targetVersion);
|
|
355
|
+
|
|
356
|
+
if (relevantEntries.length === 0 && !baseSnapshot) {
|
|
357
|
+
// Nothing to snapshot
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Rebuild state by replaying WAL on base
|
|
362
|
+
let snapshotState: Primitive.InferState<typeof config.schema> | undefined = baseState;
|
|
363
|
+
for (const entry of relevantEntries) {
|
|
364
|
+
// Create a temporary document to apply the transaction
|
|
365
|
+
const tempDoc = Document.make(config.schema, { initialState: snapshotState });
|
|
366
|
+
tempDoc.apply(entry.transaction.ops);
|
|
367
|
+
snapshotState = tempDoc.get();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (snapshotState === undefined) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const snapshotVersion = relevantEntries.length > 0
|
|
375
|
+
? relevantEntries[relevantEntries.length - 1]!.version
|
|
376
|
+
: baseVersion;
|
|
377
|
+
|
|
378
|
+
// Re-check before saving (in case another snapshot completed while we were working)
|
|
379
|
+
// This prevents a slower snapshot from overwriting a more recent one
|
|
380
|
+
const currentLastSnapshot = yield* Ref.get(instance.lastSnapshotVersion);
|
|
381
|
+
if (snapshotVersion <= currentLastSnapshot) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const storedDoc: StoredDocument = {
|
|
386
|
+
state: snapshotState,
|
|
387
|
+
version: snapshotVersion,
|
|
388
|
+
schemaVersion: SCHEMA_VERSION,
|
|
389
|
+
savedAt: Date.now(),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Save to ColdStorage - let errors propagate
|
|
393
|
+
yield* coldStorage.save(documentId, storedDoc);
|
|
394
|
+
|
|
395
|
+
// Track snapshot metrics
|
|
396
|
+
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
397
|
+
yield* Metric.increment(Metrics.storageSnapshots);
|
|
398
|
+
yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);
|
|
399
|
+
|
|
400
|
+
// Update tracking BEFORE truncate (for idempotency on retry)
|
|
401
|
+
yield* Ref.set(instance.lastSnapshotVersion, snapshotVersion);
|
|
402
|
+
yield* Ref.set(instance.lastSnapshotTime, Date.now());
|
|
403
|
+
yield* Ref.set(instance.transactionsSinceSnapshot, 0);
|
|
404
|
+
|
|
405
|
+
// Truncate WAL - non-fatal, will be retried on next snapshot
|
|
406
|
+
yield* Effect.catchAll(hotStorage.truncate(documentId, snapshotVersion), (e) =>
|
|
407
|
+
Effect.logWarning("WAL truncate failed - will retry on next snapshot", {
|
|
408
|
+
documentId,
|
|
409
|
+
version: snapshotVersion,
|
|
410
|
+
error: e,
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if snapshot should be triggered
|
|
417
|
+
*/
|
|
418
|
+
const checkSnapshotTriggers = (
|
|
419
|
+
documentId: string,
|
|
420
|
+
instance: DocumentInstance<typeof config.schema>
|
|
421
|
+
): Effect.Effect<void, ColdStorageError | HotStorageError> =>
|
|
422
|
+
Effect.gen(function* () {
|
|
423
|
+
const txCount = yield* Ref.get(instance.transactionsSinceSnapshot);
|
|
424
|
+
const lastTime = yield* Ref.get(instance.lastSnapshotTime);
|
|
425
|
+
const now = Date.now();
|
|
426
|
+
const currentVersion = instance.document.getVersion();
|
|
427
|
+
|
|
428
|
+
const intervalMs = Duration.toMillis(config.snapshot.interval);
|
|
429
|
+
const threshold = config.snapshot.transactionThreshold;
|
|
430
|
+
|
|
431
|
+
// Check transaction threshold
|
|
432
|
+
if (txCount >= threshold) {
|
|
433
|
+
yield* saveSnapshot(documentId, instance, currentVersion);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Check time interval
|
|
438
|
+
if (now - lastTime >= intervalMs) {
|
|
439
|
+
yield* saveSnapshot(documentId, instance, currentVersion);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Start background GC fiber
|
|
446
|
+
*/
|
|
447
|
+
const startGCFiber = Effect.gen(function* () {
|
|
448
|
+
const gcLoop = Effect.gen(function* () {
|
|
449
|
+
const current = yield* Ref.get(store);
|
|
450
|
+
const now = Date.now();
|
|
451
|
+
const maxIdleMs = Duration.toMillis(config.maxIdleTime);
|
|
452
|
+
|
|
453
|
+
for (const [documentId, instance] of current) {
|
|
454
|
+
const lastActivity = yield* Ref.get(instance.lastActivityTime);
|
|
455
|
+
if (now - lastActivity >= maxIdleMs) {
|
|
456
|
+
// Save final snapshot before eviction (best effort)
|
|
457
|
+
const currentVersion = instance.document.getVersion();
|
|
458
|
+
yield* Effect.catchAll(saveSnapshot(documentId, instance, currentVersion), (e) =>
|
|
459
|
+
Effect.logError("Failed to save snapshot during eviction", {
|
|
460
|
+
documentId,
|
|
461
|
+
error: e,
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// Remove from store
|
|
466
|
+
yield* Ref.update(store, (map) => HashMap.remove(map, documentId));
|
|
467
|
+
|
|
468
|
+
// Track eviction metrics
|
|
469
|
+
yield* Metric.increment(Metrics.documentsEvicted);
|
|
470
|
+
yield* Metric.incrementBy(Metrics.documentsActive, -1);
|
|
471
|
+
|
|
472
|
+
yield* Effect.logInfo("Document evicted due to idle timeout", {
|
|
473
|
+
documentId,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Run GC every minute
|
|
480
|
+
yield* gcLoop.pipe(
|
|
481
|
+
Effect.repeat(Schedule.spaced("1 minute")),
|
|
482
|
+
Effect.fork
|
|
483
|
+
);
|
|
203
484
|
});
|
|
204
485
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
486
|
+
// Start GC fiber
|
|
487
|
+
yield* startGCFiber;
|
|
488
|
+
|
|
489
|
+
// Cleanup on shutdown
|
|
490
|
+
yield* Effect.addFinalizer(() =>
|
|
491
|
+
Effect.gen(function* () {
|
|
492
|
+
const current = yield* Ref.get(store);
|
|
493
|
+
for (const [documentId, instance] of current) {
|
|
494
|
+
// Best effort save - don't fail shutdown if storage is unavailable
|
|
495
|
+
const currentVersion = instance.document.getVersion();
|
|
496
|
+
yield* Effect.catchAll(saveSnapshot(documentId, instance, currentVersion), (e) =>
|
|
497
|
+
Effect.logError("Failed to save snapshot during shutdown", {
|
|
498
|
+
documentId,
|
|
499
|
+
error: e,
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
yield* Effect.logInfo("DocumentManager shutdown complete");
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
submit: (documentId, transaction) =>
|
|
221
509
|
Effect.gen(function* () {
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
510
|
+
const instance = yield* getOrCreateDocument(documentId);
|
|
511
|
+
const submitStartTime = Date.now();
|
|
512
|
+
|
|
513
|
+
// Phase 1: Validate (no side effects)
|
|
514
|
+
const validation = instance.document.validate(transaction);
|
|
515
|
+
|
|
516
|
+
if (!validation.valid) {
|
|
517
|
+
// Track rejection
|
|
518
|
+
yield* Metric.increment(Metrics.transactionsRejected);
|
|
519
|
+
const latency = Date.now() - submitStartTime;
|
|
520
|
+
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
success: false as const,
|
|
524
|
+
reason: validation.reason,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Phase 2: Append to WAL with gap check (BEFORE state mutation)
|
|
529
|
+
const walEntry: WalEntry = {
|
|
530
|
+
transaction,
|
|
531
|
+
version: validation.nextVersion,
|
|
532
|
+
timestamp: Date.now(),
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const appendResult = yield* Effect.either(
|
|
536
|
+
hotStorage.appendWithCheck(documentId, walEntry, validation.nextVersion)
|
|
226
537
|
);
|
|
227
538
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
539
|
+
if (appendResult._tag === "Left") {
|
|
540
|
+
// WAL append failed - do NOT apply, state unchanged
|
|
541
|
+
yield* Effect.logError("WAL append failed", {
|
|
542
|
+
documentId,
|
|
543
|
+
version: validation.nextVersion,
|
|
544
|
+
error: appendResult.left,
|
|
545
|
+
});
|
|
546
|
+
yield* Metric.increment(Metrics.walAppendFailures);
|
|
232
547
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
548
|
+
const latency = Date.now() - submitStartTime;
|
|
549
|
+
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
236
550
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
551
|
+
// Return failure - client must retry
|
|
552
|
+
return {
|
|
553
|
+
success: false as const,
|
|
554
|
+
reason: "Storage unavailable. Please retry.",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
242
557
|
|
|
243
|
-
|
|
244
|
-
|
|
558
|
+
// Phase 3: Apply (state mutation + broadcast)
|
|
559
|
+
instance.document.apply(transaction);
|
|
245
560
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
561
|
+
// Track metrics
|
|
562
|
+
const latency = Date.now() - submitStartTime;
|
|
563
|
+
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
564
|
+
yield* Metric.increment(Metrics.transactionsProcessed);
|
|
565
|
+
yield* Metric.increment(Metrics.storageWalAppends);
|
|
566
|
+
|
|
567
|
+
// Increment transaction count
|
|
568
|
+
yield* Ref.update(
|
|
569
|
+
instance.transactionsSinceSnapshot,
|
|
570
|
+
(n) => n + 1
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
// Check snapshot triggers
|
|
574
|
+
yield* checkSnapshotTriggers(documentId, instance);
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
success: true as const,
|
|
578
|
+
version: validation.nextVersion,
|
|
579
|
+
};
|
|
580
|
+
}),
|
|
581
|
+
|
|
582
|
+
getSnapshot: (documentId) =>
|
|
583
|
+
Effect.gen(function* () {
|
|
584
|
+
const instance = yield* getOrCreateDocument(documentId);
|
|
585
|
+
return instance.document.getSnapshot();
|
|
586
|
+
}),
|
|
587
|
+
|
|
588
|
+
subscribe: (documentId) =>
|
|
589
|
+
Effect.gen(function* () {
|
|
590
|
+
const instance = yield* getOrCreateDocument(documentId);
|
|
591
|
+
return Stream.fromPubSub(instance.pubsub);
|
|
592
|
+
}),
|
|
593
|
+
|
|
594
|
+
touch: (documentId) =>
|
|
595
|
+
Effect.gen(function* () {
|
|
596
|
+
const current = yield* Ref.get(store);
|
|
597
|
+
const existing = HashMap.get(current, documentId);
|
|
598
|
+
if (existing._tag === "Some") {
|
|
599
|
+
yield* Ref.set(existing.value.lastActivityTime, Date.now());
|
|
600
|
+
}
|
|
601
|
+
}),
|
|
602
|
+
};
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// =============================================================================
|
|
607
|
+
// Re-export namespace
|
|
608
|
+
// =============================================================================
|
|
609
|
+
|
|
610
|
+
export const DocumentManager = {
|
|
611
|
+
Tag: DocumentManagerTag,
|
|
612
|
+
ConfigTag: DocumentManagerConfigTag,
|
|
613
|
+
layer,
|
|
614
|
+
};
|