@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 +0 -0
- package/package.json +40 -0
- package/src/DocumentManager.ts +252 -0
- package/src/DocumentProtocol.ts +112 -0
- package/src/MimicAuthService.ts +103 -0
- package/src/MimicConfig.ts +131 -0
- package/src/MimicDataStorage.ts +157 -0
- package/src/MimicServer.ts +363 -0
- package/src/PresenceManager.ts +297 -0
- package/src/WebSocketHandler.ts +735 -0
- package/src/auth/NoAuth.ts +46 -0
- package/src/errors.ts +113 -0
- package/src/index.ts +48 -0
- package/src/storage/InMemoryDataStorage.ts +66 -0
- package/tests/DocumentManager.test.ts +340 -0
- package/tests/DocumentProtocol.test.ts +113 -0
- package/tests/InMemoryDataStorage.test.ts +190 -0
- package/tests/MimicAuthService.test.ts +185 -0
- package/tests/MimicConfig.test.ts +175 -0
- package/tests/MimicDataStorage.test.ts +190 -0
- package/tests/MimicServer.test.ts +385 -0
- package/tests/NoAuth.test.ts +94 -0
- package/tests/PresenceManager.test.ts +421 -0
- package/tests/WebSocketHandler.test.ts +321 -0
- package/tests/errors.test.ts +77 -0
- package/tsconfig.build.json +24 -0
- package/tsconfig.json +8 -0
- package/tsdown.config.ts +18 -0
- package/vitest.mts +11 -0
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));
|