@voidhash/mimic-effect 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@voidhash/mimic-effect",
3
+ "version": "0.0.1-alpha.1",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/voidhashcom/mimic",
8
+ "directory": "packages/mimic-server-effect"
9
+ },
10
+ "main": "./src/index.ts",
11
+ "exports": {
12
+ ".": "./src/index.ts",
13
+ "./DocumentManager": "./src/DocumentManager.ts",
14
+ "./DocumentProtocol": "./src/DocumentProtocol.ts",
15
+ "./WebSocketHandler": "./src/WebSocketHandler.ts",
16
+ "./MimicServer": "./src/MimicServer.ts",
17
+ "./MimicConfig": "./src/MimicConfig.ts"
18
+ },
19
+ "scripts": {
20
+ "build": "tsdown",
21
+ "lint": "biome check .",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "vitest run -c vitest.mts"
24
+ },
25
+ "dependencies": {
26
+ "@effect/platform": "^0.93.8"
27
+ },
28
+ "devDependencies": {
29
+ "@effect/vitest": "^0.26.0",
30
+ "@voidhash/tsconfig": "workspace:*",
31
+ "tsdown": "^0.18.2",
32
+ "typescript": "5.8.3",
33
+ "vite-tsconfig-paths": "^5.1.4",
34
+ "vitest": "^3.2.4"
35
+ },
36
+ "peerDependencies": {
37
+ "effect": "catalog:",
38
+ "@voidhash/mimic": "workspace:*"
39
+ }
40
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @since 0.0.1
3
+ * Document manager that handles multiple document instances.
4
+ */
5
+ import * as Effect from "effect/Effect";
6
+ import * as Layer from "effect/Layer";
7
+ import * as PubSub from "effect/PubSub";
8
+ import * as Ref from "effect/Ref";
9
+ import * as HashMap from "effect/HashMap";
10
+ import * as Context from "effect/Context";
11
+ import * as Scope from "effect/Scope";
12
+ import * as Stream from "effect/Stream";
13
+ import type { Primitive, Transaction } from "@voidhash/mimic";
14
+ import { ServerDocument } from "@voidhash/mimic/server";
15
+
16
+ import * as Protocol from "./DocumentProtocol.js";
17
+ import { MimicServerConfigTag } from "./MimicConfig.js";
18
+ import { MimicDataStorageTag } from "./MimicDataStorage.js";
19
+ import { DocumentNotFoundError } from "./errors.js";
20
+
21
+ // =============================================================================
22
+ // Document Instance
23
+ // =============================================================================
24
+
25
+ /**
26
+ * A managed document instance that holds state and manages subscribers.
27
+ */
28
+ interface DocumentInstance {
29
+ /** The underlying ServerDocument */
30
+ readonly document: ServerDocument.ServerDocument<Primitive.AnyPrimitive>;
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
+ }
36
+
37
+ // =============================================================================
38
+ // Document Manager Service
39
+ // =============================================================================
40
+
41
+ /**
42
+ * Service interface for the DocumentManager.
43
+ */
44
+ export interface DocumentManager {
45
+ /**
46
+ * Submit a transaction to a document.
47
+ */
48
+ readonly submit: (
49
+ documentId: string,
50
+ transaction: Transaction.Transaction
51
+ ) => Effect.Effect<Protocol.SubmitResult>;
52
+
53
+ /**
54
+ * Get a snapshot of a document.
55
+ */
56
+ readonly getSnapshot: (
57
+ documentId: string
58
+ ) => Effect.Effect<Protocol.SnapshotMessage>;
59
+
60
+ /**
61
+ * Subscribe to broadcasts for a document.
62
+ * Returns a Stream of server broadcasts.
63
+ */
64
+ readonly subscribe: (
65
+ documentId: string
66
+ ) => Effect.Effect<
67
+ Stream.Stream<Protocol.ServerBroadcast>,
68
+ never,
69
+ Scope.Scope
70
+ >;
71
+ }
72
+
73
+ /**
74
+ * Context tag for DocumentManager.
75
+ */
76
+ export class DocumentManagerTag extends Context.Tag(
77
+ "@voidhash/mimic-server-effect/DocumentManager"
78
+ )<DocumentManagerTag, DocumentManager>() {}
79
+
80
+ // =============================================================================
81
+ // Document Manager Implementation
82
+ // =============================================================================
83
+
84
+ /**
85
+ * Create the DocumentManager service.
86
+ */
87
+ const makeDocumentManager = Effect.gen(function* () {
88
+ const config = yield* MimicServerConfigTag;
89
+ const storage = yield* MimicDataStorageTag;
90
+
91
+ // Map of document ID to document instance
92
+ const documents = yield* Ref.make(
93
+ HashMap.empty<string, DocumentInstance>()
94
+ );
95
+
96
+ // Get or create a document instance
97
+ const getOrCreateDocument = (
98
+ documentId: string
99
+ ): Effect.Effect<DocumentInstance> =>
100
+ Effect.gen(function* () {
101
+ const current = yield* Ref.get(documents);
102
+ const existing = HashMap.get(current, documentId);
103
+
104
+ if (existing._tag === "Some") {
105
+ // Increment ref count
106
+ yield* Ref.update(existing.value.refCount, (n) => n + 1);
107
+ return existing.value;
108
+ }
109
+
110
+ // Load initial state from storage
111
+ const rawState = yield* Effect.catchAll(
112
+ storage.load(documentId),
113
+ () => Effect.succeed(undefined)
114
+ );
115
+
116
+ // Transform loaded state with onLoad hook
117
+ const initialState = rawState !== undefined
118
+ ? yield* storage.onLoad(rawState)
119
+ : undefined;
120
+
121
+ // Create PubSub for broadcasting
122
+ const pubsub = yield* PubSub.unbounded<Protocol.ServerBroadcast>();
123
+
124
+ // Create ServerDocument with broadcast callback
125
+ const serverDocument = ServerDocument.make({
126
+ schema: config.schema,
127
+ initialState: initialState as Primitive.InferState<typeof config.schema> | undefined,
128
+ maxTransactionHistory: config.maxTransactionHistory,
129
+ onBroadcast: (transactionMessage) => {
130
+ // Get current state and save to storage
131
+ const currentState = serverDocument.get();
132
+
133
+ // Run save in background (fire-and-forget with error logging)
134
+ Effect.runFork(
135
+ Effect.gen(function* () {
136
+ if (currentState !== undefined) {
137
+ const transformedState = yield* storage.onSave(currentState);
138
+ yield* Effect.catchAll(
139
+ storage.save(documentId, transformedState),
140
+ (error) => Effect.logError("Failed to save document", error)
141
+ );
142
+ }
143
+ })
144
+ );
145
+
146
+ // Broadcast to subscribers
147
+ Effect.runSync(
148
+ PubSub.publish(pubsub, {
149
+ type: "transaction",
150
+ transaction: transactionMessage.transaction as Protocol.Transaction,
151
+ version: transactionMessage.version,
152
+ })
153
+ );
154
+ },
155
+ onRejection: (transactionId, reason) => {
156
+ Effect.runSync(
157
+ PubSub.publish(pubsub, {
158
+ type: "error",
159
+ transactionId,
160
+ reason,
161
+ })
162
+ );
163
+ },
164
+ });
165
+
166
+ const refCount = yield* Ref.make(1);
167
+
168
+ const instance: DocumentInstance = {
169
+ document: serverDocument,
170
+ pubsub,
171
+ refCount,
172
+ };
173
+
174
+ // Store in map
175
+ yield* Ref.update(documents, (map) =>
176
+ HashMap.set(map, documentId, instance)
177
+ );
178
+
179
+ return instance;
180
+ });
181
+
182
+ // Submit a transaction
183
+ const submit = (
184
+ documentId: string,
185
+ transaction: Transaction.Transaction
186
+ ): Effect.Effect<Protocol.SubmitResult> =>
187
+ Effect.gen(function* () {
188
+ const instance = yield* getOrCreateDocument(documentId);
189
+ const result = instance.document.submit(transaction);
190
+ return result;
191
+ });
192
+
193
+ // Get a snapshot
194
+ const getSnapshot = (
195
+ documentId: string
196
+ ): Effect.Effect<Protocol.SnapshotMessage> =>
197
+ Effect.gen(function* () {
198
+ const instance = yield* getOrCreateDocument(documentId);
199
+ const snapshot = instance.document.getSnapshot();
200
+ return snapshot;
201
+ });
202
+
203
+ // Subscribe to broadcasts
204
+ const subscribe = (
205
+ documentId: string
206
+ ): Effect.Effect<
207
+ Stream.Stream<Protocol.ServerBroadcast>,
208
+ never,
209
+ Scope.Scope
210
+ > =>
211
+ Effect.gen(function* () {
212
+ const instance = yield* getOrCreateDocument(documentId);
213
+
214
+ // Subscribe to the PubSub
215
+ const queue = yield* PubSub.subscribe(instance.pubsub);
216
+
217
+ // Ensure cleanup on scope close
218
+ yield* Effect.addFinalizer(() =>
219
+ Effect.gen(function* () {
220
+ // Decrement ref count
221
+ const count = yield* Ref.updateAndGet(
222
+ instance.refCount,
223
+ (n) => n - 1
224
+ );
225
+
226
+ // If no more subscribers, we could clean up the document
227
+ // For now, we keep it alive (could add idle timeout)
228
+ })
229
+ );
230
+
231
+ // Convert queue to stream
232
+ return Stream.fromQueue(queue);
233
+ });
234
+
235
+ const manager: DocumentManager = {
236
+ submit,
237
+ getSnapshot,
238
+ subscribe,
239
+ };
240
+
241
+ return manager;
242
+ });
243
+
244
+ /**
245
+ * Layer that provides DocumentManager.
246
+ * Requires MimicServerConfigTag and MimicDataStorageTag.
247
+ */
248
+ export const layer: Layer.Layer<
249
+ DocumentManagerTag,
250
+ never,
251
+ MimicServerConfigTag | MimicDataStorageTag
252
+ > = Layer.effect(DocumentManagerTag, makeDocumentManager);
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @since 0.0.1
3
+ * Protocol and schema definitions for document communication.
4
+ */
5
+ import * as Schema from "effect/Schema";
6
+
7
+ // =============================================================================
8
+ // Schema Definitions
9
+ // =============================================================================
10
+
11
+ /**
12
+ * Schema for a transaction operation.
13
+ */
14
+ export const OperationSchema = Schema.Struct({
15
+ kind: Schema.String,
16
+ path: Schema.Unknown, // OperationPath is complex, treat as unknown
17
+ payload: Schema.Unknown,
18
+ });
19
+
20
+ /**
21
+ * Schema for a transaction.
22
+ */
23
+ export const TransactionSchema = Schema.Struct({
24
+ id: Schema.String,
25
+ ops: Schema.Array(OperationSchema),
26
+ timestamp: Schema.Number,
27
+ });
28
+
29
+ export type Transaction = Schema.Schema.Type<typeof TransactionSchema>;
30
+
31
+ /**
32
+ * Schema for a server message that broadcasts a committed transaction.
33
+ */
34
+ export const TransactionMessageSchema = Schema.Struct({
35
+ type: Schema.Literal("transaction"),
36
+ transaction: TransactionSchema,
37
+ version: Schema.Number,
38
+ });
39
+
40
+ export type TransactionMessage = Schema.Schema.Type<typeof TransactionMessageSchema>;
41
+
42
+ /**
43
+ * Schema for a server message containing a snapshot.
44
+ */
45
+ export const SnapshotMessageSchema = Schema.Struct({
46
+ type: Schema.Literal("snapshot"),
47
+ state: Schema.Unknown,
48
+ version: Schema.Number,
49
+ });
50
+
51
+ export type SnapshotMessage = Schema.Schema.Type<typeof SnapshotMessageSchema>;
52
+
53
+ /**
54
+ * Schema for a server error message.
55
+ */
56
+ export const ErrorMessageSchema = Schema.Struct({
57
+ type: Schema.Literal("error"),
58
+ transactionId: Schema.String,
59
+ reason: Schema.String,
60
+ });
61
+
62
+ export type ErrorMessage = Schema.Schema.Type<typeof ErrorMessageSchema>;
63
+
64
+ /**
65
+ * Schema for a pong message.
66
+ */
67
+ export const PongMessageSchema = Schema.Struct({
68
+ type: Schema.Literal("pong"),
69
+ });
70
+
71
+ export type PongMessage = Schema.Schema.Type<typeof PongMessageSchema>;
72
+
73
+ /**
74
+ * Schema for authentication result message.
75
+ */
76
+ export const AuthResultMessageSchema = Schema.Struct({
77
+ type: Schema.Literal("auth_result"),
78
+ success: Schema.Boolean,
79
+ error: Schema.optional(Schema.String),
80
+ });
81
+
82
+ export type AuthResultMessage = Schema.Schema.Type<typeof AuthResultMessageSchema>;
83
+
84
+ /**
85
+ * Union of all server broadcast messages.
86
+ */
87
+ export const ServerBroadcastSchema = Schema.Union(
88
+ TransactionMessageSchema,
89
+ ErrorMessageSchema
90
+ );
91
+
92
+ export type ServerBroadcast = Schema.Schema.Type<typeof ServerBroadcastSchema>;
93
+
94
+ // =============================================================================
95
+ // Submit Result
96
+ // =============================================================================
97
+
98
+ /**
99
+ * Result of submitting a transaction.
100
+ */
101
+ export const SubmitResultSchema = Schema.Union(
102
+ Schema.Struct({
103
+ success: Schema.Literal(true),
104
+ version: Schema.Number,
105
+ }),
106
+ Schema.Struct({
107
+ success: Schema.Literal(false),
108
+ reason: Schema.String,
109
+ })
110
+ );
111
+
112
+ export type SubmitResult = Schema.Schema.Type<typeof SubmitResultSchema>;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @since 0.0.1
3
+ * Authentication service interface for Mimic connections.
4
+ * Provides pluggable authentication adapters.
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Context from "effect/Context";
8
+ import * as Layer from "effect/Layer";
9
+
10
+ // =============================================================================
11
+ // Authentication Types
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Result of an authentication attempt.
16
+ */
17
+ export type AuthResult =
18
+ | { readonly success: true; readonly userId?: string }
19
+ | { readonly success: false; readonly error: string };
20
+
21
+ /**
22
+ * Authentication handler function type.
23
+ * Can be synchronous or return a Promise.
24
+ */
25
+ export type AuthHandler = (token: string) => Promise<AuthResult> | AuthResult;
26
+
27
+ // =============================================================================
28
+ // Auth Service Interface
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Authentication service interface.
33
+ * Implementations can authenticate connections using various methods (JWT, API keys, etc.)
34
+ */
35
+ export interface MimicAuthService {
36
+ /**
37
+ * Authenticate a connection using the provided token.
38
+ * @param token - The authentication token provided by the client
39
+ * @returns The authentication result
40
+ */
41
+ readonly authenticate: (token: string) => Effect.Effect<AuthResult>;
42
+ }
43
+
44
+ // =============================================================================
45
+ // Context Tag
46
+ // =============================================================================
47
+
48
+ /**
49
+ * Context tag for MimicAuthService service.
50
+ */
51
+ export class MimicAuthServiceTag extends Context.Tag(
52
+ "@voidhash/mimic-server-effect/MimicAuthService"
53
+ )<MimicAuthServiceTag, MimicAuthService>() {}
54
+
55
+ // =============================================================================
56
+ // Layer Constructors
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Create a MimicAuthService layer from an auth handler function.
61
+ */
62
+ export const layer = (options: {
63
+ readonly authHandler: AuthHandler;
64
+ }): Layer.Layer<MimicAuthServiceTag> =>
65
+ Layer.succeed(MimicAuthServiceTag, {
66
+ authenticate: (token: string) =>
67
+ Effect.promise(() => Promise.resolve(options.authHandler(token))),
68
+ });
69
+
70
+ /**
71
+ * Create a MimicAuthService layer from an auth service implementation.
72
+ */
73
+ export const layerService = (service: MimicAuthService): Layer.Layer<MimicAuthServiceTag> =>
74
+ Layer.succeed(MimicAuthServiceTag, service);
75
+
76
+ /**
77
+ * Create a MimicAuthService layer from an Effect that produces an auth service.
78
+ */
79
+ export const layerEffect = <E, R>(
80
+ effect: Effect.Effect<MimicAuthService, E, R>
81
+ ): Layer.Layer<MimicAuthServiceTag, E, R> =>
82
+ Layer.effect(MimicAuthServiceTag, effect);
83
+
84
+ // =============================================================================
85
+ // Helper Functions
86
+ // =============================================================================
87
+
88
+ /**
89
+ * Create an auth service from an auth handler function.
90
+ */
91
+ export const make = (authHandler: AuthHandler): MimicAuthService => ({
92
+ authenticate: (token: string) =>
93
+ Effect.promise(() => Promise.resolve(authHandler(token))),
94
+ });
95
+
96
+ /**
97
+ * Create an auth service from an Effect-based authenticate function.
98
+ */
99
+ export const makeEffect = (
100
+ authenticate: (token: string) => Effect.Effect<AuthResult>
101
+ ): MimicAuthService => ({
102
+ authenticate,
103
+ });
@@ -0,0 +1,131 @@
1
+ /**
2
+ * @since 0.0.1
3
+ * Configuration types for the Mimic server.
4
+ */
5
+ import * as Context from "effect/Context";
6
+ import * as Duration from "effect/Duration";
7
+ import type { DurationInput } from "effect/Duration";
8
+ import * as Layer from "effect/Layer";
9
+ import type { Primitive, Presence } from "@voidhash/mimic";
10
+
11
+ // =============================================================================
12
+ // Mimic Server Configuration
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Configuration for the Mimic server.
17
+ *
18
+ * Note: Authentication and persistence are now handled by injectable services
19
+ * (MimicAuthService and MimicDataStorage) rather than config options.
20
+ */
21
+ export interface MimicServerConfig<TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive> {
22
+ /**
23
+ * The schema defining the document structure.
24
+ */
25
+ readonly schema: TSchema;
26
+
27
+ /**
28
+ * Maximum idle time for a document before it is cleaned up.
29
+ * @default "5 minutes"
30
+ */
31
+ readonly maxIdleTime: Duration.Duration;
32
+
33
+ /**
34
+ * Maximum number of processed transaction IDs to track for deduplication.
35
+ * @default 1000
36
+ */
37
+ readonly maxTransactionHistory: number;
38
+
39
+ /**
40
+ * Heartbeat interval for WebSocket connections.
41
+ * @default "30 seconds"
42
+ */
43
+ readonly heartbeatInterval: Duration.Duration;
44
+
45
+ /**
46
+ * Timeout for heartbeat responses before considering connection dead.
47
+ * @default "10 seconds"
48
+ */
49
+ readonly heartbeatTimeout: Duration.Duration;
50
+
51
+ /**
52
+ * Optional presence schema for ephemeral per-user data.
53
+ * When provided, enables presence features on WebSocket connections.
54
+ * @default undefined (presence disabled)
55
+ */
56
+ readonly presence: Presence.AnyPresence | undefined;
57
+ }
58
+
59
+ /**
60
+ * Options for creating a MimicServerConfig.
61
+ */
62
+ export interface MimicServerConfigOptions<TSchema extends Primitive.AnyPrimitive = Primitive.AnyPrimitive> {
63
+ /**
64
+ * The schema defining the document structure.
65
+ */
66
+ readonly schema: TSchema;
67
+
68
+ /**
69
+ * Maximum idle time for a document before it is cleaned up.
70
+ * @default "5 minutes"
71
+ */
72
+ readonly maxIdleTime?: DurationInput;
73
+
74
+ /**
75
+ * Maximum number of processed transaction IDs to track for deduplication.
76
+ * @default 1000
77
+ */
78
+ readonly maxTransactionHistory?: number;
79
+
80
+ /**
81
+ * Heartbeat interval for WebSocket connections.
82
+ * @default "30 seconds"
83
+ */
84
+ readonly heartbeatInterval?: DurationInput;
85
+
86
+ /**
87
+ * Timeout for heartbeat responses.
88
+ * @default "10 seconds"
89
+ */
90
+ readonly heartbeatTimeout?: DurationInput;
91
+
92
+ /**
93
+ * Optional presence schema for ephemeral per-user data.
94
+ * When provided, enables presence features on WebSocket connections.
95
+ * @default undefined (presence disabled)
96
+ */
97
+ readonly presence?: Presence.AnyPresence;
98
+ }
99
+
100
+ /**
101
+ * Create a MimicServerConfig from options.
102
+ */
103
+ export const make = <TSchema extends Primitive.AnyPrimitive>(
104
+ options: MimicServerConfigOptions<TSchema>
105
+ ): MimicServerConfig<TSchema> => ({
106
+ schema: options.schema,
107
+ maxIdleTime: Duration.decode(options.maxIdleTime ?? "5 minutes"),
108
+ maxTransactionHistory: options.maxTransactionHistory ?? 1000,
109
+ heartbeatInterval: Duration.decode(options.heartbeatInterval ?? "30 seconds"),
110
+ heartbeatTimeout: Duration.decode(options.heartbeatTimeout ?? "10 seconds"),
111
+ presence: options.presence,
112
+ });
113
+
114
+ // =============================================================================
115
+ // Context Tag
116
+ // =============================================================================
117
+
118
+ /**
119
+ * Context tag for MimicServerConfig.
120
+ */
121
+ export class MimicServerConfigTag extends Context.Tag(
122
+ "@voidhash/mimic-server-effect/MimicServerConfig"
123
+ )<MimicServerConfigTag, MimicServerConfig>() {}
124
+
125
+ /**
126
+ * Create a Layer that provides MimicServerConfig.
127
+ */
128
+ export const layer = <TSchema extends Primitive.AnyPrimitive>(
129
+ options: MimicServerConfigOptions<TSchema>
130
+ ): Layer.Layer<MimicServerConfigTag> =>
131
+ Layer.succeed(MimicServerConfigTag, make(options));