@voidhash/mimic-effect 0.0.9 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +93 -89
- 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 +193 -82
- package/dist/DocumentManager.d.cts +33 -19
- package/dist/DocumentManager.d.cts.map +1 -1
- package/dist/DocumentManager.d.mts +33 -19
- package/dist/DocumentManager.d.mts.map +1 -1
- package/dist/DocumentManager.mjs +189 -67
- package/dist/DocumentManager.mjs.map +1 -1
- package/dist/Errors.cjs +45 -0
- package/dist/Errors.d.cts +81 -0
- package/dist/Errors.d.cts.map +1 -0
- package/dist/Errors.d.mts +81 -0
- package/dist/Errors.d.mts.map +1 -0
- package/dist/Errors.mjs +40 -0
- package/dist/Errors.mjs.map +1 -0
- package/dist/HotStorage.cjs +77 -0
- package/dist/HotStorage.d.cts +54 -0
- package/dist/HotStorage.d.cts.map +1 -0
- package/dist/HotStorage.d.mts +54 -0
- package/dist/HotStorage.d.mts.map +1 -0
- package/dist/HotStorage.mjs +77 -0
- package/dist/HotStorage.mjs.map +1 -0
- package/dist/Metrics.cjs +121 -0
- package/dist/Metrics.d.cts +27 -0
- package/dist/Metrics.d.cts.map +1 -0
- package/dist/Metrics.d.mts +27 -0
- package/dist/Metrics.d.mts.map +1 -0
- package/dist/Metrics.mjs +106 -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 +443 -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 +445 -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 +75 -0
- package/dist/MimicServerEngine.d.cts.map +1 -0
- package/dist/MimicServerEngine.d.mts +75 -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/package.json +13 -3
- package/src/ColdStorage.ts +136 -0
- package/src/DocumentManager.ts +445 -193
- package/src/Errors.ts +100 -0
- package/src/HotStorage.ts +165 -0
- package/src/Metrics.ts +163 -0
- package/src/MimicAuthService.ts +126 -64
- package/src/MimicClusterServerEngine.ts +824 -0
- package/src/MimicServer.ts +448 -195
- package/src/MimicServerEngine.ts +272 -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/tests/ColdStorage.test.ts +136 -0
- package/tests/DocumentManager.test.ts +158 -287
- package/tests/HotStorage.test.ts +143 -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/tsconfig.json +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
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @voidhash/mimic-effect - MimicClusterServerEngine
|
|
3
|
+
*
|
|
4
|
+
* Clustered document management service using Effect Cluster for horizontal scaling.
|
|
5
|
+
* Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.
|
|
6
|
+
*
|
|
7
|
+
* This is an alternative to MimicServerEngine for distributed deployments.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
Context,
|
|
11
|
+
Duration,
|
|
12
|
+
Effect,
|
|
13
|
+
HashMap,
|
|
14
|
+
Layer,
|
|
15
|
+
Metric,
|
|
16
|
+
Option,
|
|
17
|
+
PubSub,
|
|
18
|
+
Ref,
|
|
19
|
+
Schema,
|
|
20
|
+
Scope,
|
|
21
|
+
Stream,
|
|
22
|
+
} from "effect";
|
|
23
|
+
import { Entity, Sharding } from "@effect/cluster";
|
|
24
|
+
import { Rpc } from "@effect/rpc";
|
|
25
|
+
import type { Presence, Primitive, Transaction } from "@voidhash/mimic";
|
|
26
|
+
import { ServerDocument } from "@voidhash/mimic/server";
|
|
27
|
+
import type {
|
|
28
|
+
MimicClusterServerEngineConfig,
|
|
29
|
+
PresenceEntry,
|
|
30
|
+
PresenceEvent,
|
|
31
|
+
PresenceSnapshot,
|
|
32
|
+
ResolvedClusterConfig,
|
|
33
|
+
StoredDocument,
|
|
34
|
+
WalEntry,
|
|
35
|
+
} from "./Types";
|
|
36
|
+
import type * as Protocol from "./Protocol";
|
|
37
|
+
import { ColdStorageTag, type ColdStorage } from "./ColdStorage";
|
|
38
|
+
import { HotStorageTag, type HotStorage } from "./HotStorage";
|
|
39
|
+
import { MimicAuthServiceTag } from "./MimicAuthService";
|
|
40
|
+
import { MimicServerEngineTag, type MimicServerEngine } from "./MimicServerEngine";
|
|
41
|
+
import type { SubmitResult } from "./DocumentManager";
|
|
42
|
+
import * as Metrics from "./Metrics";
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Default Configuration
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
const DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);
|
|
49
|
+
const DEFAULT_MAX_TRANSACTION_HISTORY = 1000;
|
|
50
|
+
const DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);
|
|
51
|
+
const DEFAULT_SNAPSHOT_THRESHOLD = 100;
|
|
52
|
+
const DEFAULT_SHARD_GROUP = "mimic-documents";
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// RPC Schemas
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Schema for encoded transaction (wire format)
|
|
60
|
+
*/
|
|
61
|
+
const EncodedTransactionSchema = Schema.Struct({
|
|
62
|
+
id: Schema.String,
|
|
63
|
+
ops: Schema.Array(Schema.Unknown),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Schema for submit result
|
|
68
|
+
*/
|
|
69
|
+
const SubmitResultSchema = Schema.Union(
|
|
70
|
+
Schema.Struct({
|
|
71
|
+
success: Schema.Literal(true),
|
|
72
|
+
version: Schema.Number,
|
|
73
|
+
}),
|
|
74
|
+
Schema.Struct({
|
|
75
|
+
success: Schema.Literal(false),
|
|
76
|
+
reason: Schema.String,
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Schema for snapshot response
|
|
82
|
+
*/
|
|
83
|
+
const SnapshotResponseSchema = Schema.Struct({
|
|
84
|
+
state: Schema.Unknown,
|
|
85
|
+
version: Schema.Number,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Schema for presence entry
|
|
90
|
+
*/
|
|
91
|
+
const PresenceEntrySchema = Schema.Struct({
|
|
92
|
+
data: Schema.Unknown,
|
|
93
|
+
userId: Schema.optional(Schema.String),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Schema for presence snapshot response
|
|
98
|
+
*/
|
|
99
|
+
const PresenceSnapshotResponseSchema = Schema.Struct({
|
|
100
|
+
presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Schema for presence event
|
|
105
|
+
*/
|
|
106
|
+
const PresenceEventSchema = Schema.Union(
|
|
107
|
+
Schema.Struct({
|
|
108
|
+
type: Schema.Literal("presence_update"),
|
|
109
|
+
id: Schema.String,
|
|
110
|
+
data: Schema.Unknown,
|
|
111
|
+
userId: Schema.optional(Schema.String),
|
|
112
|
+
}),
|
|
113
|
+
Schema.Struct({
|
|
114
|
+
type: Schema.Literal("presence_remove"),
|
|
115
|
+
id: Schema.String,
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Schema for server message (for broadcasts)
|
|
121
|
+
*/
|
|
122
|
+
const ServerMessageSchema = Schema.Unknown;
|
|
123
|
+
|
|
124
|
+
// =============================================================================
|
|
125
|
+
// Mimic Document Entity Definition
|
|
126
|
+
// =============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Define the Mimic Document Entity with its RPC protocol.
|
|
130
|
+
* This entity handles document operations for a single documentId.
|
|
131
|
+
*/
|
|
132
|
+
const MimicDocumentEntity = Entity.make("MimicDocument", [
|
|
133
|
+
// Submit a transaction
|
|
134
|
+
Rpc.make("Submit", {
|
|
135
|
+
payload: { transaction: EncodedTransactionSchema },
|
|
136
|
+
success: SubmitResultSchema,
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
// Get document snapshot
|
|
140
|
+
Rpc.make("GetSnapshot", {
|
|
141
|
+
success: SnapshotResponseSchema,
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// Touch document to prevent idle GC
|
|
145
|
+
Rpc.make("Touch", {
|
|
146
|
+
success: Schema.Void,
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
// Set presence for a connection
|
|
150
|
+
Rpc.make("SetPresence", {
|
|
151
|
+
payload: {
|
|
152
|
+
connectionId: Schema.String,
|
|
153
|
+
entry: PresenceEntrySchema,
|
|
154
|
+
},
|
|
155
|
+
success: Schema.Void,
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
// Remove presence for a connection
|
|
159
|
+
Rpc.make("RemovePresence", {
|
|
160
|
+
payload: { connectionId: Schema.String },
|
|
161
|
+
success: Schema.Void,
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
// Get presence snapshot
|
|
165
|
+
Rpc.make("GetPresenceSnapshot", {
|
|
166
|
+
success: PresenceSnapshotResponseSchema,
|
|
167
|
+
}),
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Entity State Types
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Document state managed by the entity
|
|
176
|
+
*/
|
|
177
|
+
interface EntityDocumentState<TSchema extends Primitive.AnyPrimitive> {
|
|
178
|
+
readonly document: ServerDocument.ServerDocument<TSchema>;
|
|
179
|
+
readonly broadcastPubSub: PubSub.PubSub<Protocol.ServerMessage>;
|
|
180
|
+
readonly presences: HashMap.HashMap<string, PresenceEntry>;
|
|
181
|
+
readonly presencePubSub: PubSub.PubSub<PresenceEvent>;
|
|
182
|
+
readonly lastSnapshotVersion: number;
|
|
183
|
+
readonly lastSnapshotTime: number;
|
|
184
|
+
readonly transactionsSinceSnapshot: number;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// =============================================================================
|
|
188
|
+
// Config Context Tag
|
|
189
|
+
// =============================================================================
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Context tag for cluster engine configuration
|
|
193
|
+
*/
|
|
194
|
+
class MimicClusterConfigTag extends Context.Tag(
|
|
195
|
+
"@voidhash/mimic-effect/MimicClusterConfig"
|
|
196
|
+
)<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}
|
|
197
|
+
|
|
198
|
+
// =============================================================================
|
|
199
|
+
// Resolve Configuration
|
|
200
|
+
// =============================================================================
|
|
201
|
+
|
|
202
|
+
const resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(
|
|
203
|
+
config: MimicClusterServerEngineConfig<TSchema>
|
|
204
|
+
): ResolvedClusterConfig<TSchema> => ({
|
|
205
|
+
schema: config.schema,
|
|
206
|
+
initial: config.initial,
|
|
207
|
+
presence: config.presence,
|
|
208
|
+
maxIdleTime: config.maxIdleTime
|
|
209
|
+
? Duration.decode(config.maxIdleTime)
|
|
210
|
+
: DEFAULT_MAX_IDLE_TIME,
|
|
211
|
+
maxTransactionHistory:
|
|
212
|
+
config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,
|
|
213
|
+
snapshot: {
|
|
214
|
+
interval: config.snapshot?.interval
|
|
215
|
+
? Duration.decode(config.snapshot.interval)
|
|
216
|
+
: DEFAULT_SNAPSHOT_INTERVAL,
|
|
217
|
+
transactionThreshold:
|
|
218
|
+
config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,
|
|
219
|
+
},
|
|
220
|
+
shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// =============================================================================
|
|
224
|
+
// Helper to decode/encode transactions
|
|
225
|
+
// =============================================================================
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Decode an encoded transaction to a Transaction object
|
|
229
|
+
*/
|
|
230
|
+
const decodeTransaction = (
|
|
231
|
+
encoded: { id: string; ops: readonly unknown[] }
|
|
232
|
+
): Transaction.Transaction => {
|
|
233
|
+
// Import Transaction dynamically to avoid circular deps
|
|
234
|
+
const { Transaction } = require("@voidhash/mimic");
|
|
235
|
+
return Transaction.decode(encoded as Transaction.EncodedTransaction);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Encode a Transaction to wire format
|
|
240
|
+
*/
|
|
241
|
+
const encodeTransaction = (
|
|
242
|
+
tx: Transaction.Transaction
|
|
243
|
+
): { id: string; ops: readonly unknown[] } => {
|
|
244
|
+
const { Transaction } = require("@voidhash/mimic");
|
|
245
|
+
return Transaction.encode(tx);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// =============================================================================
|
|
249
|
+
// Entity Handler Factory
|
|
250
|
+
// =============================================================================
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create the entity handler for MimicDocument
|
|
254
|
+
*/
|
|
255
|
+
const createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(
|
|
256
|
+
config: ResolvedClusterConfig<TSchema>,
|
|
257
|
+
coldStorage: ColdStorage,
|
|
258
|
+
hotStorage: HotStorage
|
|
259
|
+
) =>
|
|
260
|
+
Effect.gen(function* () {
|
|
261
|
+
// Get entity address to determine documentId
|
|
262
|
+
const address = yield* Entity.CurrentAddress;
|
|
263
|
+
const documentId = address.entityId;
|
|
264
|
+
|
|
265
|
+
// Current schema version (hard-coded to 1 for now)
|
|
266
|
+
const SCHEMA_VERSION = 1;
|
|
267
|
+
|
|
268
|
+
// Compute initial state
|
|
269
|
+
const computeInitialState = (): Effect.Effect<
|
|
270
|
+
Primitive.InferSetInput<TSchema> | undefined
|
|
271
|
+
> => {
|
|
272
|
+
if (config.initial === undefined) {
|
|
273
|
+
return Effect.succeed(undefined);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof config.initial === "function") {
|
|
277
|
+
return (
|
|
278
|
+
config.initial as (ctx: {
|
|
279
|
+
documentId: string;
|
|
280
|
+
}) => Effect.Effect<Primitive.InferSetInput<TSchema>>
|
|
281
|
+
)({ documentId });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return Effect.succeed(
|
|
285
|
+
config.initial as Primitive.InferSetInput<TSchema>
|
|
286
|
+
);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// Load snapshot from ColdStorage
|
|
290
|
+
const storedDoc = yield* Effect.catchAll(
|
|
291
|
+
coldStorage.load(documentId),
|
|
292
|
+
() => Effect.succeed(undefined)
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
let initialState: Primitive.InferSetInput<TSchema> | undefined;
|
|
296
|
+
let initialVersion = 0;
|
|
297
|
+
|
|
298
|
+
if (storedDoc) {
|
|
299
|
+
initialState =
|
|
300
|
+
storedDoc.state as Primitive.InferSetInput<TSchema>;
|
|
301
|
+
initialVersion = storedDoc.version;
|
|
302
|
+
} else {
|
|
303
|
+
initialState = yield* computeInitialState();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Create PubSubs for broadcasting
|
|
307
|
+
const broadcastPubSub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
308
|
+
const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();
|
|
309
|
+
|
|
310
|
+
// Create state ref
|
|
311
|
+
const stateRef = yield* Ref.make<EntityDocumentState<TSchema>>({
|
|
312
|
+
document: undefined as unknown as ServerDocument.ServerDocument<TSchema>,
|
|
313
|
+
broadcastPubSub,
|
|
314
|
+
presences: HashMap.empty(),
|
|
315
|
+
presencePubSub,
|
|
316
|
+
lastSnapshotVersion: initialVersion,
|
|
317
|
+
lastSnapshotTime: Date.now(),
|
|
318
|
+
transactionsSinceSnapshot: 0,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Create ServerDocument with callbacks
|
|
322
|
+
const document = ServerDocument.make({
|
|
323
|
+
schema: config.schema,
|
|
324
|
+
initialState,
|
|
325
|
+
initialVersion,
|
|
326
|
+
maxTransactionHistory: config.maxTransactionHistory,
|
|
327
|
+
onBroadcast: (message: ServerDocument.TransactionMessage) => {
|
|
328
|
+
Effect.runSync(
|
|
329
|
+
PubSub.publish(broadcastPubSub, {
|
|
330
|
+
type: "transaction",
|
|
331
|
+
transaction: message.transaction,
|
|
332
|
+
version: message.version,
|
|
333
|
+
} as Protocol.ServerMessage)
|
|
334
|
+
);
|
|
335
|
+
},
|
|
336
|
+
onRejection: (transactionId: string, reason: string) => {
|
|
337
|
+
Effect.runSync(
|
|
338
|
+
PubSub.publish(broadcastPubSub, {
|
|
339
|
+
type: "error",
|
|
340
|
+
transactionId,
|
|
341
|
+
reason,
|
|
342
|
+
} as Protocol.ServerMessage)
|
|
343
|
+
);
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Update state with document
|
|
348
|
+
yield* Ref.update(stateRef, (s) => ({ ...s, document }));
|
|
349
|
+
|
|
350
|
+
// Replay WAL entries
|
|
351
|
+
const walEntries = yield* Effect.catchAll(
|
|
352
|
+
hotStorage.getEntries(documentId, initialVersion),
|
|
353
|
+
() => Effect.succeed([] as WalEntry[])
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
for (const entry of walEntries) {
|
|
357
|
+
const result = document.submit(entry.transaction);
|
|
358
|
+
if (!result.success) {
|
|
359
|
+
yield* Effect.logWarning("Skipping corrupted WAL entry", {
|
|
360
|
+
documentId,
|
|
361
|
+
version: entry.version,
|
|
362
|
+
reason: result.reason,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Track metrics
|
|
368
|
+
if (storedDoc) {
|
|
369
|
+
yield* Metric.increment(Metrics.documentsRestored);
|
|
370
|
+
} else {
|
|
371
|
+
yield* Metric.increment(Metrics.documentsCreated);
|
|
372
|
+
}
|
|
373
|
+
yield* Metric.incrementBy(Metrics.documentsActive, 1);
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Save snapshot to ColdStorage
|
|
377
|
+
*/
|
|
378
|
+
const saveSnapshot = Effect.gen(function* () {
|
|
379
|
+
const state = yield* Ref.get(stateRef);
|
|
380
|
+
const docState = state.document.get();
|
|
381
|
+
const version = state.document.getVersion();
|
|
382
|
+
|
|
383
|
+
if (docState === undefined) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const storedDocument: StoredDocument = {
|
|
388
|
+
state: docState,
|
|
389
|
+
version,
|
|
390
|
+
schemaVersion: SCHEMA_VERSION,
|
|
391
|
+
savedAt: Date.now(),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const snapshotStartTime = Date.now();
|
|
395
|
+
|
|
396
|
+
yield* Effect.catchAll(
|
|
397
|
+
coldStorage.save(documentId, storedDocument),
|
|
398
|
+
(e) =>
|
|
399
|
+
Effect.logError("Failed to save snapshot", { documentId, error: e })
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const snapshotDuration = Date.now() - snapshotStartTime;
|
|
403
|
+
yield* Metric.increment(Metrics.storageSnapshots);
|
|
404
|
+
yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);
|
|
405
|
+
|
|
406
|
+
yield* Effect.catchAll(hotStorage.truncate(documentId, version), (e) =>
|
|
407
|
+
Effect.logError("Failed to truncate WAL", { documentId, error: e })
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
411
|
+
...s,
|
|
412
|
+
lastSnapshotVersion: version,
|
|
413
|
+
lastSnapshotTime: Date.now(),
|
|
414
|
+
transactionsSinceSnapshot: 0,
|
|
415
|
+
}));
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check if snapshot should be triggered
|
|
420
|
+
*/
|
|
421
|
+
const checkSnapshotTriggers = Effect.gen(function* () {
|
|
422
|
+
const state = yield* Ref.get(stateRef);
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
|
|
425
|
+
const intervalMs = Duration.toMillis(config.snapshot.interval);
|
|
426
|
+
const threshold = config.snapshot.transactionThreshold;
|
|
427
|
+
|
|
428
|
+
if (state.transactionsSinceSnapshot >= threshold) {
|
|
429
|
+
yield* saveSnapshot;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (now - state.lastSnapshotTime >= intervalMs) {
|
|
434
|
+
yield* saveSnapshot;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Cleanup on entity finalization
|
|
440
|
+
yield* Effect.addFinalizer(() =>
|
|
441
|
+
Effect.gen(function* () {
|
|
442
|
+
// Save final snapshot before entity is garbage collected
|
|
443
|
+
yield* saveSnapshot;
|
|
444
|
+
yield* Metric.incrementBy(Metrics.documentsActive, -1);
|
|
445
|
+
yield* Metric.increment(Metrics.documentsEvicted);
|
|
446
|
+
yield* Effect.logDebug("Entity finalized", { documentId });
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Return RPC handlers
|
|
451
|
+
return {
|
|
452
|
+
Submit: Effect.fnUntraced(function* ({ payload }) {
|
|
453
|
+
const submitStartTime = Date.now();
|
|
454
|
+
const state = yield* Ref.get(stateRef);
|
|
455
|
+
|
|
456
|
+
// Decode transaction
|
|
457
|
+
const transaction = decodeTransaction(payload.transaction);
|
|
458
|
+
|
|
459
|
+
// Submit to ServerDocument
|
|
460
|
+
const result = state.document.submit(transaction);
|
|
461
|
+
|
|
462
|
+
// Track latency
|
|
463
|
+
const latency = Date.now() - submitStartTime;
|
|
464
|
+
yield* Metric.update(Metrics.transactionsLatency, latency);
|
|
465
|
+
|
|
466
|
+
if (result.success) {
|
|
467
|
+
yield* Metric.increment(Metrics.transactionsProcessed);
|
|
468
|
+
|
|
469
|
+
// Append to WAL
|
|
470
|
+
const walEntry: WalEntry = {
|
|
471
|
+
transaction,
|
|
472
|
+
version: result.version,
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
yield* Effect.catchAll(hotStorage.append(documentId, walEntry), (e) =>
|
|
477
|
+
Effect.logError("Failed to append to WAL", {
|
|
478
|
+
documentId,
|
|
479
|
+
error: e,
|
|
480
|
+
})
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
yield* Metric.increment(Metrics.storageWalAppends);
|
|
484
|
+
|
|
485
|
+
// Increment transaction count
|
|
486
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
487
|
+
...s,
|
|
488
|
+
transactionsSinceSnapshot: s.transactionsSinceSnapshot + 1,
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
// Check snapshot triggers
|
|
492
|
+
yield* checkSnapshotTriggers;
|
|
493
|
+
} else {
|
|
494
|
+
yield* Metric.increment(Metrics.transactionsRejected);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return result;
|
|
498
|
+
}),
|
|
499
|
+
|
|
500
|
+
GetSnapshot: Effect.fnUntraced(function* () {
|
|
501
|
+
const state = yield* Ref.get(stateRef);
|
|
502
|
+
return state.document.getSnapshot();
|
|
503
|
+
}),
|
|
504
|
+
|
|
505
|
+
Touch: Effect.fnUntraced(function* () {
|
|
506
|
+
// Entity touch is handled automatically by the cluster framework
|
|
507
|
+
// Just update last activity time conceptually
|
|
508
|
+
return void 0;
|
|
509
|
+
}),
|
|
510
|
+
|
|
511
|
+
SetPresence: Effect.fnUntraced(function* ({ payload }) {
|
|
512
|
+
const { connectionId, entry } = payload;
|
|
513
|
+
|
|
514
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
515
|
+
...s,
|
|
516
|
+
presences: HashMap.set(s.presences, connectionId, entry),
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
yield* Metric.increment(Metrics.presenceUpdates);
|
|
520
|
+
yield* Metric.incrementBy(Metrics.presenceActive, 1);
|
|
521
|
+
|
|
522
|
+
const state = yield* Ref.get(stateRef);
|
|
523
|
+
const event: PresenceEvent = {
|
|
524
|
+
type: "presence_update",
|
|
525
|
+
id: connectionId,
|
|
526
|
+
data: entry.data,
|
|
527
|
+
userId: entry.userId,
|
|
528
|
+
};
|
|
529
|
+
yield* PubSub.publish(state.presencePubSub, event);
|
|
530
|
+
}),
|
|
531
|
+
|
|
532
|
+
RemovePresence: Effect.fnUntraced(function* ({ payload }) {
|
|
533
|
+
const { connectionId } = payload;
|
|
534
|
+
const state = yield* Ref.get(stateRef);
|
|
535
|
+
|
|
536
|
+
if (!HashMap.has(state.presences, connectionId)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
yield* Ref.update(stateRef, (s) => ({
|
|
541
|
+
...s,
|
|
542
|
+
presences: HashMap.remove(s.presences, connectionId),
|
|
543
|
+
}));
|
|
544
|
+
|
|
545
|
+
yield* Metric.incrementBy(Metrics.presenceActive, -1);
|
|
546
|
+
|
|
547
|
+
const event: PresenceEvent = {
|
|
548
|
+
type: "presence_remove",
|
|
549
|
+
id: connectionId,
|
|
550
|
+
};
|
|
551
|
+
yield* PubSub.publish(state.presencePubSub, event);
|
|
552
|
+
}),
|
|
553
|
+
|
|
554
|
+
GetPresenceSnapshot: Effect.fnUntraced(function* () {
|
|
555
|
+
const state = yield* Ref.get(stateRef);
|
|
556
|
+
const presences: Record<string, PresenceEntry> = {};
|
|
557
|
+
for (const [id, entry] of state.presences) {
|
|
558
|
+
presences[id] = entry;
|
|
559
|
+
}
|
|
560
|
+
return { presences };
|
|
561
|
+
}),
|
|
562
|
+
};
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// =============================================================================
|
|
566
|
+
// Subscription Store (for managing subscriptions at the gateway level)
|
|
567
|
+
// =============================================================================
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Store for managing document subscriptions
|
|
571
|
+
* This is needed because cluster entities don't support streaming directly
|
|
572
|
+
*/
|
|
573
|
+
interface SubscriptionStore {
|
|
574
|
+
readonly getOrCreatePubSub: (
|
|
575
|
+
documentId: string
|
|
576
|
+
) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;
|
|
577
|
+
readonly getOrCreatePresencePubSub: (
|
|
578
|
+
documentId: string
|
|
579
|
+
) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
class SubscriptionStoreTag extends Context.Tag(
|
|
583
|
+
"@voidhash/mimic-effect/SubscriptionStore"
|
|
584
|
+
)<SubscriptionStoreTag, SubscriptionStore>() {}
|
|
585
|
+
|
|
586
|
+
const subscriptionStoreLayer = Layer.effect(
|
|
587
|
+
SubscriptionStoreTag,
|
|
588
|
+
Effect.gen(function* () {
|
|
589
|
+
const documentPubSubs = yield* Ref.make(
|
|
590
|
+
HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()
|
|
591
|
+
);
|
|
592
|
+
const presencePubSubs = yield* Ref.make(
|
|
593
|
+
HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
getOrCreatePubSub: (documentId: string) =>
|
|
598
|
+
Effect.gen(function* () {
|
|
599
|
+
const current = yield* Ref.get(documentPubSubs);
|
|
600
|
+
const existing = HashMap.get(current, documentId);
|
|
601
|
+
if (existing._tag === "Some") {
|
|
602
|
+
return existing.value;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();
|
|
606
|
+
yield* Ref.update(documentPubSubs, (map) =>
|
|
607
|
+
HashMap.set(map, documentId, pubsub)
|
|
608
|
+
);
|
|
609
|
+
return pubsub;
|
|
610
|
+
}),
|
|
611
|
+
|
|
612
|
+
getOrCreatePresencePubSub: (documentId: string) =>
|
|
613
|
+
Effect.gen(function* () {
|
|
614
|
+
const current = yield* Ref.get(presencePubSubs);
|
|
615
|
+
const existing = HashMap.get(current, documentId);
|
|
616
|
+
if (existing._tag === "Some") {
|
|
617
|
+
return existing.value;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const pubsub = yield* PubSub.unbounded<PresenceEvent>();
|
|
621
|
+
yield* Ref.update(presencePubSubs, (map) =>
|
|
622
|
+
HashMap.set(map, documentId, pubsub)
|
|
623
|
+
);
|
|
624
|
+
return pubsub;
|
|
625
|
+
}),
|
|
626
|
+
};
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// =============================================================================
|
|
631
|
+
// Factory
|
|
632
|
+
// =============================================================================
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Create a MimicClusterServerEngine layer.
|
|
636
|
+
*
|
|
637
|
+
* This creates a clustered document management service using Effect Cluster.
|
|
638
|
+
* Each document becomes a cluster Entity with automatic sharding and failover.
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```typescript
|
|
642
|
+
* // 1. Create the engine
|
|
643
|
+
* const Engine = MimicClusterServerEngine.make({
|
|
644
|
+
* schema: DocSchema,
|
|
645
|
+
* initial: { title: "Untitled" },
|
|
646
|
+
* presence: CursorPresence,
|
|
647
|
+
* maxIdleTime: "5 minutes",
|
|
648
|
+
* snapshot: { interval: "5 minutes", transactionThreshold: 100 },
|
|
649
|
+
* shardGroup: "documents",
|
|
650
|
+
* })
|
|
651
|
+
*
|
|
652
|
+
* // 2. Create the WebSocket route
|
|
653
|
+
* const MimicRoute = MimicServer.layerHttpLayerRouter({
|
|
654
|
+
* path: "/mimic",
|
|
655
|
+
* })
|
|
656
|
+
*
|
|
657
|
+
* // 3. Wire together with cluster infrastructure
|
|
658
|
+
* const MimicLive = MimicRoute.pipe(
|
|
659
|
+
* Layer.provide(Engine),
|
|
660
|
+
* Layer.provide(ColdStorage.S3.make(...)),
|
|
661
|
+
* Layer.provide(HotStorage.Redis.make(...)),
|
|
662
|
+
* Layer.provide(MimicAuthService.make(...)),
|
|
663
|
+
* Layer.provide(ClusterInfrastructure),
|
|
664
|
+
* )
|
|
665
|
+
* ```
|
|
666
|
+
*/
|
|
667
|
+
export const make = <TSchema extends Primitive.AnyPrimitive>(
|
|
668
|
+
config: MimicClusterServerEngineConfig<TSchema>
|
|
669
|
+
): Layer.Layer<
|
|
670
|
+
MimicServerEngineTag,
|
|
671
|
+
never,
|
|
672
|
+
ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding
|
|
673
|
+
> => {
|
|
674
|
+
const resolvedConfig = resolveClusterConfig(config);
|
|
675
|
+
|
|
676
|
+
// Create config layer
|
|
677
|
+
const configLayer = Layer.succeed(
|
|
678
|
+
MimicClusterConfigTag,
|
|
679
|
+
resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>
|
|
680
|
+
);
|
|
681
|
+
|
|
682
|
+
// Create entity layer that registers with sharding
|
|
683
|
+
const entityLayer = MimicDocumentEntity.toLayer(
|
|
684
|
+
Effect.gen(function* () {
|
|
685
|
+
const clusterConfig = yield* MimicClusterConfigTag;
|
|
686
|
+
const coldStorage = yield* ColdStorageTag;
|
|
687
|
+
const hotStorage = yield* HotStorageTag;
|
|
688
|
+
|
|
689
|
+
return yield* createEntityHandler(
|
|
690
|
+
clusterConfig as ResolvedClusterConfig<TSchema>,
|
|
691
|
+
coldStorage,
|
|
692
|
+
hotStorage
|
|
693
|
+
);
|
|
694
|
+
}),
|
|
695
|
+
{
|
|
696
|
+
maxIdleTime: resolvedConfig.maxIdleTime,
|
|
697
|
+
concurrency: 1, // Sequential message processing per document
|
|
698
|
+
mailboxCapacity: 4096,
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// Create the engine service
|
|
703
|
+
const engineLayer = Layer.scoped(
|
|
704
|
+
MimicServerEngineTag,
|
|
705
|
+
Effect.gen(function* () {
|
|
706
|
+
// Get entity client maker
|
|
707
|
+
const makeClient = yield* MimicDocumentEntity.client;
|
|
708
|
+
|
|
709
|
+
// Get subscription store
|
|
710
|
+
const subscriptionStore = yield* SubscriptionStoreTag;
|
|
711
|
+
|
|
712
|
+
const engine: MimicServerEngine = {
|
|
713
|
+
submit: (documentId, transaction) =>
|
|
714
|
+
Effect.gen(function* () {
|
|
715
|
+
const client = makeClient(documentId);
|
|
716
|
+
const encodedTx = encodeTransaction(transaction);
|
|
717
|
+
const result = yield* client.Submit({
|
|
718
|
+
transaction: encodedTx as { id: string; ops: unknown[] },
|
|
719
|
+
}).pipe(
|
|
720
|
+
Effect.catchAll((error) =>
|
|
721
|
+
Effect.succeed({
|
|
722
|
+
success: false as const,
|
|
723
|
+
reason: `Cluster error: ${String(error)}`,
|
|
724
|
+
})
|
|
725
|
+
)
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
// Broadcast to local subscribers if success
|
|
729
|
+
if (result.success) {
|
|
730
|
+
const pubsub =
|
|
731
|
+
yield* subscriptionStore.getOrCreatePubSub(documentId);
|
|
732
|
+
yield* PubSub.publish(pubsub, {
|
|
733
|
+
type: "transaction",
|
|
734
|
+
transaction,
|
|
735
|
+
version: result.version,
|
|
736
|
+
} as Protocol.ServerMessage);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return result;
|
|
740
|
+
}),
|
|
741
|
+
|
|
742
|
+
getSnapshot: (documentId) =>
|
|
743
|
+
Effect.gen(function* () {
|
|
744
|
+
const client = makeClient(documentId);
|
|
745
|
+
return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);
|
|
746
|
+
}),
|
|
747
|
+
|
|
748
|
+
subscribe: (documentId) =>
|
|
749
|
+
Effect.gen(function* () {
|
|
750
|
+
const pubsub =
|
|
751
|
+
yield* subscriptionStore.getOrCreatePubSub(documentId);
|
|
752
|
+
return Stream.fromPubSub(pubsub);
|
|
753
|
+
}),
|
|
754
|
+
|
|
755
|
+
touch: (documentId) =>
|
|
756
|
+
Effect.gen(function* () {
|
|
757
|
+
const client = makeClient(documentId);
|
|
758
|
+
yield* client.Touch(undefined as void).pipe(Effect.orDie);
|
|
759
|
+
}),
|
|
760
|
+
|
|
761
|
+
getPresenceSnapshot: (documentId) =>
|
|
762
|
+
Effect.gen(function* () {
|
|
763
|
+
const client = makeClient(documentId);
|
|
764
|
+
return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);
|
|
765
|
+
}),
|
|
766
|
+
|
|
767
|
+
setPresence: (documentId, connectionId, entry) =>
|
|
768
|
+
Effect.gen(function* () {
|
|
769
|
+
const client = makeClient(documentId);
|
|
770
|
+
yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);
|
|
771
|
+
|
|
772
|
+
// Broadcast to local presence subscribers
|
|
773
|
+
const pubsub =
|
|
774
|
+
yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
775
|
+
yield* PubSub.publish(pubsub, {
|
|
776
|
+
type: "presence_update",
|
|
777
|
+
id: connectionId,
|
|
778
|
+
data: entry.data,
|
|
779
|
+
userId: entry.userId,
|
|
780
|
+
});
|
|
781
|
+
}),
|
|
782
|
+
|
|
783
|
+
removePresence: (documentId, connectionId) =>
|
|
784
|
+
Effect.gen(function* () {
|
|
785
|
+
const client = makeClient(documentId);
|
|
786
|
+
yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);
|
|
787
|
+
|
|
788
|
+
// Broadcast to local presence subscribers
|
|
789
|
+
const pubsub =
|
|
790
|
+
yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
791
|
+
yield* PubSub.publish(pubsub, {
|
|
792
|
+
type: "presence_remove",
|
|
793
|
+
id: connectionId,
|
|
794
|
+
});
|
|
795
|
+
}),
|
|
796
|
+
|
|
797
|
+
subscribePresence: (documentId) =>
|
|
798
|
+
Effect.gen(function* () {
|
|
799
|
+
const pubsub =
|
|
800
|
+
yield* subscriptionStore.getOrCreatePresencePubSub(documentId);
|
|
801
|
+
return Stream.fromPubSub(pubsub);
|
|
802
|
+
}),
|
|
803
|
+
|
|
804
|
+
config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
return engine;
|
|
808
|
+
})
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
// Compose all layers
|
|
812
|
+
return Layer.mergeAll(entityLayer, engineLayer).pipe(
|
|
813
|
+
Layer.provideMerge(subscriptionStoreLayer),
|
|
814
|
+
Layer.provideMerge(configLayer)
|
|
815
|
+
);
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// =============================================================================
|
|
819
|
+
// Re-export namespace
|
|
820
|
+
// =============================================================================
|
|
821
|
+
|
|
822
|
+
export const MimicClusterServerEngine = {
|
|
823
|
+
make,
|
|
824
|
+
};
|