@voidhash/mimic-effect 1.0.0-beta.1 → 1.0.0-beta.10
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 +116 -74
- package/dist/ColdStorage.cjs +9 -5
- package/dist/ColdStorage.d.cts.map +1 -1
- package/dist/ColdStorage.d.mts.map +1 -1
- package/dist/ColdStorage.mjs +9 -5
- package/dist/ColdStorage.mjs.map +1 -1
- package/dist/DocumentInstance.cjs +263 -0
- package/dist/DocumentInstance.d.cts +78 -0
- package/dist/DocumentInstance.d.cts.map +1 -0
- package/dist/DocumentInstance.d.mts +78 -0
- package/dist/DocumentInstance.d.mts.map +1 -0
- package/dist/DocumentInstance.mjs +264 -0
- package/dist/DocumentInstance.mjs.map +1 -0
- package/dist/Errors.cjs +10 -1
- package/dist/Errors.d.cts +18 -3
- package/dist/Errors.d.cts.map +1 -1
- package/dist/Errors.d.mts +18 -3
- package/dist/Errors.d.mts.map +1 -1
- package/dist/Errors.mjs +9 -1
- package/dist/Errors.mjs.map +1 -1
- package/dist/HotStorage.cjs +39 -12
- package/dist/HotStorage.d.cts +17 -1
- package/dist/HotStorage.d.cts.map +1 -1
- package/dist/HotStorage.d.mts +17 -1
- package/dist/HotStorage.d.mts.map +1 -1
- package/dist/HotStorage.mjs +39 -12
- package/dist/HotStorage.mjs.map +1 -1
- package/dist/Metrics.cjs +29 -1
- package/dist/Metrics.d.cts +5 -0
- package/dist/Metrics.d.cts.map +1 -1
- package/dist/Metrics.d.mts +5 -0
- package/dist/Metrics.d.mts.map +1 -1
- package/dist/Metrics.mjs +26 -1
- package/dist/Metrics.mjs.map +1 -1
- package/dist/MimicClusterServerEngine.cjs +44 -139
- package/dist/MimicClusterServerEngine.d.cts.map +1 -1
- package/dist/MimicClusterServerEngine.d.mts +1 -1
- package/dist/MimicClusterServerEngine.d.mts.map +1 -1
- package/dist/MimicClusterServerEngine.mjs +46 -141
- package/dist/MimicClusterServerEngine.mjs.map +1 -1
- package/dist/MimicServer.cjs +20 -20
- package/dist/MimicServer.d.cts.map +1 -1
- package/dist/MimicServer.d.mts.map +1 -1
- package/dist/MimicServer.mjs +20 -20
- package/dist/MimicServer.mjs.map +1 -1
- package/dist/MimicServerEngine.cjs +92 -11
- package/dist/MimicServerEngine.d.cts +12 -4
- package/dist/MimicServerEngine.d.cts.map +1 -1
- package/dist/MimicServerEngine.d.mts +12 -4
- package/dist/MimicServerEngine.d.mts.map +1 -1
- package/dist/MimicServerEngine.mjs +94 -13
- package/dist/MimicServerEngine.mjs.map +1 -1
- package/dist/PresenceManager.cjs +5 -5
- package/dist/PresenceManager.d.cts.map +1 -1
- package/dist/PresenceManager.d.mts.map +1 -1
- package/dist/PresenceManager.mjs +5 -5
- package/dist/PresenceManager.mjs.map +1 -1
- package/dist/Protocol.d.cts +1 -1
- package/dist/Protocol.d.mts +1 -1
- package/dist/Types.d.cts +9 -2
- package/dist/Types.d.cts.map +1 -1
- package/dist/Types.d.mts +9 -2
- package/dist/Types.d.mts.map +1 -1
- package/dist/index.cjs +5 -6
- package/dist/index.d.cts +3 -3
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +3 -3
- 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 +162 -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 +163 -0
- package/dist/testing/FailingStorage.mjs.map +1 -0
- package/dist/testing/HotStorageTestSuite.cjs +820 -0
- package/dist/testing/HotStorageTestSuite.d.cts +42 -0
- package/dist/testing/HotStorageTestSuite.d.cts.map +1 -0
- package/dist/testing/HotStorageTestSuite.d.mts +42 -0
- package/dist/testing/HotStorageTestSuite.d.mts.map +1 -0
- package/dist/testing/HotStorageTestSuite.mjs +820 -0
- package/dist/testing/HotStorageTestSuite.mjs.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.cjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.cts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts +37 -0
- package/dist/testing/StorageIntegrationTestSuite.d.mts.map +1 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs +487 -0
- package/dist/testing/StorageIntegrationTestSuite.mjs.map +1 -0
- package/dist/testing/assertions.cjs +117 -0
- package/dist/testing/assertions.mjs +112 -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 +8 -3
- package/src/ColdStorage.ts +21 -12
- package/src/DocumentInstance.ts +527 -0
- package/src/Errors.ts +15 -1
- package/src/HotStorage.ts +115 -24
- package/src/Metrics.ts +30 -0
- package/src/MimicClusterServerEngine.ts +120 -275
- package/src/MimicServer.ts +83 -75
- package/src/MimicServerEngine.ts +230 -30
- package/src/PresenceManager.ts +44 -34
- package/src/Types.ts +9 -2
- package/src/index.ts +5 -35
- package/src/testing/ColdStorageTestSuite.ts +589 -0
- package/src/testing/FailingStorage.ts +338 -0
- package/src/testing/HotStorageTestSuite.ts +1105 -0
- package/src/testing/StorageIntegrationTestSuite.ts +736 -0
- package/src/testing/assertions.ts +188 -0
- package/src/testing/index.ts +83 -0
- package/src/testing/types.ts +100 -0
- package/tests/ColdStorage.test.ts +8 -120
- package/tests/DocumentInstance.test.ts +669 -0
- package/tests/HotStorage.test.ts +7 -126
- package/tests/StorageIntegration.test.ts +259 -0
- package/tsdown.config.ts +1 -1
- package/dist/DocumentManager.cjs +0 -229
- package/dist/DocumentManager.d.cts +0 -59
- package/dist/DocumentManager.d.cts.map +0 -1
- package/dist/DocumentManager.d.mts +0 -59
- package/dist/DocumentManager.d.mts.map +0 -1
- package/dist/DocumentManager.mjs +0 -227
- package/dist/DocumentManager.mjs.map +0 -1
- package/src/DocumentManager.ts +0 -506
- package/tests/DocumentManager.test.ts +0 -335
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicClusterServerEngine.mjs","names":["initialState: Primitive.InferSetInput<TSchema> | undefined","Metrics.documentsRestored","Metrics.documentsCreated","Metrics.documentsActive","storedDocument: StoredDocument","Metrics.storageSnapshots","Metrics.storageSnapshotLatency","Metrics.documentsEvicted","Metrics.transactionsLatency","Metrics.transactionsProcessed","walEntry: WalEntry","Metrics.storageWalAppends","Metrics.transactionsRejected","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent","presences: Record<string, PresenceEntry>"],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicClusterServerEngine\n *\n * Clustered document management service using Effect Cluster for horizontal scaling.\n * Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.\n *\n * This is an alternative to MimicServerEngine for distributed deployments.\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n Option,\n PubSub,\n Ref,\n Schema,\n Scope,\n Stream,\n} from \"effect\";\nimport { Entity, Sharding } from \"@effect/cluster\";\nimport { Rpc } from \"@effect/rpc\";\nimport type { Presence, Primitive, Transaction } from \"@voidhash/mimic\";\nimport { ServerDocument } from \"@voidhash/mimic/server\";\nimport type {\n MimicClusterServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n PresenceSnapshot,\n ResolvedClusterConfig,\n StoredDocument,\n WalEntry,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag, type ColdStorage } from \"./ColdStorage\";\nimport { HotStorageTag, type HotStorage } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport type { SubmitResult } from \"./DocumentManager\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SHARD_GROUP = \"mimic-documents\";\n\n// =============================================================================\n// RPC Schemas\n// =============================================================================\n\n/**\n * Schema for encoded transaction (wire format)\n */\nconst EncodedTransactionSchema = Schema.Struct({\n id: Schema.String,\n ops: Schema.Array(Schema.Unknown),\n});\n\n/**\n * Schema for submit result\n */\nconst SubmitResultSchema = Schema.Union(\n Schema.Struct({\n success: Schema.Literal(true),\n version: Schema.Number,\n }),\n Schema.Struct({\n success: Schema.Literal(false),\n reason: Schema.String,\n })\n);\n\n/**\n * Schema for snapshot response\n */\nconst SnapshotResponseSchema = Schema.Struct({\n state: Schema.Unknown,\n version: Schema.Number,\n});\n\n/**\n * Schema for presence entry\n */\nconst PresenceEntrySchema = Schema.Struct({\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n});\n\n/**\n * Schema for presence snapshot response\n */\nconst PresenceSnapshotResponseSchema = Schema.Struct({\n presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),\n});\n\n/**\n * Schema for presence event\n */\nconst PresenceEventSchema = Schema.Union(\n Schema.Struct({\n type: Schema.Literal(\"presence_update\"),\n id: Schema.String,\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"presence_remove\"),\n id: Schema.String,\n })\n);\n\n/**\n * Schema for server message (for broadcasts)\n */\nconst ServerMessageSchema = Schema.Unknown;\n\n// =============================================================================\n// Mimic Document Entity Definition\n// =============================================================================\n\n/**\n * Define the Mimic Document Entity with its RPC protocol.\n * This entity handles document operations for a single documentId.\n */\nconst MimicDocumentEntity = Entity.make(\"MimicDocument\", [\n // Submit a transaction\n Rpc.make(\"Submit\", {\n payload: { transaction: EncodedTransactionSchema },\n success: SubmitResultSchema,\n }),\n\n // Get document snapshot\n Rpc.make(\"GetSnapshot\", {\n success: SnapshotResponseSchema,\n }),\n\n // Touch document to prevent idle GC\n Rpc.make(\"Touch\", {\n success: Schema.Void,\n }),\n\n // Set presence for a connection\n Rpc.make(\"SetPresence\", {\n payload: {\n connectionId: Schema.String,\n entry: PresenceEntrySchema,\n },\n success: Schema.Void,\n }),\n\n // Remove presence for a connection\n Rpc.make(\"RemovePresence\", {\n payload: { connectionId: Schema.String },\n success: Schema.Void,\n }),\n\n // Get presence snapshot\n Rpc.make(\"GetPresenceSnapshot\", {\n success: PresenceSnapshotResponseSchema,\n }),\n]);\n\n// =============================================================================\n// Entity State Types\n// =============================================================================\n\n/**\n * Document state managed by the entity\n */\ninterface EntityDocumentState<TSchema extends Primitive.AnyPrimitive> {\n readonly document: ServerDocument.ServerDocument<TSchema>;\n readonly broadcastPubSub: PubSub.PubSub<Protocol.ServerMessage>;\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly presencePubSub: PubSub.PubSub<PresenceEvent>;\n readonly lastSnapshotVersion: number;\n readonly lastSnapshotTime: number;\n readonly transactionsSinceSnapshot: number;\n}\n\n// =============================================================================\n// Config Context Tag\n// =============================================================================\n\n/**\n * Context tag for cluster engine configuration\n */\nclass MimicClusterConfigTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicClusterConfig\"\n)<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}\n\n// =============================================================================\n// Resolve Configuration\n// =============================================================================\n\nconst resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): ResolvedClusterConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n },\n shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,\n});\n\n// =============================================================================\n// Helper to decode/encode transactions\n// =============================================================================\n\n/**\n * Decode an encoded transaction to a Transaction object\n */\nconst decodeTransaction = (\n encoded: { id: string; ops: readonly unknown[] }\n): Transaction.Transaction => {\n // Import Transaction dynamically to avoid circular deps\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.decode(encoded as Transaction.EncodedTransaction);\n};\n\n/**\n * Encode a Transaction to wire format\n */\nconst encodeTransaction = (\n tx: Transaction.Transaction\n): { id: string; ops: readonly unknown[] } => {\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.encode(tx);\n};\n\n// =============================================================================\n// Entity Handler Factory\n// =============================================================================\n\n/**\n * Create the entity handler for MimicDocument\n */\nconst createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(\n config: ResolvedClusterConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n) =>\n Effect.gen(function* () {\n // Get entity address to determine documentId\n const address = yield* Entity.CurrentAddress;\n const documentId = address.entityId;\n\n // Current schema version (hard-coded to 1 for now)\n const SCHEMA_VERSION = 1;\n\n // Compute initial state\n const computeInitialState = (): Effect.Effect<\n Primitive.InferSetInput<TSchema> | undefined\n > => {\n if (config.initial === undefined) {\n return Effect.succeed(undefined);\n }\n\n if (typeof config.initial === \"function\") {\n return (\n config.initial as (ctx: {\n documentId: string;\n }) => Effect.Effect<Primitive.InferSetInput<TSchema>>\n )({ documentId });\n }\n\n return Effect.succeed(\n config.initial as Primitive.InferSetInput<TSchema>\n );\n };\n\n // Load snapshot from ColdStorage\n const storedDoc = yield* Effect.catchAll(\n coldStorage.load(documentId),\n () => Effect.succeed(undefined)\n );\n\n let initialState: Primitive.InferSetInput<TSchema> | undefined;\n let initialVersion = 0;\n\n if (storedDoc) {\n initialState =\n storedDoc.state as Primitive.InferSetInput<TSchema>;\n initialVersion = storedDoc.version;\n } else {\n initialState = yield* computeInitialState();\n }\n\n // Create PubSubs for broadcasting\n const broadcastPubSub = yield* PubSub.unbounded<Protocol.ServerMessage>();\n const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();\n\n // Create state ref\n const stateRef = yield* Ref.make<EntityDocumentState<TSchema>>({\n document: undefined as unknown as ServerDocument.ServerDocument<TSchema>,\n broadcastPubSub,\n presences: HashMap.empty(),\n presencePubSub,\n lastSnapshotVersion: initialVersion,\n lastSnapshotTime: Date.now(),\n transactionsSinceSnapshot: 0,\n });\n\n // Create ServerDocument with callbacks\n const document = ServerDocument.make({\n schema: config.schema,\n initialState,\n initialVersion,\n maxTransactionHistory: config.maxTransactionHistory,\n onBroadcast: (message: ServerDocument.TransactionMessage) => {\n Effect.runSync(\n PubSub.publish(broadcastPubSub, {\n type: \"transaction\",\n transaction: message.transaction,\n version: message.version,\n } as Protocol.ServerMessage)\n );\n },\n onRejection: (transactionId: string, reason: string) => {\n Effect.runSync(\n PubSub.publish(broadcastPubSub, {\n type: \"error\",\n transactionId,\n reason,\n } as Protocol.ServerMessage)\n );\n },\n });\n\n // Update state with document\n yield* Ref.update(stateRef, (s) => ({ ...s, document }));\n\n // Replay WAL entries\n const walEntries = yield* Effect.catchAll(\n hotStorage.getEntries(documentId, initialVersion),\n () => Effect.succeed([] as WalEntry[])\n );\n\n for (const entry of walEntries) {\n const result = document.submit(entry.transaction);\n if (!result.success) {\n yield* Effect.logWarning(\"Skipping corrupted WAL entry\", {\n documentId,\n version: entry.version,\n reason: result.reason,\n });\n }\n }\n\n // Track metrics\n if (storedDoc) {\n yield* Metric.increment(Metrics.documentsRestored);\n } else {\n yield* Metric.increment(Metrics.documentsCreated);\n }\n yield* Metric.incrementBy(Metrics.documentsActive, 1);\n\n /**\n * Save snapshot to ColdStorage\n */\n const saveSnapshot = Effect.gen(function* () {\n const state = yield* Ref.get(stateRef);\n const docState = state.document.get();\n const version = state.document.getVersion();\n\n if (docState === undefined) {\n return;\n }\n\n const storedDocument: StoredDocument = {\n state: docState,\n version,\n schemaVersion: SCHEMA_VERSION,\n savedAt: Date.now(),\n };\n\n const snapshotStartTime = Date.now();\n\n yield* Effect.catchAll(\n coldStorage.save(documentId, storedDocument),\n (e) =>\n Effect.logError(\"Failed to save snapshot\", { documentId, error: e })\n );\n\n const snapshotDuration = Date.now() - snapshotStartTime;\n yield* Metric.increment(Metrics.storageSnapshots);\n yield* Metric.update(Metrics.storageSnapshotLatency, snapshotDuration);\n\n yield* Effect.catchAll(hotStorage.truncate(documentId, version), (e) =>\n Effect.logError(\"Failed to truncate WAL\", { documentId, error: e })\n );\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n lastSnapshotVersion: version,\n lastSnapshotTime: Date.now(),\n transactionsSinceSnapshot: 0,\n }));\n });\n\n /**\n * Check if snapshot should be triggered\n */\n const checkSnapshotTriggers = Effect.gen(function* () {\n const state = yield* Ref.get(stateRef);\n const now = Date.now();\n\n const intervalMs = Duration.toMillis(config.snapshot.interval);\n const threshold = config.snapshot.transactionThreshold;\n\n if (state.transactionsSinceSnapshot >= threshold) {\n yield* saveSnapshot;\n return;\n }\n\n if (now - state.lastSnapshotTime >= intervalMs) {\n yield* saveSnapshot;\n return;\n }\n });\n\n // Cleanup on entity finalization\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n // Save final snapshot before entity is garbage collected\n yield* saveSnapshot;\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Effect.logDebug(\"Entity finalized\", { documentId });\n })\n );\n\n // Return RPC handlers\n return {\n Submit: Effect.fnUntraced(function* ({ payload }) {\n const submitStartTime = Date.now();\n const state = yield* Ref.get(stateRef);\n\n // Decode transaction\n const transaction = decodeTransaction(payload.transaction);\n\n // Submit to ServerDocument\n const result = state.document.submit(transaction);\n\n // Track latency\n const latency = Date.now() - submitStartTime;\n yield* Metric.update(Metrics.transactionsLatency, latency);\n\n if (result.success) {\n yield* Metric.increment(Metrics.transactionsProcessed);\n\n // Append to WAL\n const walEntry: WalEntry = {\n transaction,\n version: result.version,\n timestamp: Date.now(),\n };\n\n yield* Effect.catchAll(hotStorage.append(documentId, walEntry), (e) =>\n Effect.logError(\"Failed to append to WAL\", {\n documentId,\n error: e,\n })\n );\n\n yield* Metric.increment(Metrics.storageWalAppends);\n\n // Increment transaction count\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n transactionsSinceSnapshot: s.transactionsSinceSnapshot + 1,\n }));\n\n // Check snapshot triggers\n yield* checkSnapshotTriggers;\n } else {\n yield* Metric.increment(Metrics.transactionsRejected);\n }\n\n return result;\n }),\n\n GetSnapshot: Effect.fnUntraced(function* () {\n const state = yield* Ref.get(stateRef);\n return state.document.getSnapshot();\n }),\n\n Touch: Effect.fnUntraced(function* () {\n // Entity touch is handled automatically by the cluster framework\n // Just update last activity time conceptually\n return void 0;\n }),\n\n SetPresence: Effect.fnUntraced(function* ({ payload }) {\n const { connectionId, entry } = payload;\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.set(s.presences, connectionId, entry),\n }));\n\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n const state = yield* Ref.get(stateRef);\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n RemovePresence: Effect.fnUntraced(function* ({ payload }) {\n const { connectionId } = payload;\n const state = yield* Ref.get(stateRef);\n\n if (!HashMap.has(state.presences, connectionId)) {\n return;\n }\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.remove(s.presences, connectionId),\n }));\n\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n GetPresenceSnapshot: Effect.fnUntraced(function* () {\n const state = yield* Ref.get(stateRef);\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of state.presences) {\n presences[id] = entry;\n }\n return { presences };\n }),\n };\n });\n\n// =============================================================================\n// Subscription Store (for managing subscriptions at the gateway level)\n// =============================================================================\n\n/**\n * Store for managing document subscriptions\n * This is needed because cluster entities don't support streaming directly\n */\ninterface SubscriptionStore {\n readonly getOrCreatePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;\n readonly getOrCreatePresencePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;\n}\n\nclass SubscriptionStoreTag extends Context.Tag(\n \"@voidhash/mimic-effect/SubscriptionStore\"\n)<SubscriptionStoreTag, SubscriptionStore>() {}\n\nconst subscriptionStoreLayer = Layer.effect(\n SubscriptionStoreTag,\n Effect.gen(function* () {\n const documentPubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()\n );\n const presencePubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()\n );\n\n return {\n getOrCreatePubSub: (documentId: string) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(documentPubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();\n yield* Ref.update(documentPubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n\n getOrCreatePresencePubSub: (documentId: string) =>\n Effect.gen(function* () {\n const current = yield* Ref.get(presencePubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n yield* Ref.update(presencePubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n };\n })\n);\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicClusterServerEngine layer.\n *\n * This creates a clustered document management service using Effect Cluster.\n * Each document becomes a cluster Entity with automatic sharding and failover.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicClusterServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * shardGroup: \"documents\",\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together with cluster infrastructure\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.S3.make(...)),\n * Layer.provide(HotStorage.Redis.make(...)),\n * Layer.provide(MimicAuthService.make(...)),\n * Layer.provide(ClusterInfrastructure),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding\n> => {\n const resolvedConfig = resolveClusterConfig(config);\n\n // Create config layer\n const configLayer = Layer.succeed(\n MimicClusterConfigTag,\n resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>\n );\n\n // Create entity layer that registers with sharding\n const entityLayer = MimicDocumentEntity.toLayer(\n Effect.gen(function* () {\n const clusterConfig = yield* MimicClusterConfigTag;\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n\n return yield* createEntityHandler(\n clusterConfig as ResolvedClusterConfig<TSchema>,\n coldStorage,\n hotStorage\n );\n }),\n {\n maxIdleTime: resolvedConfig.maxIdleTime,\n concurrency: 1, // Sequential message processing per document\n mailboxCapacity: 4096,\n }\n );\n\n // Create the engine service\n const engineLayer = Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n // Get entity client maker\n const makeClient = yield* MimicDocumentEntity.client;\n\n // Get subscription store\n const subscriptionStore = yield* SubscriptionStoreTag;\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n const encodedTx = encodeTransaction(transaction);\n const result = yield* client.Submit({\n transaction: encodedTx as { id: string; ops: unknown[] },\n }).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Cluster error: ${String(error)}`,\n })\n )\n );\n\n // Broadcast to local subscribers if success\n if (result.success) {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction,\n version: result.version,\n } as Protocol.ServerMessage);\n }\n\n return result;\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.Touch(undefined as void).pipe(Effect.orDie);\n }),\n\n getPresenceSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n setPresence: (documentId, connectionId, entry) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n });\n }),\n\n removePresence: (documentId, connectionId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_remove\",\n id: connectionId,\n });\n }),\n\n subscribePresence: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n );\n\n // Compose all layers\n return Layer.mergeAll(entityLayer, engineLayer).pipe(\n Layer.provideMerge(subscriptionStoreLayer),\n Layer.provideMerge(configLayer)\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicClusterServerEngine = {\n make,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MimicClusterServerEngine.mjs","names":["Metrics.documentsActive","Metrics.documentsEvicted","Metrics.storageIdleSnapshots","Metrics.presenceUpdates","Metrics.presenceActive","event: PresenceEvent","presences: Record<string, PresenceEntry>"],"sources":["../src/MimicClusterServerEngine.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicClusterServerEngine\n *\n * Clustered document management service using Effect Cluster for horizontal scaling.\n * Each document becomes a cluster Entity with automatic sharding, failover, and location-transparent routing.\n *\n * This is an alternative to MimicServerEngine for distributed deployments.\n */\nimport {\n Context,\n Duration,\n Effect,\n HashMap,\n Layer,\n Metric,\n PubSub,\n Ref,\n Schedule,\n Schema,\n Stream,\n} from \"effect\";\nimport { Entity, Sharding } from \"@effect/cluster\";\nimport { Rpc } from \"@effect/rpc\";\nimport { type Primitive, type Transaction } from \"@voidhash/mimic\";\nimport type {\n MimicClusterServerEngineConfig,\n PresenceEntry,\n PresenceEvent,\n ResolvedClusterConfig,\n} from \"./Types\";\nimport type * as Protocol from \"./Protocol\";\nimport { ColdStorageTag, type ColdStorage } from \"./ColdStorage\";\nimport { HotStorageTag, type HotStorage } from \"./HotStorage\";\nimport { MimicAuthServiceTag } from \"./MimicAuthService\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport {\n DocumentInstance,\n type DocumentInstance as DocumentInstanceInterface,\n} from \"./DocumentInstance\";\nimport * as Metrics from \"./Metrics\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_MAX_IDLE_TIME = Duration.minutes(5);\nconst DEFAULT_MAX_TRANSACTION_HISTORY = 1000;\nconst DEFAULT_SNAPSHOT_INTERVAL = Duration.minutes(5);\nconst DEFAULT_SNAPSHOT_THRESHOLD = 100;\nconst DEFAULT_SNAPSHOT_IDLE_TIMEOUT = Duration.seconds(30);\nconst DEFAULT_SHARD_GROUP = \"mimic-documents\";\n\n// =============================================================================\n// RPC Schemas\n// =============================================================================\n\n/**\n * Schema for encoded transaction (wire format)\n */\nconst EncodedTransactionSchema = Schema.Struct({\n id: Schema.String,\n ops: Schema.Array(Schema.Unknown),\n});\n\n/**\n * Schema for submit result\n */\nconst SubmitResultSchema = Schema.Union(\n Schema.Struct({\n success: Schema.Literal(true),\n version: Schema.Number,\n }),\n Schema.Struct({\n success: Schema.Literal(false),\n reason: Schema.String,\n })\n);\n\n/**\n * Schema for snapshot response\n */\nconst SnapshotResponseSchema = Schema.Struct({\n state: Schema.Unknown,\n version: Schema.Number,\n});\n\n/**\n * Schema for presence entry\n */\nconst PresenceEntrySchema = Schema.Struct({\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n});\n\n/**\n * Schema for presence snapshot response\n */\nconst PresenceSnapshotResponseSchema = Schema.Struct({\n presences: Schema.Record({ key: Schema.String, value: PresenceEntrySchema }),\n});\n\n/**\n * Schema for presence event\n */\nconst PresenceEventSchema = Schema.Union(\n Schema.Struct({\n type: Schema.Literal(\"presence_update\"),\n id: Schema.String,\n data: Schema.Unknown,\n userId: Schema.optional(Schema.String),\n }),\n Schema.Struct({\n type: Schema.Literal(\"presence_remove\"),\n id: Schema.String,\n })\n);\n\n/**\n * Schema for server message (for broadcasts)\n */\nconst ServerMessageSchema = Schema.Unknown;\n\n// =============================================================================\n// Mimic Document Entity Definition\n// =============================================================================\n\n/**\n * Define the Mimic Document Entity with its RPC protocol.\n * This entity handles document operations for a single documentId.\n */\nconst MimicDocumentEntity = Entity.make(\"MimicDocument\", [\n // Submit a transaction\n Rpc.make(\"Submit\", {\n payload: { transaction: EncodedTransactionSchema },\n success: SubmitResultSchema,\n }),\n\n // Get document snapshot\n Rpc.make(\"GetSnapshot\", {\n success: SnapshotResponseSchema,\n }),\n\n // Touch document to prevent idle GC\n Rpc.make(\"Touch\", {\n success: Schema.Void,\n }),\n\n // Set presence for a connection\n Rpc.make(\"SetPresence\", {\n payload: {\n connectionId: Schema.String,\n entry: PresenceEntrySchema,\n },\n success: Schema.Void,\n }),\n\n // Remove presence for a connection\n Rpc.make(\"RemovePresence\", {\n payload: { connectionId: Schema.String },\n success: Schema.Void,\n }),\n\n // Get presence snapshot\n Rpc.make(\"GetPresenceSnapshot\", {\n success: PresenceSnapshotResponseSchema,\n }),\n]);\n\n// =============================================================================\n// Entity State Types\n// =============================================================================\n\n/**\n * Entity state that wraps DocumentInstance and adds presence management\n */\ninterface EntityState<TSchema extends Primitive.AnyPrimitive> {\n readonly instance: DocumentInstanceInterface<TSchema>;\n readonly presences: HashMap.HashMap<string, PresenceEntry>;\n readonly presencePubSub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Config Context Tag\n// =============================================================================\n\n/**\n * Context tag for cluster engine configuration\n */\nclass MimicClusterConfigTag extends Context.Tag(\n \"@voidhash/mimic-effect/MimicClusterConfig\"\n)<MimicClusterConfigTag, ResolvedClusterConfig<Primitive.AnyPrimitive>>() {}\n\n// =============================================================================\n// Resolve Configuration\n// =============================================================================\n\nconst resolveClusterConfig = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): ResolvedClusterConfig<TSchema> => ({\n schema: config.schema,\n initial: config.initial,\n presence: config.presence,\n maxIdleTime: config.maxIdleTime\n ? Duration.decode(config.maxIdleTime)\n : DEFAULT_MAX_IDLE_TIME,\n maxTransactionHistory:\n config.maxTransactionHistory ?? DEFAULT_MAX_TRANSACTION_HISTORY,\n snapshot: {\n interval: config.snapshot?.interval\n ? Duration.decode(config.snapshot.interval)\n : DEFAULT_SNAPSHOT_INTERVAL,\n transactionThreshold:\n config.snapshot?.transactionThreshold ?? DEFAULT_SNAPSHOT_THRESHOLD,\n idleTimeout: config.snapshot?.idleTimeout\n ? Duration.decode(config.snapshot.idleTimeout)\n : DEFAULT_SNAPSHOT_IDLE_TIMEOUT,\n },\n shardGroup: config.shardGroup ?? DEFAULT_SHARD_GROUP,\n});\n\n// =============================================================================\n// Helper to decode/encode transactions\n// =============================================================================\n\n/**\n * Decode an encoded transaction to a Transaction object\n */\nconst decodeTransaction = (\n encoded: { id: string; ops: readonly unknown[] }\n): Transaction.Transaction => {\n // Import Transaction dynamically to avoid circular deps\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.decode(encoded as Transaction.EncodedTransaction);\n};\n\n/**\n * Encode a Transaction to wire format\n */\nconst encodeTransaction = (\n tx: Transaction.Transaction\n): { id: string; ops: readonly unknown[] } => {\n const { Transaction } = require(\"@voidhash/mimic\");\n return Transaction.encode(tx);\n};\n\n// =============================================================================\n// Entity Handler Factory\n// =============================================================================\n\n/**\n * Create the entity handler for MimicDocument\n */\nconst createEntityHandler = <TSchema extends Primitive.AnyPrimitive>(\n config: ResolvedClusterConfig<TSchema>,\n coldStorage: ColdStorage,\n hotStorage: HotStorage\n) =>\n Effect.fn(\"cluster.entity.handler.create\")(function* () {\n // Get entity address to determine documentId\n const address = yield* Entity.CurrentAddress;\n const documentId = address.entityId;\n\n // Create DocumentInstance (fatal if unavailable - entity cannot start)\n const instance = yield* DocumentInstance.make(\n documentId,\n {\n schema: config.schema,\n initial: config.initial,\n maxTransactionHistory: config.maxTransactionHistory,\n snapshot: config.snapshot,\n },\n coldStorage,\n hotStorage\n ).pipe(Effect.orDie);\n\n // Create presence PubSub and state ref\n const presencePubSub = yield* PubSub.unbounded<PresenceEvent>();\n const stateRef = yield* Ref.make<EntityState<TSchema>>({\n instance,\n presences: HashMap.empty(),\n presencePubSub,\n });\n\n // Cleanup on entity finalization\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"cluster.entity.finalize\")(function* () {\n // Best effort save - don't fail shutdown if storage is unavailable\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logError(\"Failed to save snapshot during entity finalization\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.incrementBy(Metrics.documentsActive, -1);\n yield* Metric.increment(Metrics.documentsEvicted);\n yield* Effect.logDebug(\"Entity finalized\", { documentId });\n })()\n );\n\n // Start periodic snapshot fiber for this entity\n const idleTimeoutMs = Duration.toMillis(config.snapshot.idleTimeout);\n if (idleTimeoutMs > 0) {\n const snapshotLoop = Effect.fn(\"cluster.entity.snapshot.loop\")(function* () {\n const needs = yield* instance.needsSnapshot();\n if (needs) {\n yield* Effect.catchAll(instance.saveSnapshot(), (e) =>\n Effect.logWarning(\"Periodic snapshot failed in cluster entity\", {\n documentId,\n error: e,\n })\n );\n yield* Metric.increment(Metrics.storageIdleSnapshots);\n }\n });\n\n // Run every idleTimeout\n yield* snapshotLoop().pipe(\n Effect.repeat(Schedule.spaced(config.snapshot.idleTimeout)),\n Effect.fork\n );\n }\n\n // Return RPC handlers\n return {\n Submit: Effect.fn(\"cluster.document.transaction.submit\")(function* ({\n payload,\n }) {\n // Decode transaction\n const transaction = decodeTransaction(payload.transaction);\n\n // Use DocumentInstance's submit method, catching storage errors\n return yield* instance.submit(transaction).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Storage error: ${String(error)}`,\n })\n )\n );\n }),\n\n GetSnapshot: Effect.fn(\"cluster.document.snapshot.get\")(function* () {\n return instance.getSnapshot();\n }),\n\n Touch: Effect.fn(\"cluster.document.touch\")(function* () {\n yield* instance.touch();\n }),\n\n SetPresence: Effect.fn(\"cluster.presence.set\")(function* ({ payload }) {\n const { connectionId, entry } = payload;\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.set(s.presences, connectionId, entry),\n }));\n\n yield* Metric.increment(Metrics.presenceUpdates);\n yield* Metric.incrementBy(Metrics.presenceActive, 1);\n\n const state = yield* Ref.get(stateRef);\n const event: PresenceEvent = {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n RemovePresence: Effect.fn(\"cluster.presence.remove\")(function* ({\n payload,\n }) {\n const { connectionId } = payload;\n const state = yield* Ref.get(stateRef);\n\n if (!HashMap.has(state.presences, connectionId)) {\n return;\n }\n\n yield* Ref.update(stateRef, (s) => ({\n ...s,\n presences: HashMap.remove(s.presences, connectionId),\n }));\n\n yield* Metric.incrementBy(Metrics.presenceActive, -1);\n\n const event: PresenceEvent = {\n type: \"presence_remove\",\n id: connectionId,\n };\n yield* PubSub.publish(state.presencePubSub, event);\n }),\n\n GetPresenceSnapshot: Effect.fn(\"cluster.presence.snapshot.get\")(\n function* () {\n const state = yield* Ref.get(stateRef);\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of state.presences) {\n presences[id] = entry;\n }\n return { presences };\n }\n ),\n };\n })();\n\n// =============================================================================\n// Subscription Store (for managing subscriptions at the gateway level)\n// =============================================================================\n\n/**\n * Store for managing document subscriptions\n * This is needed because cluster entities don't support streaming directly\n */\ninterface SubscriptionStore {\n readonly getOrCreatePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<Protocol.ServerMessage>>;\n readonly getOrCreatePresencePubSub: (\n documentId: string\n ) => Effect.Effect<PubSub.PubSub<PresenceEvent>>;\n}\n\nclass SubscriptionStoreTag extends Context.Tag(\n \"@voidhash/mimic-effect/SubscriptionStore\"\n)<SubscriptionStoreTag, SubscriptionStore>() {}\n\nconst subscriptionStoreLayer = Layer.effect(\n SubscriptionStoreTag,\n Effect.fn(\"cluster.subscriptions.layer.create\")(function* () {\n const documentPubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<Protocol.ServerMessage>>()\n );\n const presencePubSubs = yield* Ref.make(\n HashMap.empty<string, PubSub.PubSub<PresenceEvent>>()\n );\n\n return {\n getOrCreatePubSub: Effect.fn(\n \"cluster.subscriptions.pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(documentPubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<Protocol.ServerMessage>();\n yield* Ref.update(documentPubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n\n getOrCreatePresencePubSub: Effect.fn(\n \"cluster.subscriptions.presence-pubsub.get-or-create\"\n )(function* (documentId: string) {\n const current = yield* Ref.get(presencePubSubs);\n const existing = HashMap.get(current, documentId);\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n yield* Ref.update(presencePubSubs, (map) =>\n HashMap.set(map, documentId, pubsub)\n );\n return pubsub;\n }),\n };\n })()\n);\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a MimicClusterServerEngine layer.\n *\n * This creates a clustered document management service using Effect Cluster.\n * Each document becomes a cluster Entity with automatic sharding and failover.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicClusterServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * presence: CursorPresence,\n * maxIdleTime: \"5 minutes\",\n * snapshot: { interval: \"5 minutes\", transactionThreshold: 100 },\n * shardGroup: \"documents\",\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together with cluster infrastructure\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.S3.make(...)),\n * Layer.provide(HotStorage.Redis.make(...)),\n * Layer.provide(MimicAuthService.make(...)),\n * Layer.provide(ClusterInfrastructure),\n * )\n * ```\n */\nexport const make = <TSchema extends Primitive.AnyPrimitive>(\n config: MimicClusterServerEngineConfig<TSchema>\n): Layer.Layer<\n MimicServerEngineTag,\n never,\n ColdStorageTag | HotStorageTag | MimicAuthServiceTag | Sharding.Sharding\n> => {\n const resolvedConfig = resolveClusterConfig(config);\n\n // Create config layer\n const configLayer = Layer.succeed(\n MimicClusterConfigTag,\n resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>\n );\n\n // Create entity layer that registers with sharding\n const entityLayer = MimicDocumentEntity.toLayer(\n Effect.gen(function* () {\n const clusterConfig = yield* MimicClusterConfigTag;\n const coldStorage = yield* ColdStorageTag;\n const hotStorage = yield* HotStorageTag;\n\n return yield* createEntityHandler(\n clusterConfig as ResolvedClusterConfig<TSchema>,\n coldStorage,\n hotStorage\n );\n }),\n {\n maxIdleTime: resolvedConfig.maxIdleTime,\n concurrency: 1, // Sequential message processing per document\n mailboxCapacity: 4096,\n }\n );\n\n // Create the engine service\n const engineLayer = Layer.scoped(\n MimicServerEngineTag,\n Effect.gen(function* () {\n // Get entity client maker\n const makeClient = yield* MimicDocumentEntity.client;\n\n // Get subscription store\n const subscriptionStore = yield* SubscriptionStoreTag;\n\n const engine: MimicServerEngine = {\n submit: (documentId, transaction) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n const encodedTx = encodeTransaction(transaction);\n const result = yield* client.Submit({\n transaction: encodedTx as { id: string; ops: unknown[] },\n }).pipe(\n Effect.catchAll((error) =>\n Effect.succeed({\n success: false as const,\n reason: `Cluster error: ${String(error)}`,\n })\n )\n );\n\n // Broadcast to local subscribers if success\n if (result.success) {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"transaction\",\n transaction,\n version: result.version,\n } as Protocol.ServerMessage);\n }\n\n return result;\n }),\n\n getSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n subscribe: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n touch: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.Touch(undefined as void).pipe(Effect.orDie);\n }),\n\n getPresenceSnapshot: (documentId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n return yield* client.GetPresenceSnapshot(undefined as void).pipe(Effect.orDie);\n }),\n\n setPresence: (documentId, connectionId, entry) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.SetPresence({ connectionId, entry }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n });\n }),\n\n removePresence: (documentId, connectionId) =>\n Effect.gen(function* () {\n const client = makeClient(documentId);\n yield* client.RemovePresence({ connectionId }).pipe(Effect.orDie);\n\n // Broadcast to local presence subscribers\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n yield* PubSub.publish(pubsub, {\n type: \"presence_remove\",\n id: connectionId,\n });\n }),\n\n subscribePresence: (documentId) =>\n Effect.gen(function* () {\n const pubsub =\n yield* subscriptionStore.getOrCreatePresencePubSub(documentId);\n return Stream.fromPubSub(pubsub);\n }),\n\n config: resolvedConfig as ResolvedClusterConfig<Primitive.AnyPrimitive>,\n };\n\n return engine;\n })\n );\n\n // Compose all layers\n return Layer.mergeAll(entityLayer, engineLayer).pipe(\n Layer.provideMerge(subscriptionStoreLayer),\n Layer.provideMerge(configLayer)\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicClusterServerEngine = {\n make,\n};\n"],"mappings":"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"}
|
package/dist/MimicServer.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
const require_Errors = require('./Errors.cjs');
|
|
1
2
|
const require_Metrics = require('./Metrics.cjs');
|
|
2
3
|
const require_MimicServerEngine = require('./MimicServerEngine.cjs');
|
|
3
|
-
const require_Errors = require('./Errors.cjs');
|
|
4
4
|
const require_Protocol = require('./Protocol.cjs');
|
|
5
5
|
const require_MimicAuthService = require('./MimicAuthService.cjs');
|
|
6
6
|
let effect = require("effect");
|
|
@@ -42,7 +42,7 @@ const extractDocumentId = (path) => {
|
|
|
42
42
|
/**
|
|
43
43
|
* Handle a WebSocket connection for a document.
|
|
44
44
|
*/
|
|
45
|
-
const handleWebSocketConnection = (socket, documentId, engine, authService, _routeConfig)
|
|
45
|
+
const handleWebSocketConnection = effect.Effect.fn("websocket.connection.handle")(function* (socket, documentId, engine, authService, _routeConfig) {
|
|
46
46
|
const connectionId = crypto.randomUUID();
|
|
47
47
|
const connectionStartTime = Date.now();
|
|
48
48
|
yield* effect.Metric.increment(require_Metrics.connectionsTotal);
|
|
@@ -55,30 +55,30 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
55
55
|
};
|
|
56
56
|
const write = yield* socket.writer;
|
|
57
57
|
const sendMessage = (message) => write(require_Protocol.encodeServerMessage(message));
|
|
58
|
-
const sendPresenceSnapshot = effect.Effect.
|
|
58
|
+
const sendPresenceSnapshot = effect.Effect.fn("presence.snapshot.send")(function* () {
|
|
59
59
|
if (!engine.config.presence) return;
|
|
60
60
|
const snapshot = yield* engine.getPresenceSnapshot(documentId);
|
|
61
61
|
yield* sendMessage(require_Protocol.presenceSnapshotMessage(connectionId, snapshot.presences));
|
|
62
62
|
});
|
|
63
|
-
const sendDocumentSnapshot = effect.Effect.
|
|
63
|
+
const sendDocumentSnapshot = effect.Effect.fn("document.snapshot.send")(function* () {
|
|
64
64
|
const snapshot = yield* engine.getSnapshot(documentId);
|
|
65
65
|
yield* sendMessage(require_Protocol.snapshotMessage(snapshot.state, snapshot.version));
|
|
66
66
|
});
|
|
67
|
-
const handleAuth =
|
|
67
|
+
const handleAuth = effect.Effect.fn("auth.handle")(function* (token) {
|
|
68
68
|
const result = yield* effect.Effect.either(authService.authenticate(token, documentId));
|
|
69
69
|
if (result._tag === "Right") {
|
|
70
70
|
state.authenticated = true;
|
|
71
71
|
state.authContext = result.right;
|
|
72
72
|
yield* sendMessage(require_Protocol.authResultSuccess(result.right.userId, result.right.permission));
|
|
73
|
-
yield* sendDocumentSnapshot;
|
|
74
|
-
yield* sendPresenceSnapshot;
|
|
73
|
+
yield* sendDocumentSnapshot();
|
|
74
|
+
yield* sendPresenceSnapshot();
|
|
75
75
|
} else {
|
|
76
76
|
var _result$left$reason;
|
|
77
77
|
yield* effect.Metric.increment(require_Metrics.connectionsErrors);
|
|
78
78
|
yield* sendMessage(require_Protocol.authResultFailure((_result$left$reason = result.left.reason) !== null && _result$left$reason !== void 0 ? _result$left$reason : "Authentication failed"));
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
|
-
const handlePresenceSet =
|
|
81
|
+
const handlePresenceSet = effect.Effect.fn("presence.set.handle")(function* (data) {
|
|
82
82
|
if (!state.authenticated) return;
|
|
83
83
|
if (!state.authContext) return;
|
|
84
84
|
if (!engine.config.presence) return;
|
|
@@ -100,13 +100,13 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
100
100
|
});
|
|
101
101
|
state.hasPresence = true;
|
|
102
102
|
});
|
|
103
|
-
const handlePresenceClear = effect.Effect.
|
|
103
|
+
const handlePresenceClear = effect.Effect.fn("presence.clear.handle")(function* () {
|
|
104
104
|
if (!state.authenticated) return;
|
|
105
105
|
if (!engine.config.presence) return;
|
|
106
106
|
yield* engine.removePresence(documentId, connectionId);
|
|
107
107
|
state.hasPresence = false;
|
|
108
108
|
});
|
|
109
|
-
const handleMessage =
|
|
109
|
+
const handleMessage = effect.Effect.fn("message.handle")(function* (message) {
|
|
110
110
|
yield* engine.touch(documentId);
|
|
111
111
|
switch (message.type) {
|
|
112
112
|
case "auth":
|
|
@@ -137,16 +137,16 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
137
137
|
yield* handlePresenceSet(message.data);
|
|
138
138
|
break;
|
|
139
139
|
case "presence_clear":
|
|
140
|
-
yield* handlePresenceClear;
|
|
140
|
+
yield* handlePresenceClear();
|
|
141
141
|
break;
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
|
-
const subscribeFiber = yield* effect.Effect.fork(effect.Effect.
|
|
144
|
+
const subscribeFiber = yield* effect.Effect.fork(effect.Effect.fn("subscriptions.document.start")(function* () {
|
|
145
145
|
while (!state.authenticated) yield* effect.Effect.sleep(effect.Duration.millis(100));
|
|
146
146
|
const broadcastStream = yield* engine.subscribe(documentId);
|
|
147
147
|
yield* effect.Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
|
|
148
|
-
}).pipe(effect.Effect.scoped));
|
|
149
|
-
const presenceFiber = yield* effect.Effect.fork(effect.Effect.
|
|
148
|
+
})().pipe(effect.Effect.scoped));
|
|
149
|
+
const presenceFiber = yield* effect.Effect.fork(effect.Effect.fn("subscriptions.presence.start")(function* () {
|
|
150
150
|
if (!engine.config.presence) return;
|
|
151
151
|
while (!state.authenticated) yield* effect.Effect.sleep(effect.Duration.millis(100));
|
|
152
152
|
const presenceStream = yield* engine.subscribePresence(documentId);
|
|
@@ -155,8 +155,8 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
155
155
|
if (event.type === "presence_update") yield* sendMessage(require_Protocol.presenceUpdateMessage(event.id, event.data, event.userId));
|
|
156
156
|
else if (event.type === "presence_remove") yield* sendMessage(require_Protocol.presenceRemoveMessage(event.id));
|
|
157
157
|
}));
|
|
158
|
-
}).pipe(effect.Effect.scoped));
|
|
159
|
-
yield* effect.Effect.addFinalizer(() => effect.Effect.
|
|
158
|
+
})().pipe(effect.Effect.scoped));
|
|
159
|
+
yield* effect.Effect.addFinalizer(() => effect.Effect.fn("connection.cleanup")(function* () {
|
|
160
160
|
const duration = Date.now() - connectionStartTime;
|
|
161
161
|
yield* effect.Fiber.interrupt(subscribeFiber);
|
|
162
162
|
yield* effect.Fiber.interrupt(presenceFiber);
|
|
@@ -168,10 +168,10 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
168
168
|
documentId,
|
|
169
169
|
durationMs: duration
|
|
170
170
|
});
|
|
171
|
-
}));
|
|
172
|
-
yield* socket.runRaw((data) => effect.Effect.
|
|
171
|
+
})());
|
|
172
|
+
yield* socket.runRaw((data) => effect.Effect.fn("message.process")(function* () {
|
|
173
173
|
yield* handleMessage(yield* require_Protocol.parseClientMessage(data));
|
|
174
|
-
}).pipe(effect.Effect.catchAll((error) => effect.Effect.logError("Message handling error", error))));
|
|
174
|
+
})().pipe(effect.Effect.catchAll((error) => effect.Effect.logError("Message handling error", error))));
|
|
175
175
|
});
|
|
176
176
|
/**
|
|
177
177
|
* Create a route layer for MimicServerEngine.
|
|
@@ -218,7 +218,7 @@ const layerHttpLayerRouter = (options) => {
|
|
|
218
218
|
const router = yield* _effect_platform.HttpLayerRouter.HttpRouter;
|
|
219
219
|
const engine = yield* require_MimicServerEngine.MimicServerEngineTag;
|
|
220
220
|
const authService = yield* require_MimicAuthService.MimicAuthServiceTag;
|
|
221
|
-
const handler =
|
|
221
|
+
const handler = effect.Effect.fn("websocket.route.handler")(function* (request) {
|
|
222
222
|
const documentIdResult = yield* effect.Effect.either(extractDocumentId(request.url));
|
|
223
223
|
if (documentIdResult._tag === "Left") return _effect_platform.HttpServerResponse.text(`Missing document ID in path: ${request.url}`, { status: 400 });
|
|
224
224
|
const documentId = documentIdResult.right;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"MimicServer.d.cts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cA2ea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,eAAA,CAAA,aAAA,eAAA,CAAA,iBAAA,iCAAA,CAAA,YAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"MimicServer.d.mts","names":[],"sources":["../src/MimicServer.ts"],"sourcesContent":[],"mappings":";;;;;;;;;cA2ea;mCA9DD,2BAAsB,KAAA,CAAA,oBAAA,uBAAA,sBAAA,eAAA,CAAA,aAAA,eAAA,CAAA,iBAAA,iCAAA,CAAA,YAAA"}
|
package/dist/MimicServer.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { MissingDocumentIdError } from "./Errors.mjs";
|
|
1
2
|
import { connectionsActive, connectionsDuration, connectionsErrors, connectionsTotal } from "./Metrics.mjs";
|
|
2
3
|
import { MimicServerEngineTag } from "./MimicServerEngine.mjs";
|
|
3
|
-
import { MissingDocumentIdError } from "./Errors.mjs";
|
|
4
4
|
import { authResultFailure, authResultSuccess, encodeServerMessage, errorMessage, parseClientMessage, pong, presenceRemoveMessage, presenceSnapshotMessage, presenceUpdateMessage, snapshotMessage } from "./Protocol.mjs";
|
|
5
5
|
import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
|
|
6
6
|
import { Duration, Effect, Fiber, Layer, Metric, Stream } from "effect";
|
|
@@ -42,7 +42,7 @@ const extractDocumentId = (path) => {
|
|
|
42
42
|
/**
|
|
43
43
|
* Handle a WebSocket connection for a document.
|
|
44
44
|
*/
|
|
45
|
-
const handleWebSocketConnection = (socket, documentId, engine, authService, _routeConfig)
|
|
45
|
+
const handleWebSocketConnection = Effect.fn("websocket.connection.handle")(function* (socket, documentId, engine, authService, _routeConfig) {
|
|
46
46
|
const connectionId = crypto.randomUUID();
|
|
47
47
|
const connectionStartTime = Date.now();
|
|
48
48
|
yield* Metric.increment(connectionsTotal);
|
|
@@ -55,30 +55,30 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
55
55
|
};
|
|
56
56
|
const write = yield* socket.writer;
|
|
57
57
|
const sendMessage = (message) => write(encodeServerMessage(message));
|
|
58
|
-
const sendPresenceSnapshot = Effect.
|
|
58
|
+
const sendPresenceSnapshot = Effect.fn("presence.snapshot.send")(function* () {
|
|
59
59
|
if (!engine.config.presence) return;
|
|
60
60
|
const snapshot = yield* engine.getPresenceSnapshot(documentId);
|
|
61
61
|
yield* sendMessage(presenceSnapshotMessage(connectionId, snapshot.presences));
|
|
62
62
|
});
|
|
63
|
-
const sendDocumentSnapshot = Effect.
|
|
63
|
+
const sendDocumentSnapshot = Effect.fn("document.snapshot.send")(function* () {
|
|
64
64
|
const snapshot = yield* engine.getSnapshot(documentId);
|
|
65
65
|
yield* sendMessage(snapshotMessage(snapshot.state, snapshot.version));
|
|
66
66
|
});
|
|
67
|
-
const handleAuth =
|
|
67
|
+
const handleAuth = Effect.fn("auth.handle")(function* (token) {
|
|
68
68
|
const result = yield* Effect.either(authService.authenticate(token, documentId));
|
|
69
69
|
if (result._tag === "Right") {
|
|
70
70
|
state.authenticated = true;
|
|
71
71
|
state.authContext = result.right;
|
|
72
72
|
yield* sendMessage(authResultSuccess(result.right.userId, result.right.permission));
|
|
73
|
-
yield* sendDocumentSnapshot;
|
|
74
|
-
yield* sendPresenceSnapshot;
|
|
73
|
+
yield* sendDocumentSnapshot();
|
|
74
|
+
yield* sendPresenceSnapshot();
|
|
75
75
|
} else {
|
|
76
76
|
var _result$left$reason;
|
|
77
77
|
yield* Metric.increment(connectionsErrors);
|
|
78
78
|
yield* sendMessage(authResultFailure((_result$left$reason = result.left.reason) !== null && _result$left$reason !== void 0 ? _result$left$reason : "Authentication failed"));
|
|
79
79
|
}
|
|
80
80
|
});
|
|
81
|
-
const handlePresenceSet =
|
|
81
|
+
const handlePresenceSet = Effect.fn("presence.set.handle")(function* (data) {
|
|
82
82
|
if (!state.authenticated) return;
|
|
83
83
|
if (!state.authContext) return;
|
|
84
84
|
if (!engine.config.presence) return;
|
|
@@ -100,13 +100,13 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
100
100
|
});
|
|
101
101
|
state.hasPresence = true;
|
|
102
102
|
});
|
|
103
|
-
const handlePresenceClear = Effect.
|
|
103
|
+
const handlePresenceClear = Effect.fn("presence.clear.handle")(function* () {
|
|
104
104
|
if (!state.authenticated) return;
|
|
105
105
|
if (!engine.config.presence) return;
|
|
106
106
|
yield* engine.removePresence(documentId, connectionId);
|
|
107
107
|
state.hasPresence = false;
|
|
108
108
|
});
|
|
109
|
-
const handleMessage = (message)
|
|
109
|
+
const handleMessage = Effect.fn("message.handle")(function* (message) {
|
|
110
110
|
yield* engine.touch(documentId);
|
|
111
111
|
switch (message.type) {
|
|
112
112
|
case "auth":
|
|
@@ -137,16 +137,16 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
137
137
|
yield* handlePresenceSet(message.data);
|
|
138
138
|
break;
|
|
139
139
|
case "presence_clear":
|
|
140
|
-
yield* handlePresenceClear;
|
|
140
|
+
yield* handlePresenceClear();
|
|
141
141
|
break;
|
|
142
142
|
}
|
|
143
143
|
});
|
|
144
|
-
const subscribeFiber = yield* Effect.fork(Effect.
|
|
144
|
+
const subscribeFiber = yield* Effect.fork(Effect.fn("subscriptions.document.start")(function* () {
|
|
145
145
|
while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
|
|
146
146
|
const broadcastStream = yield* engine.subscribe(documentId);
|
|
147
147
|
yield* Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
|
|
148
|
-
}).pipe(Effect.scoped));
|
|
149
|
-
const presenceFiber = yield* Effect.fork(Effect.
|
|
148
|
+
})().pipe(Effect.scoped));
|
|
149
|
+
const presenceFiber = yield* Effect.fork(Effect.fn("subscriptions.presence.start")(function* () {
|
|
150
150
|
if (!engine.config.presence) return;
|
|
151
151
|
while (!state.authenticated) yield* Effect.sleep(Duration.millis(100));
|
|
152
152
|
const presenceStream = yield* engine.subscribePresence(documentId);
|
|
@@ -155,8 +155,8 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
155
155
|
if (event.type === "presence_update") yield* sendMessage(presenceUpdateMessage(event.id, event.data, event.userId));
|
|
156
156
|
else if (event.type === "presence_remove") yield* sendMessage(presenceRemoveMessage(event.id));
|
|
157
157
|
}));
|
|
158
|
-
}).pipe(Effect.scoped));
|
|
159
|
-
yield* Effect.addFinalizer(() => Effect.
|
|
158
|
+
})().pipe(Effect.scoped));
|
|
159
|
+
yield* Effect.addFinalizer(() => Effect.fn("connection.cleanup")(function* () {
|
|
160
160
|
const duration = Date.now() - connectionStartTime;
|
|
161
161
|
yield* Fiber.interrupt(subscribeFiber);
|
|
162
162
|
yield* Fiber.interrupt(presenceFiber);
|
|
@@ -168,10 +168,10 @@ const handleWebSocketConnection = (socket, documentId, engine, authService, _rou
|
|
|
168
168
|
documentId,
|
|
169
169
|
durationMs: duration
|
|
170
170
|
});
|
|
171
|
-
}));
|
|
172
|
-
yield* socket.runRaw((data) => Effect.
|
|
171
|
+
})());
|
|
172
|
+
yield* socket.runRaw((data) => Effect.fn("message.process")(function* () {
|
|
173
173
|
yield* handleMessage(yield* parseClientMessage(data));
|
|
174
|
-
}).pipe(Effect.catchAll((error) => Effect.logError("Message handling error", error))));
|
|
174
|
+
})().pipe(Effect.catchAll((error) => Effect.logError("Message handling error", error))));
|
|
175
175
|
});
|
|
176
176
|
/**
|
|
177
177
|
* Create a route layer for MimicServerEngine.
|
|
@@ -218,7 +218,7 @@ const layerHttpLayerRouter = (options) => {
|
|
|
218
218
|
const router = yield* HttpLayerRouter.HttpRouter;
|
|
219
219
|
const engine = yield* MimicServerEngineTag;
|
|
220
220
|
const authService = yield* MimicAuthServiceTag;
|
|
221
|
-
const handler =
|
|
221
|
+
const handler = Effect.fn("websocket.route.handler")(function* (request) {
|
|
222
222
|
const documentIdResult = yield* Effect.either(extractDocumentId(request.url));
|
|
223
223
|
if (documentIdResult._tag === "Left") return HttpServerResponse.text(`Missing document ID in path: ${request.url}`, { status: 400 });
|
|
224
224
|
const documentId = documentIdResult.right;
|
package/dist/MimicServer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MimicServer.mjs","names":["Metrics.connectionsTotal","Metrics.connectionsActive","state: ConnectionState","Protocol.encodeServerMessage","Protocol.presenceSnapshotMessage","Protocol.snapshotMessage","Protocol.authResultSuccess","Metrics.connectionsErrors","Protocol.authResultFailure","Protocol.pong","Protocol.errorMessage","Protocol.presenceUpdateMessage","Protocol.presenceRemoveMessage","Metrics.connectionsDuration","Protocol.parseClientMessage"],"sources":["../src/MimicServer.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServer\n *\n * WebSocket route layer for MimicServerEngine.\n * Creates routes compatible with HttpLayerRouter.\n */\nimport {\n Duration,\n Effect,\n Fiber,\n Layer,\n Metric,\n Scope,\n Stream,\n} from \"effect\";\nimport {\n HttpLayerRouter,\n HttpServerRequest,\n HttpServerResponse,\n} from \"@effect/platform\";\nimport type * as Socket from \"@effect/platform/Socket\";\nimport { Presence } from \"@voidhash/mimic\";\nimport type { MimicServerRouteConfig, ResolvedRouteConfig } from \"./Types\";\nimport * as Protocol from \"./Protocol\";\nimport { MissingDocumentIdError } from \"./Errors\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport { MimicAuthServiceTag, type MimicAuthService } from \"./MimicAuthService\";\nimport * as Metrics from \"./Metrics\";\nimport type { AuthContext } from \"./Types\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_PATH = \"/mimic\";\nconst DEFAULT_HEARTBEAT_INTERVAL = Duration.seconds(30);\nconst DEFAULT_HEARTBEAT_TIMEOUT = Duration.seconds(10);\n\n/**\n * Resolve route configuration with defaults\n */\nconst resolveRouteConfig = (\n config?: MimicServerRouteConfig\n): ResolvedRouteConfig => ({\n path: config?.path ?? DEFAULT_PATH,\n heartbeatInterval: config?.heartbeatInterval\n ? Duration.decode(config.heartbeatInterval)\n : DEFAULT_HEARTBEAT_INTERVAL,\n heartbeatTimeout: config?.heartbeatTimeout\n ? Duration.decode(config.heartbeatTimeout)\n : DEFAULT_HEARTBEAT_TIMEOUT,\n});\n\n// =============================================================================\n// URL Path Parsing\n// =============================================================================\n\n/**\n * Extract document ID from URL path.\n * Expected format: /basePath/doc/{documentId}\n */\nconst extractDocumentId = (\n path: string\n): Effect.Effect<string, MissingDocumentIdError> => {\n // Remove leading slash and split\n const parts = path.replace(/^\\/+/, \"\").split(\"/\");\n\n // Find the last occurrence of 'doc' in the path\n const docIndex = parts.lastIndexOf(\"doc\");\n const part = parts[docIndex + 1];\n if (docIndex !== -1 && part) {\n return Effect.succeed(decodeURIComponent(part));\n }\n return Effect.fail(new MissingDocumentIdError({ path }));\n};\n\n// =============================================================================\n// Connection State\n// =============================================================================\n\ninterface ConnectionState {\n readonly documentId: string;\n readonly connectionId: string;\n authenticated: boolean;\n authContext?: AuthContext;\n hasPresence: boolean;\n}\n\n// =============================================================================\n// WebSocket Connection Handler\n// =============================================================================\n\n/**\n * Handle a WebSocket connection for a document.\n */\nconst handleWebSocketConnection = (\n socket: Socket.Socket,\n documentId: string,\n engine: MimicServerEngine,\n authService: MimicAuthService,\n _routeConfig: ResolvedRouteConfig\n): Effect.Effect<void, Socket.SocketError, Scope.Scope> =>\n Effect.gen(function* () {\n const connectionId = crypto.randomUUID();\n const connectionStartTime = Date.now();\n\n // Track connection metrics\n yield* Metric.increment(Metrics.connectionsTotal);\n yield* Metric.incrementBy(Metrics.connectionsActive, 1);\n\n // Track connection state (mutable for simplicity)\n const state: ConnectionState = {\n documentId,\n connectionId,\n authenticated: false,\n hasPresence: false,\n };\n\n // Get the socket writer\n const write = yield* socket.writer;\n\n // Helper to send a message to the client\n const sendMessage = (message: Protocol.ServerMessage) =>\n write(Protocol.encodeServerMessage(message));\n\n // Send presence snapshot after auth\n const sendPresenceSnapshot = Effect.gen(function* () {\n if (!engine.config.presence) return;\n\n const snapshot = yield* engine.getPresenceSnapshot(documentId);\n yield* sendMessage(\n Protocol.presenceSnapshotMessage(connectionId, snapshot.presences)\n );\n });\n\n // Send document snapshot after auth\n const sendDocumentSnapshot = Effect.gen(function* () {\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n });\n\n // Handle authentication\n const handleAuth = (token: string) =>\n Effect.gen(function* () {\n const result = yield* Effect.either(\n authService.authenticate(token, documentId)\n );\n\n if (result._tag === \"Right\") {\n state.authenticated = true;\n state.authContext = result.right;\n\n yield* sendMessage(\n Protocol.authResultSuccess(\n result.right.userId,\n result.right.permission\n )\n );\n\n // Send document snapshot after successful auth\n yield* sendDocumentSnapshot;\n\n // Send presence snapshot after successful auth\n yield* sendPresenceSnapshot;\n } else {\n yield* Metric.increment(Metrics.connectionsErrors);\n yield* sendMessage(\n Protocol.authResultFailure(\n result.left.reason ?? \"Authentication failed\"\n )\n );\n }\n });\n\n // Handle presence set\n const handlePresenceSet = (data: unknown) =>\n Effect.gen(function* () {\n if (!state.authenticated) return;\n if (!state.authContext) return;\n if (!engine.config.presence) return;\n\n // Check write permission\n if (state.authContext.permission !== \"write\") {\n yield* Effect.logWarning(\"Presence set rejected - read-only user\", {\n connectionId,\n });\n return;\n }\n\n // Validate presence data against schema\n const validated = Presence.validateSafe(engine.config.presence, data);\n if (validated === undefined) {\n yield* Effect.logWarning(\"Invalid presence data received\", {\n connectionId,\n data,\n });\n return;\n }\n\n // Store in engine\n yield* engine.setPresence(documentId, connectionId, {\n data: validated,\n userId: state.authContext.userId,\n });\n\n state.hasPresence = true;\n });\n\n // Handle presence clear\n const handlePresenceClear = Effect.gen(function* () {\n if (!state.authenticated) return;\n if (!engine.config.presence) return;\n\n yield* engine.removePresence(documentId, connectionId);\n state.hasPresence = false;\n });\n\n // Handle a client message\n const handleMessage = (message: Protocol.ClientMessage) =>\n Effect.gen(function* () {\n // Touch document on any activity (prevents idle GC)\n yield* engine.touch(documentId);\n\n switch (message.type) {\n case \"auth\":\n yield* handleAuth(message.token);\n break;\n\n case \"ping\":\n yield* sendMessage(Protocol.pong());\n break;\n\n case \"submit\":\n if (!state.authenticated) {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Not authenticated\"\n )\n );\n return;\n }\n\n // Check write permission\n if (state.authContext?.permission !== \"write\") {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Write permission required\"\n )\n );\n return;\n }\n\n // Submit to the engine\n const submitResult = yield* engine.submit(\n documentId,\n message.transaction\n );\n\n // If rejected, send error (success is broadcast to all)\n if (!submitResult.success) {\n yield* sendMessage(\n Protocol.errorMessage(message.transaction.id, submitResult.reason)\n );\n }\n break;\n\n case \"request_snapshot\":\n if (!state.authenticated) {\n return;\n }\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n break;\n\n case \"presence_set\":\n yield* handlePresenceSet(message.data);\n break;\n\n case \"presence_clear\":\n yield* handlePresenceClear;\n break;\n }\n });\n\n // Subscribe to document broadcasts\n const subscribeFiber = yield* Effect.fork(\n Effect.gen(function* () {\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to the document\n const broadcastStream = yield* engine.subscribe(documentId);\n\n // Forward broadcasts to the WebSocket\n yield* Stream.runForEach(broadcastStream, (broadcast) =>\n sendMessage(broadcast as Protocol.ServerMessage)\n );\n }).pipe(Effect.scoped)\n );\n\n // Subscribe to presence events (if presence is enabled)\n const presenceFiber = yield* Effect.fork(\n Effect.gen(function* () {\n if (!engine.config.presence) return;\n\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to presence events\n const presenceStream = yield* engine.subscribePresence(documentId);\n\n // Forward presence events to the WebSocket, filtering out our own events (no-echo)\n yield* Stream.runForEach(presenceStream, (event) =>\n Effect.gen(function* () {\n // Don't echo our own presence events\n if (event.id === connectionId) return;\n\n if (event.type === \"presence_update\") {\n yield* sendMessage(\n Protocol.presenceUpdateMessage(event.id, event.data, event.userId)\n );\n } else if (event.type === \"presence_remove\") {\n yield* sendMessage(Protocol.presenceRemoveMessage(event.id));\n }\n })\n );\n }).pipe(Effect.scoped)\n );\n\n // Ensure cleanup on disconnect\n yield* Effect.addFinalizer(() =>\n Effect.gen(function* () {\n // Calculate connection duration\n const duration = Date.now() - connectionStartTime;\n\n // Interrupt the subscribe fibers\n yield* Fiber.interrupt(subscribeFiber);\n yield* Fiber.interrupt(presenceFiber);\n\n // Remove presence if we had any\n if (state.hasPresence && engine.config.presence) {\n yield* engine.removePresence(documentId, connectionId);\n }\n\n // Update connection metrics\n yield* Metric.incrementBy(Metrics.connectionsActive, -1);\n yield* Metric.update(Metrics.connectionsDuration, duration);\n\n yield* Effect.logDebug(\"WebSocket connection closed\", {\n connectionId,\n documentId,\n durationMs: duration,\n });\n })\n );\n\n // Process incoming messages\n yield* socket.runRaw((data) =>\n Effect.gen(function* () {\n const message = yield* Protocol.parseClientMessage(data);\n yield* handleMessage(message);\n }).pipe(\n Effect.catchAll((error) =>\n Effect.logError(\"Message handling error\", error)\n )\n )\n );\n });\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a route layer for MimicServerEngine.\n *\n * This creates a WebSocket route that connects to the engine.\n * Use Layer.mergeAll to compose with other routes.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.InMemory.make()),\n * Layer.provide(HotStorage.InMemory.make()),\n * Layer.provide(MimicAuthService.NoAuth.make()),\n * )\n *\n * // 4. Compose with other routes\n * const AllRoutes = Layer.mergeAll(MimicLive, DocsRoute, OtherRoutes)\n *\n * // 5. Serve\n * HttpLayerRouter.serve(AllRoutes).pipe(\n * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),\n * Layer.launch,\n * NodeRuntime.runMain\n * )\n * ```\n */\nexport const layerHttpLayerRouter = (\n options?: MimicServerRouteConfig\n) => {\n const routeConfig = resolveRouteConfig(options);\n\n // Build the route path pattern: {path}/doc/:documentId\n const routePath =\n `${routeConfig.path}/doc/:documentId` as HttpLayerRouter.PathInput;\n\n return Layer.scopedDiscard(\n Effect.gen(function* () {\n const router = yield* HttpLayerRouter.HttpRouter;\n // Capture engine and auth service at layer creation time\n const engine = yield* MimicServerEngineTag;\n const authService = yield* MimicAuthServiceTag;\n\n // Create the handler that receives the request\n // Engine and authService are captured in closure, not yielded per-request\n const handler = (request: HttpServerRequest.HttpServerRequest) =>\n Effect.gen(function* () {\n // Extract document ID from path\n const documentIdResult = yield* Effect.either(\n extractDocumentId(request.url)\n );\n if (documentIdResult._tag === \"Left\") {\n return HttpServerResponse.text(\n `Missing document ID in path: ${request.url}`,\n { status: 400 }\n );\n }\n const documentId = documentIdResult.right;\n\n // Upgrade to WebSocket\n const socket = yield* request.upgrade;\n\n // Handle the WebSocket connection\n yield* handleWebSocketConnection(\n socket,\n documentId,\n engine,\n authService,\n routeConfig\n ).pipe(\n Effect.scoped,\n Effect.catchAll((error) =>\n Effect.logError(\"WebSocket connection error\", error)\n )\n );\n\n // Return empty response - the WebSocket upgrade handles the connection\n return HttpServerResponse.empty();\n });\n\n yield* router.add(\"GET\", routePath, handler);\n })\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServer = {\n layerHttpLayerRouter,\n};\n\n// =============================================================================\n// Re-export types\n// =============================================================================\n\nexport type { MimicServerRouteConfig };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MimicServer.mjs","names":["Metrics.connectionsTotal","Metrics.connectionsActive","state: ConnectionState","Protocol.encodeServerMessage","Protocol.presenceSnapshotMessage","Protocol.snapshotMessage","Protocol.authResultSuccess","Metrics.connectionsErrors","Protocol.authResultFailure","Protocol.pong","Protocol.errorMessage","Protocol.presenceUpdateMessage","Protocol.presenceRemoveMessage","Metrics.connectionsDuration","Protocol.parseClientMessage"],"sources":["../src/MimicServer.ts"],"sourcesContent":["/**\n * @voidhash/mimic-effect - MimicServer\n *\n * WebSocket route layer for MimicServerEngine.\n * Creates routes compatible with HttpLayerRouter.\n */\nimport {\n Duration,\n Effect,\n Fiber,\n Layer,\n Metric,\n Stream,\n} from \"effect\";\nimport {\n HttpLayerRouter,\n HttpServerRequest,\n HttpServerResponse,\n} from \"@effect/platform\";\nimport type * as Socket from \"@effect/platform/Socket\";\nimport { Presence } from \"@voidhash/mimic\";\nimport type { MimicServerRouteConfig, ResolvedRouteConfig } from \"./Types\";\nimport * as Protocol from \"./Protocol\";\nimport { MissingDocumentIdError } from \"./Errors\";\nimport { MimicServerEngineTag, type MimicServerEngine } from \"./MimicServerEngine\";\nimport { MimicAuthServiceTag, type MimicAuthService } from \"./MimicAuthService\";\nimport * as Metrics from \"./Metrics\";\nimport type { AuthContext } from \"./Types\";\n\n// =============================================================================\n// Default Configuration\n// =============================================================================\n\nconst DEFAULT_PATH = \"/mimic\";\nconst DEFAULT_HEARTBEAT_INTERVAL = Duration.seconds(30);\nconst DEFAULT_HEARTBEAT_TIMEOUT = Duration.seconds(10);\n\n/**\n * Resolve route configuration with defaults\n */\nconst resolveRouteConfig = (\n config?: MimicServerRouteConfig\n): ResolvedRouteConfig => ({\n path: config?.path ?? DEFAULT_PATH,\n heartbeatInterval: config?.heartbeatInterval\n ? Duration.decode(config.heartbeatInterval)\n : DEFAULT_HEARTBEAT_INTERVAL,\n heartbeatTimeout: config?.heartbeatTimeout\n ? Duration.decode(config.heartbeatTimeout)\n : DEFAULT_HEARTBEAT_TIMEOUT,\n});\n\n// =============================================================================\n// URL Path Parsing\n// =============================================================================\n\n/**\n * Extract document ID from URL path.\n * Expected format: /basePath/doc/{documentId}\n */\nconst extractDocumentId = (\n path: string\n): Effect.Effect<string, MissingDocumentIdError> => {\n // Remove leading slash and split\n const parts = path.replace(/^\\/+/, \"\").split(\"/\");\n\n // Find the last occurrence of 'doc' in the path\n const docIndex = parts.lastIndexOf(\"doc\");\n const part = parts[docIndex + 1];\n if (docIndex !== -1 && part) {\n return Effect.succeed(decodeURIComponent(part));\n }\n return Effect.fail(new MissingDocumentIdError({ path }));\n};\n\n// =============================================================================\n// Connection State\n// =============================================================================\n\ninterface ConnectionState {\n readonly documentId: string;\n readonly connectionId: string;\n authenticated: boolean;\n authContext?: AuthContext;\n hasPresence: boolean;\n}\n\n// =============================================================================\n// WebSocket Connection Handler\n// =============================================================================\n\n/**\n * Handle a WebSocket connection for a document.\n */\nconst handleWebSocketConnection = Effect.fn(\"websocket.connection.handle\")(\n function* (\n socket: Socket.Socket,\n documentId: string,\n engine: MimicServerEngine,\n authService: MimicAuthService,\n _routeConfig: ResolvedRouteConfig\n ) {\n const connectionId = crypto.randomUUID();\n const connectionStartTime = Date.now();\n\n // Track connection metrics\n yield* Metric.increment(Metrics.connectionsTotal);\n yield* Metric.incrementBy(Metrics.connectionsActive, 1);\n\n // Track connection state (mutable for simplicity)\n const state: ConnectionState = {\n documentId,\n connectionId,\n authenticated: false,\n hasPresence: false,\n };\n\n // Get the socket writer\n const write = yield* socket.writer;\n\n // Helper to send a message to the client\n const sendMessage = (message: Protocol.ServerMessage) =>\n write(Protocol.encodeServerMessage(message));\n\n // Send presence snapshot after auth\n const sendPresenceSnapshot = Effect.fn(\"presence.snapshot.send\")(\n function* () {\n if (!engine.config.presence) return;\n\n const snapshot = yield* engine.getPresenceSnapshot(documentId);\n yield* sendMessage(\n Protocol.presenceSnapshotMessage(connectionId, snapshot.presences)\n );\n }\n );\n\n // Send document snapshot after auth\n const sendDocumentSnapshot = Effect.fn(\"document.snapshot.send\")(\n function* () {\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n }\n );\n\n // Handle authentication\n const handleAuth = Effect.fn(\"auth.handle\")(function* (token: string) {\n const result = yield* Effect.either(\n authService.authenticate(token, documentId)\n );\n\n if (result._tag === \"Right\") {\n state.authenticated = true;\n state.authContext = result.right;\n\n yield* sendMessage(\n Protocol.authResultSuccess(\n result.right.userId,\n result.right.permission\n )\n );\n\n // Send document snapshot after successful auth\n yield* sendDocumentSnapshot();\n\n // Send presence snapshot after successful auth\n yield* sendPresenceSnapshot();\n } else {\n yield* Metric.increment(Metrics.connectionsErrors);\n yield* sendMessage(\n Protocol.authResultFailure(\n result.left.reason ?? \"Authentication failed\"\n )\n );\n }\n });\n\n // Handle presence set\n const handlePresenceSet = Effect.fn(\"presence.set.handle\")(\n function* (data: unknown) {\n if (!state.authenticated) return;\n if (!state.authContext) return;\n if (!engine.config.presence) return;\n\n // Check write permission\n if (state.authContext.permission !== \"write\") {\n yield* Effect.logWarning(\"Presence set rejected - read-only user\", {\n connectionId,\n });\n return;\n }\n\n // Validate presence data against schema\n const validated = Presence.validateSafe(engine.config.presence, data);\n if (validated === undefined) {\n yield* Effect.logWarning(\"Invalid presence data received\", {\n connectionId,\n data,\n });\n return;\n }\n\n // Store in engine\n yield* engine.setPresence(documentId, connectionId, {\n data: validated,\n userId: state.authContext.userId,\n });\n\n state.hasPresence = true;\n }\n );\n\n // Handle presence clear\n const handlePresenceClear = Effect.fn(\"presence.clear.handle\")(\n function* () {\n if (!state.authenticated) return;\n if (!engine.config.presence) return;\n\n yield* engine.removePresence(documentId, connectionId);\n state.hasPresence = false;\n }\n );\n\n // Handle a client message\n const handleMessage = Effect.fn(\"message.handle\")(\n function* (message: Protocol.ClientMessage) {\n // Touch document on any activity (prevents idle GC)\n yield* engine.touch(documentId);\n\n switch (message.type) {\n case \"auth\":\n yield* handleAuth(message.token);\n break;\n\n case \"ping\":\n yield* sendMessage(Protocol.pong());\n break;\n\n case \"submit\":\n if (!state.authenticated) {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Not authenticated\"\n )\n );\n return;\n }\n\n // Check write permission\n if (state.authContext?.permission !== \"write\") {\n yield* sendMessage(\n Protocol.errorMessage(\n message.transaction.id,\n \"Write permission required\"\n )\n );\n return;\n }\n\n // Submit to the engine\n const submitResult = yield* engine.submit(\n documentId,\n message.transaction\n );\n\n // If rejected, send error (success is broadcast to all)\n if (!submitResult.success) {\n yield* sendMessage(\n Protocol.errorMessage(message.transaction.id, submitResult.reason)\n );\n }\n break;\n\n case \"request_snapshot\":\n if (!state.authenticated) {\n return;\n }\n const snapshot = yield* engine.getSnapshot(documentId);\n yield* sendMessage(\n Protocol.snapshotMessage(snapshot.state, snapshot.version)\n );\n break;\n\n case \"presence_set\":\n yield* handlePresenceSet(message.data);\n break;\n\n case \"presence_clear\":\n yield* handlePresenceClear();\n break;\n }\n }\n );\n\n // Subscribe to document broadcasts\n const subscribeFiber = yield* Effect.fork(\n Effect.fn(\"subscriptions.document.start\")(function* () {\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to the document\n const broadcastStream = yield* engine.subscribe(documentId);\n\n // Forward broadcasts to the WebSocket\n yield* Stream.runForEach(broadcastStream, (broadcast) =>\n sendMessage(broadcast as Protocol.ServerMessage)\n );\n })().pipe(Effect.scoped)\n );\n\n // Subscribe to presence events (if presence is enabled)\n const presenceFiber = yield* Effect.fork(\n Effect.fn(\"subscriptions.presence.start\")(function* () {\n if (!engine.config.presence) return;\n\n // Wait until authenticated before subscribing\n while (!state.authenticated) {\n yield* Effect.sleep(Duration.millis(100));\n }\n\n // Subscribe to presence events\n const presenceStream = yield* engine.subscribePresence(documentId);\n\n // Forward presence events to the WebSocket, filtering out our own events (no-echo)\n yield* Stream.runForEach(presenceStream, (event) =>\n Effect.gen(function* () {\n // Don't echo our own presence events\n if (event.id === connectionId) return;\n\n if (event.type === \"presence_update\") {\n yield* sendMessage(\n Protocol.presenceUpdateMessage(event.id, event.data, event.userId)\n );\n } else if (event.type === \"presence_remove\") {\n yield* sendMessage(Protocol.presenceRemoveMessage(event.id));\n }\n })\n );\n })().pipe(Effect.scoped)\n );\n\n // Ensure cleanup on disconnect\n yield* Effect.addFinalizer(() =>\n Effect.fn(\"connection.cleanup\")(function* () {\n // Calculate connection duration\n const duration = Date.now() - connectionStartTime;\n\n // Interrupt the subscribe fibers\n yield* Fiber.interrupt(subscribeFiber);\n yield* Fiber.interrupt(presenceFiber);\n\n // Remove presence if we had any\n if (state.hasPresence && engine.config.presence) {\n yield* engine.removePresence(documentId, connectionId);\n }\n\n // Update connection metrics\n yield* Metric.incrementBy(Metrics.connectionsActive, -1);\n yield* Metric.update(Metrics.connectionsDuration, duration);\n\n yield* Effect.logDebug(\"WebSocket connection closed\", {\n connectionId,\n documentId,\n durationMs: duration,\n });\n })()\n );\n\n // Process incoming messages\n yield* socket.runRaw((data) =>\n Effect.fn(\"message.process\")(function* () {\n const message = yield* Protocol.parseClientMessage(data);\n yield* handleMessage(message);\n })().pipe(\n Effect.catchAll((error) =>\n Effect.logError(\"Message handling error\", error)\n )\n )\n );\n }\n);\n\n// =============================================================================\n// Factory\n// =============================================================================\n\n/**\n * Create a route layer for MimicServerEngine.\n *\n * This creates a WebSocket route that connects to the engine.\n * Use Layer.mergeAll to compose with other routes.\n *\n * @example\n * ```typescript\n * // 1. Create the engine\n * const Engine = MimicServerEngine.make({\n * schema: DocSchema,\n * initial: { title: \"Untitled\" },\n * })\n *\n * // 2. Create the WebSocket route\n * const MimicRoute = MimicServer.layerHttpLayerRouter({\n * path: \"/mimic\",\n * })\n *\n * // 3. Wire together\n * const MimicLive = MimicRoute.pipe(\n * Layer.provide(Engine),\n * Layer.provide(ColdStorage.InMemory.make()),\n * Layer.provide(HotStorage.InMemory.make()),\n * Layer.provide(MimicAuthService.NoAuth.make()),\n * )\n *\n * // 4. Compose with other routes\n * const AllRoutes = Layer.mergeAll(MimicLive, DocsRoute, OtherRoutes)\n *\n * // 5. Serve\n * HttpLayerRouter.serve(AllRoutes).pipe(\n * Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })),\n * Layer.launch,\n * NodeRuntime.runMain\n * )\n * ```\n */\nexport const layerHttpLayerRouter = (\n options?: MimicServerRouteConfig\n) => {\n const routeConfig = resolveRouteConfig(options);\n\n // Build the route path pattern: {path}/doc/:documentId\n const routePath =\n `${routeConfig.path}/doc/:documentId` as HttpLayerRouter.PathInput;\n\n return Layer.scopedDiscard(\n Effect.gen(function* () {\n const router = yield* HttpLayerRouter.HttpRouter;\n // Capture engine and auth service at layer creation time\n const engine = yield* MimicServerEngineTag;\n const authService = yield* MimicAuthServiceTag;\n\n // Create the handler that receives the request\n // Engine and authService are captured in closure, not yielded per-request\n const handler = Effect.fn(\"websocket.route.handler\")(\n function* (request: HttpServerRequest.HttpServerRequest) {\n // Extract document ID from path\n const documentIdResult = yield* Effect.either(\n extractDocumentId(request.url)\n );\n if (documentIdResult._tag === \"Left\") {\n return HttpServerResponse.text(\n `Missing document ID in path: ${request.url}`,\n { status: 400 }\n );\n }\n const documentId = documentIdResult.right;\n\n // Upgrade to WebSocket\n const socket = yield* request.upgrade;\n\n // Handle the WebSocket connection\n yield* handleWebSocketConnection(\n socket,\n documentId,\n engine,\n authService,\n routeConfig\n ).pipe(\n Effect.scoped,\n Effect.catchAll((error) =>\n Effect.logError(\"WebSocket connection error\", error)\n )\n );\n\n // Return empty response - the WebSocket upgrade handles the connection\n return HttpServerResponse.empty();\n }\n );\n\n yield* router.add(\"GET\", routePath, handler);\n })\n );\n};\n\n// =============================================================================\n// Re-export namespace\n// =============================================================================\n\nexport const MimicServer = {\n layerHttpLayerRouter,\n};\n\n// =============================================================================\n// Re-export types\n// =============================================================================\n\nexport type { MimicServerRouteConfig };\n"],"mappings":"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"}
|