@voidhash/mimic-effect 0.0.2 → 0.0.3

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.
Files changed (99) hide show
  1. package/.turbo/turbo-build.log +99 -14
  2. package/dist/DocumentManager.cjs +118 -0
  3. package/dist/DocumentManager.d.cts +45 -0
  4. package/dist/DocumentManager.d.cts.map +1 -0
  5. package/dist/DocumentManager.d.mts +45 -0
  6. package/dist/DocumentManager.d.mts.map +1 -0
  7. package/dist/DocumentManager.mjs +105 -0
  8. package/dist/DocumentManager.mjs.map +1 -0
  9. package/dist/DocumentProtocol.cjs +94 -0
  10. package/dist/DocumentProtocol.d.cts +113 -0
  11. package/dist/DocumentProtocol.d.cts.map +1 -0
  12. package/dist/DocumentProtocol.d.mts +113 -0
  13. package/dist/DocumentProtocol.d.mts.map +1 -0
  14. package/dist/DocumentProtocol.mjs +89 -0
  15. package/dist/DocumentProtocol.mjs.map +1 -0
  16. package/dist/MimicAuthService.cjs +55 -0
  17. package/dist/MimicAuthService.d.cts +65 -0
  18. package/dist/MimicAuthService.d.cts.map +1 -0
  19. package/dist/MimicAuthService.d.mts +65 -0
  20. package/dist/MimicAuthService.d.mts.map +1 -0
  21. package/dist/MimicAuthService.mjs +47 -0
  22. package/dist/MimicAuthService.mjs.map +1 -0
  23. package/dist/MimicConfig.cjs +50 -0
  24. package/dist/MimicConfig.d.cts +99 -0
  25. package/dist/MimicConfig.d.cts.map +1 -0
  26. package/dist/MimicConfig.d.mts +99 -0
  27. package/dist/MimicConfig.d.mts.map +1 -0
  28. package/dist/MimicConfig.mjs +41 -0
  29. package/dist/MimicConfig.mjs.map +1 -0
  30. package/dist/MimicDataStorage.cjs +83 -0
  31. package/dist/MimicDataStorage.d.cts +113 -0
  32. package/dist/MimicDataStorage.d.cts.map +1 -0
  33. package/dist/MimicDataStorage.d.mts +113 -0
  34. package/dist/MimicDataStorage.d.mts.map +1 -0
  35. package/dist/MimicDataStorage.mjs +74 -0
  36. package/dist/MimicDataStorage.mjs.map +1 -0
  37. package/dist/MimicServer.cjs +230 -0
  38. package/dist/MimicServer.d.cts +192 -0
  39. package/dist/MimicServer.d.cts.map +1 -0
  40. package/dist/MimicServer.d.mts +192 -0
  41. package/dist/MimicServer.d.mts.map +1 -0
  42. package/dist/MimicServer.mjs +223 -0
  43. package/dist/MimicServer.mjs.map +1 -0
  44. package/dist/PresenceManager.cjs +108 -0
  45. package/dist/PresenceManager.d.cts +91 -0
  46. package/dist/PresenceManager.d.cts.map +1 -0
  47. package/dist/PresenceManager.d.mts +91 -0
  48. package/dist/PresenceManager.d.mts.map +1 -0
  49. package/dist/PresenceManager.mjs +95 -0
  50. package/dist/PresenceManager.mjs.map +1 -0
  51. package/dist/WebSocketHandler.cjs +366 -0
  52. package/dist/WebSocketHandler.d.cts +34 -0
  53. package/dist/WebSocketHandler.d.cts.map +1 -0
  54. package/dist/WebSocketHandler.d.mts +34 -0
  55. package/dist/WebSocketHandler.d.mts.map +1 -0
  56. package/dist/WebSocketHandler.mjs +355 -0
  57. package/dist/WebSocketHandler.mjs.map +1 -0
  58. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/defineProperty.cjs +14 -0
  59. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/defineProperty.mjs +14 -0
  60. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectSpread2.cjs +27 -0
  61. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/objectSpread2.mjs +27 -0
  62. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/toPrimitive.cjs +16 -0
  63. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/toPrimitive.mjs +16 -0
  64. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/toPropertyKey.cjs +11 -0
  65. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/toPropertyKey.mjs +11 -0
  66. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/typeof.cjs +18 -0
  67. package/dist/_virtual/_@oxc-project_runtime@0.103.0/helpers/typeof.mjs +12 -0
  68. package/dist/_virtual/rolldown_runtime.cjs +43 -0
  69. package/dist/{chunk-C6wwvPpM.mjs → _virtual/rolldown_runtime.mjs} +1 -1
  70. package/dist/auth/NoAuth.cjs +43 -0
  71. package/dist/auth/NoAuth.d.cts +22 -0
  72. package/dist/auth/NoAuth.d.cts.map +1 -0
  73. package/dist/auth/NoAuth.d.mts +22 -0
  74. package/dist/auth/NoAuth.d.mts.map +1 -0
  75. package/dist/auth/NoAuth.mjs +36 -0
  76. package/dist/auth/NoAuth.mjs.map +1 -0
  77. package/dist/errors.cjs +74 -0
  78. package/dist/errors.d.cts +89 -0
  79. package/dist/errors.d.cts.map +1 -0
  80. package/dist/errors.d.mts +89 -0
  81. package/dist/errors.d.mts.map +1 -0
  82. package/dist/errors.mjs +67 -0
  83. package/dist/errors.mjs.map +1 -0
  84. package/dist/index.cjs +29 -1227
  85. package/dist/index.d.cts +12 -795
  86. package/dist/index.d.mts +12 -795
  87. package/dist/index.mjs +13 -1162
  88. package/dist/storage/InMemoryDataStorage.cjs +57 -0
  89. package/dist/storage/InMemoryDataStorage.d.cts +19 -0
  90. package/dist/storage/InMemoryDataStorage.d.cts.map +1 -0
  91. package/dist/storage/InMemoryDataStorage.d.mts +19 -0
  92. package/dist/storage/InMemoryDataStorage.d.mts.map +1 -0
  93. package/dist/storage/InMemoryDataStorage.mjs +48 -0
  94. package/dist/storage/InMemoryDataStorage.mjs.map +1 -0
  95. package/package.json +3 -3
  96. package/tsdown.config.ts +1 -1
  97. package/dist/index.d.cts.map +0 -1
  98. package/dist/index.d.mts.map +0 -1
  99. package/dist/index.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PresenceManager.mjs","names":["docPresence: DocumentPresence","presences: Record<string, PresenceEntry>","layer: Layer.Layer<PresenceManagerTag>","layerDefault: Layer.Layer<PresenceManagerTag>"],"sources":["../src/PresenceManager.ts"],"sourcesContent":["/**\n * @since 0.0.1\n * Presence manager for ephemeral per-connection state.\n * Handles in-memory storage and broadcasting of presence updates.\n */\nimport * as Effect from \"effect/Effect\";\nimport * as Layer from \"effect/Layer\";\nimport * as PubSub from \"effect/PubSub\";\nimport * as Ref from \"effect/Ref\";\nimport * as HashMap from \"effect/HashMap\";\nimport * as Context from \"effect/Context\";\nimport * as Scope from \"effect/Scope\";\nimport * as Stream from \"effect/Stream\";\nimport type { Presence } from \"@voidhash/mimic\";\n\n// =============================================================================\n// Presence Entry Types\n// =============================================================================\n\n/**\n * A presence entry stored in the manager.\n */\nexport interface PresenceEntry {\n /** The presence data */\n readonly data: unknown;\n /** Optional user ID from authentication */\n readonly userId?: string;\n}\n\n// =============================================================================\n// Presence Events\n// =============================================================================\n\n/**\n * Event emitted when a presence is updated.\n */\nexport interface PresenceUpdateEvent {\n readonly type: \"presence_update\";\n /** The connection ID of the user who updated */\n readonly id: string;\n /** The presence data */\n readonly data: unknown;\n /** Optional user ID from authentication */\n readonly userId?: string;\n}\n\n/**\n * Event emitted when a presence is removed (user disconnected).\n */\nexport interface PresenceRemoveEvent {\n readonly type: \"presence_remove\";\n /** The connection ID of the user who disconnected */\n readonly id: string;\n}\n\n/**\n * Union of all presence events.\n */\nexport type PresenceEvent = PresenceUpdateEvent | PresenceRemoveEvent;\n\n// =============================================================================\n// Presence Snapshot\n// =============================================================================\n\n/**\n * A snapshot of all presence entries for a document.\n */\nexport interface PresenceSnapshot {\n /** Map of connectionId to presence entry */\n readonly presences: Record<string, PresenceEntry>;\n}\n\n// =============================================================================\n// Document Presence Instance\n// =============================================================================\n\n/**\n * Per-document presence state.\n */\ninterface DocumentPresence {\n /** Map of connectionId to presence entry */\n readonly entries: Ref.Ref<HashMap.HashMap<string, PresenceEntry>>;\n /** PubSub for broadcasting presence events */\n readonly pubsub: PubSub.PubSub<PresenceEvent>;\n}\n\n// =============================================================================\n// Presence Manager Service\n// =============================================================================\n\n/**\n * Service interface for the PresenceManager.\n */\nexport interface PresenceManager {\n /**\n * Get a snapshot of all presences for a document.\n */\n readonly getSnapshot: (\n documentId: string\n ) => Effect.Effect<PresenceSnapshot>;\n\n /**\n * Set/update presence for a connection.\n * Broadcasts the update to all subscribers.\n */\n readonly set: (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ) => Effect.Effect<void>;\n\n /**\n * Remove presence for a connection (e.g., on disconnect).\n * Broadcasts the removal to all subscribers.\n */\n readonly remove: (\n documentId: string,\n connectionId: string\n ) => Effect.Effect<void>;\n\n /**\n * Subscribe to presence events for a document.\n * Returns a Stream of presence events.\n */\n readonly subscribe: (\n documentId: string\n ) => Effect.Effect<\n Stream.Stream<PresenceEvent>,\n never,\n Scope.Scope\n >;\n}\n\n/**\n * Context tag for PresenceManager.\n */\nexport class PresenceManagerTag extends Context.Tag(\n \"@voidhash/mimic-server-effect/PresenceManager\"\n)<PresenceManagerTag, PresenceManager>() {}\n\n// =============================================================================\n// Presence Manager Implementation\n// =============================================================================\n\n/**\n * Create the PresenceManager service.\n */\nconst makePresenceManager = Effect.gen(function* () {\n // Map of document ID to document presence state\n const documents = yield* Ref.make(\n HashMap.empty<string, DocumentPresence>()\n );\n\n // Get or create a document presence instance\n const getOrCreateDocument = (\n documentId: string\n ): Effect.Effect<DocumentPresence> =>\n Effect.gen(function* () {\n const current = yield* Ref.get(documents);\n const existing = HashMap.get(current, documentId);\n\n if (existing._tag === \"Some\") {\n return existing.value;\n }\n\n // Create new document presence\n const entries = yield* Ref.make(\n HashMap.empty<string, PresenceEntry>()\n );\n const pubsub = yield* PubSub.unbounded<PresenceEvent>();\n\n const docPresence: DocumentPresence = {\n entries,\n pubsub,\n };\n\n // Store in map\n yield* Ref.update(documents, (map) =>\n HashMap.set(map, documentId, docPresence)\n );\n\n return docPresence;\n });\n\n // Get snapshot of all presences for a document\n const getSnapshot = (documentId: string): Effect.Effect<PresenceSnapshot> =>\n Effect.gen(function* () {\n const docPresence = yield* getOrCreateDocument(documentId);\n const entriesMap = yield* Ref.get(docPresence.entries);\n\n // Convert HashMap to Record\n const presences: Record<string, PresenceEntry> = {};\n for (const [id, entry] of entriesMap) {\n presences[id] = entry;\n }\n\n return { presences };\n });\n\n // Set/update presence for a connection\n const set = (\n documentId: string,\n connectionId: string,\n entry: PresenceEntry\n ): Effect.Effect<void> =>\n Effect.gen(function* () {\n const docPresence = yield* getOrCreateDocument(documentId);\n\n // Update the entry\n yield* Ref.update(docPresence.entries, (map) =>\n HashMap.set(map, connectionId, entry)\n );\n\n // Broadcast the update\n yield* PubSub.publish(docPresence.pubsub, {\n type: \"presence_update\",\n id: connectionId,\n data: entry.data,\n userId: entry.userId,\n });\n });\n\n // Remove presence for a connection\n const remove = (\n documentId: string,\n connectionId: string\n ): Effect.Effect<void> =>\n Effect.gen(function* () {\n const current = yield* Ref.get(documents);\n const existing = HashMap.get(current, documentId);\n\n if (existing._tag === \"None\") {\n return; // Document doesn't exist, nothing to remove\n }\n\n const docPresence = existing.value;\n\n // Check if the connection has a presence\n const entries = yield* Ref.get(docPresence.entries);\n const hasEntry = HashMap.has(entries, connectionId);\n\n if (!hasEntry) {\n return; // No presence to remove\n }\n\n // Remove the entry\n yield* Ref.update(docPresence.entries, (map) =>\n HashMap.remove(map, connectionId)\n );\n\n // Broadcast the removal\n yield* PubSub.publish(docPresence.pubsub, {\n type: \"presence_remove\",\n id: connectionId,\n });\n });\n\n // Subscribe to presence events\n const subscribe = (\n documentId: string\n ): Effect.Effect<Stream.Stream<PresenceEvent>, never, Scope.Scope> =>\n Effect.gen(function* () {\n const docPresence = yield* getOrCreateDocument(documentId);\n\n // Subscribe to the PubSub\n const queue = yield* PubSub.subscribe(docPresence.pubsub);\n\n // Convert queue to stream\n return Stream.fromQueue(queue);\n });\n\n const manager: PresenceManager = {\n getSnapshot,\n set,\n remove,\n subscribe,\n };\n\n return manager;\n});\n\n/**\n * Layer that provides PresenceManager.\n */\nexport const layer: Layer.Layer<PresenceManagerTag> = Layer.effect(\n PresenceManagerTag,\n makePresenceManager\n);\n\n/**\n * Default layer that provides PresenceManager.\n * Uses the default priority for layer composition.\n */\nexport const layerDefault: Layer.Layer<PresenceManagerTag> = Layer.effectDiscard(\n Effect.succeed(undefined)\n).pipe(Layer.provideMerge(layer));\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwIA,IAAa,qBAAb,cAAwC,QAAQ,IAC9C,gDACD,EAAuC,CAAC;;;;AASzC,MAAM,sBAAsB,OAAO,IAAI,aAAa;CAElD,MAAM,YAAY,OAAO,IAAI,KAC3B,QAAQ,OAAiC,CAC1C;CAGD,MAAM,uBACJ,eAEA,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,IAAI,UAAU;EACzC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AAEjD,MAAI,SAAS,SAAS,OACpB,QAAO,SAAS;EASlB,MAAMA,cAAgC;GACpC,SANc,OAAO,IAAI,KACzB,QAAQ,OAA8B,CACvC;GAKC,QAJa,OAAO,OAAO,WAA0B;GAKtD;AAGD,SAAO,IAAI,OAAO,YAAY,QAC5B,QAAQ,IAAI,KAAK,YAAY,YAAY,CAC1C;AAED,SAAO;GACP;CAGJ,MAAM,eAAe,eACnB,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,OAAO,oBAAoB,WAAW;EAC1D,MAAM,aAAa,OAAO,IAAI,IAAI,YAAY,QAAQ;EAGtD,MAAMC,YAA2C,EAAE;AACnD,OAAK,MAAM,CAAC,IAAI,UAAU,WACxB,WAAU,MAAM;AAGlB,SAAO,EAAE,WAAW;GACpB;CAGJ,MAAM,OACJ,YACA,cACA,UAEA,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,OAAO,oBAAoB,WAAW;AAG1D,SAAO,IAAI,OAAO,YAAY,UAAU,QACtC,QAAQ,IAAI,KAAK,cAAc,MAAM,CACtC;AAGD,SAAO,OAAO,QAAQ,YAAY,QAAQ;GACxC,MAAM;GACN,IAAI;GACJ,MAAM,MAAM;GACZ,QAAQ,MAAM;GACf,CAAC;GACF;CAGJ,MAAM,UACJ,YACA,iBAEA,OAAO,IAAI,aAAa;EACtB,MAAM,UAAU,OAAO,IAAI,IAAI,UAAU;EACzC,MAAM,WAAW,QAAQ,IAAI,SAAS,WAAW;AAEjD,MAAI,SAAS,SAAS,OACpB;EAGF,MAAM,cAAc,SAAS;EAG7B,MAAM,UAAU,OAAO,IAAI,IAAI,YAAY,QAAQ;AAGnD,MAAI,CAFa,QAAQ,IAAI,SAAS,aAAa,CAGjD;AAIF,SAAO,IAAI,OAAO,YAAY,UAAU,QACtC,QAAQ,OAAO,KAAK,aAAa,CAClC;AAGD,SAAO,OAAO,QAAQ,YAAY,QAAQ;GACxC,MAAM;GACN,IAAI;GACL,CAAC;GACF;CAGJ,MAAM,aACJ,eAEA,OAAO,IAAI,aAAa;EACtB,MAAM,cAAc,OAAO,oBAAoB,WAAW;EAG1D,MAAM,QAAQ,OAAO,OAAO,UAAU,YAAY,OAAO;AAGzD,SAAO,OAAO,UAAU,MAAM;GAC9B;AASJ,QAPiC;EAC/B;EACA;EACA;EACA;EACD;EAGD;;;;AAKF,MAAaC,QAAyC,MAAM,OAC1D,oBACA,oBACD;;;;;AAMD,MAAaC,eAAgD,MAAM,cACjE,OAAO,QAAQ,OAAU,CAC1B,CAAC,KAAK,MAAM,aAAa,MAAM,CAAC"}
@@ -0,0 +1,366 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const require_MimicConfig = require('./MimicConfig.cjs');
3
+ const require_DocumentManager = require('./DocumentManager.cjs');
4
+ const require_MimicAuthService = require('./MimicAuthService.cjs');
5
+ const require_PresenceManager = require('./PresenceManager.cjs');
6
+ const require_errors = require('./errors.cjs');
7
+ const require_objectSpread2 = require('./_virtual/_@oxc-project_runtime@0.103.0/helpers/objectSpread2.cjs');
8
+ let effect_Effect = require("effect/Effect");
9
+ effect_Effect = require_rolldown_runtime.__toESM(effect_Effect);
10
+ let effect_Stream = require("effect/Stream");
11
+ effect_Stream = require_rolldown_runtime.__toESM(effect_Stream);
12
+ let effect_Duration = require("effect/Duration");
13
+ effect_Duration = require_rolldown_runtime.__toESM(effect_Duration);
14
+ let effect_Fiber = require("effect/Fiber");
15
+ effect_Fiber = require_rolldown_runtime.__toESM(effect_Fiber);
16
+ let _voidhash_mimic = require("@voidhash/mimic");
17
+
18
+ //#region src/WebSocketHandler.ts
19
+ /**
20
+ * @since 0.0.1
21
+ * WebSocket connection handler using Effect Platform Socket API.
22
+ */
23
+ var WebSocketHandler_exports = /* @__PURE__ */ require_rolldown_runtime.__export({
24
+ extractDocumentId: () => extractDocumentId,
25
+ handleConnection: () => handleConnection,
26
+ makeHandler: () => makeHandler
27
+ });
28
+ /**
29
+ * Extract document ID from URL path.
30
+ * Expected format: /doc/{documentId}
31
+ */
32
+ const extractDocumentId = (path) => {
33
+ const parts = path.replace(/^\/+/, "").split("/");
34
+ const docIndex = parts.lastIndexOf("doc");
35
+ const part = parts[docIndex + 1];
36
+ if (docIndex !== -1 && part) return effect_Effect.succeed(decodeURIComponent(part));
37
+ return effect_Effect.fail(new require_errors.MissingDocumentIdError({}));
38
+ };
39
+ /**
40
+ * Decodes an encoded client message from the wire format.
41
+ */
42
+ const decodeClientMessage = (encoded) => {
43
+ if (encoded.type === "submit") return {
44
+ type: "submit",
45
+ transaction: _voidhash_mimic.Transaction.decode(encoded.transaction)
46
+ };
47
+ return encoded;
48
+ };
49
+ /**
50
+ * Encodes a server message for the wire format.
51
+ */
52
+ const encodeServerMessageForWire = (message) => {
53
+ if (message.type === "transaction") return {
54
+ type: "transaction",
55
+ transaction: _voidhash_mimic.Transaction.encode(message.transaction),
56
+ version: message.version
57
+ };
58
+ return message;
59
+ };
60
+ const parseClientMessage = (data) => effect_Effect.try({
61
+ try: () => {
62
+ const text = typeof data === "string" ? data : new TextDecoder().decode(data);
63
+ return decodeClientMessage(JSON.parse(text));
64
+ },
65
+ catch: (cause) => new require_errors.MessageParseError({ cause })
66
+ });
67
+ const encodeServerMessage = (message) => JSON.stringify(encodeServerMessageForWire(message));
68
+ /**
69
+ * Handle a WebSocket connection for a document.
70
+ *
71
+ * @param socket - The Effect Platform Socket
72
+ * @param path - The URL path (e.g., "/doc/my-document-id")
73
+ * @returns An Effect that handles the connection lifecycle
74
+ */
75
+ const handleConnection = (socket, path) => effect_Effect.gen(function* () {
76
+ const config = yield* require_MimicConfig.MimicServerConfigTag;
77
+ const authService = yield* require_MimicAuthService.MimicAuthServiceTag;
78
+ const documentManager = yield* require_DocumentManager.DocumentManagerTag;
79
+ const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
80
+ const documentId = yield* extractDocumentId(path);
81
+ const connectionId = crypto.randomUUID();
82
+ let state = {
83
+ documentId,
84
+ connectionId,
85
+ authenticated: false
86
+ };
87
+ let hasPresence = false;
88
+ const write = yield* socket.writer;
89
+ const sendMessage = (message) => write(encodeServerMessage(message));
90
+ const sendPresenceSnapshot = effect_Effect.gen(function* () {
91
+ if (!config.presence) return;
92
+ yield* sendMessage({
93
+ type: "presence_snapshot",
94
+ selfId: connectionId,
95
+ presences: (yield* presenceManager.getSnapshot(documentId)).presences
96
+ });
97
+ });
98
+ const handleAuth = (token) => effect_Effect.gen(function* () {
99
+ const result = yield* authService.authenticate(token);
100
+ if (result.success) {
101
+ state = require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, state), {}, {
102
+ authenticated: true,
103
+ userId: result.userId
104
+ });
105
+ yield* sendMessage({
106
+ type: "auth_result",
107
+ success: true
108
+ });
109
+ yield* sendPresenceSnapshot;
110
+ } else yield* sendMessage({
111
+ type: "auth_result",
112
+ success: false,
113
+ error: result.error
114
+ });
115
+ });
116
+ const handlePresenceSet = (data) => effect_Effect.gen(function* () {
117
+ if (!state.authenticated) return;
118
+ if (!config.presence) return;
119
+ const validated = _voidhash_mimic.Presence.validateSafe(config.presence, data);
120
+ if (validated === void 0) {
121
+ yield* effect_Effect.logWarning("Invalid presence data received", {
122
+ connectionId,
123
+ data
124
+ });
125
+ return;
126
+ }
127
+ yield* presenceManager.set(documentId, connectionId, {
128
+ data: validated,
129
+ userId: state.userId
130
+ });
131
+ hasPresence = true;
132
+ });
133
+ const handlePresenceClear = effect_Effect.gen(function* () {
134
+ if (!state.authenticated) return;
135
+ if (!config.presence) return;
136
+ yield* presenceManager.remove(documentId, connectionId);
137
+ hasPresence = false;
138
+ });
139
+ const handleMessage = (message) => effect_Effect.gen(function* () {
140
+ switch (message.type) {
141
+ case "auth":
142
+ yield* handleAuth(message.token);
143
+ break;
144
+ case "ping":
145
+ yield* sendMessage({ type: "pong" });
146
+ break;
147
+ case "submit":
148
+ if (!state.authenticated) {
149
+ yield* sendMessage({
150
+ type: "error",
151
+ transactionId: message.transaction.id,
152
+ reason: "Not authenticated"
153
+ });
154
+ return;
155
+ }
156
+ const submitResult = yield* documentManager.submit(documentId, message.transaction);
157
+ if (!submitResult.success) yield* sendMessage({
158
+ type: "error",
159
+ transactionId: message.transaction.id,
160
+ reason: submitResult.reason
161
+ });
162
+ break;
163
+ case "request_snapshot":
164
+ if (!state.authenticated) return;
165
+ yield* sendMessage(yield* effect_Effect.catchAll(documentManager.getSnapshot(documentId), () => effect_Effect.succeed({
166
+ type: "snapshot",
167
+ state: null,
168
+ version: 0
169
+ })));
170
+ break;
171
+ case "presence_set":
172
+ yield* handlePresenceSet(message.data);
173
+ break;
174
+ case "presence_clear":
175
+ yield* handlePresenceClear;
176
+ break;
177
+ }
178
+ });
179
+ const subscribeFiber = yield* effect_Effect.fork(effect_Effect.gen(function* () {
180
+ while (!state.authenticated) yield* effect_Effect.sleep(effect_Duration.millis(100));
181
+ const broadcastStream = yield* effect_Effect.catchAll(documentManager.subscribe(documentId), () => effect_Effect.succeed(effect_Stream.empty));
182
+ yield* effect_Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
183
+ }).pipe(effect_Effect.scoped));
184
+ const presenceFiber = yield* effect_Effect.fork(effect_Effect.gen(function* () {
185
+ if (!config.presence) return;
186
+ while (!state.authenticated) yield* effect_Effect.sleep(effect_Duration.millis(100));
187
+ const presenceStream = yield* presenceManager.subscribe(documentId);
188
+ yield* effect_Stream.runForEach(presenceStream, (event) => effect_Effect.gen(function* () {
189
+ if (event.id === connectionId) return;
190
+ if (event.type === "presence_update") yield* sendMessage({
191
+ type: "presence_update",
192
+ id: event.id,
193
+ data: event.data,
194
+ userId: event.userId
195
+ });
196
+ else if (event.type === "presence_remove") yield* sendMessage({
197
+ type: "presence_remove",
198
+ id: event.id
199
+ });
200
+ }));
201
+ }).pipe(effect_Effect.scoped));
202
+ yield* effect_Effect.addFinalizer(() => effect_Effect.gen(function* () {
203
+ yield* effect_Fiber.interrupt(subscribeFiber);
204
+ yield* effect_Fiber.interrupt(presenceFiber);
205
+ if (hasPresence && config.presence) yield* presenceManager.remove(documentId, connectionId);
206
+ }));
207
+ yield* socket.runRaw((data) => effect_Effect.gen(function* () {
208
+ yield* handleMessage(yield* parseClientMessage(data));
209
+ }).pipe(effect_Effect.catchAll((error) => effect_Effect.logError("Message handling error", error))));
210
+ });
211
+ /**
212
+ * Create a handler function for the WebSocket server.
213
+ * Returns a function that takes a socket and document ID.
214
+ */
215
+ const makeHandler = effect_Effect.gen(function* () {
216
+ const config = yield* require_MimicConfig.MimicServerConfigTag;
217
+ const authService = yield* require_MimicAuthService.MimicAuthServiceTag;
218
+ const documentManager = yield* require_DocumentManager.DocumentManagerTag;
219
+ const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
220
+ return (socket, documentId) => handleConnectionWithDocumentId(socket, documentId).pipe(effect_Effect.provideService(require_MimicConfig.MimicServerConfigTag, config), effect_Effect.provideService(require_MimicAuthService.MimicAuthServiceTag, authService), effect_Effect.provideService(require_DocumentManager.DocumentManagerTag, documentManager), effect_Effect.provideService(require_PresenceManager.PresenceManagerTag, presenceManager), effect_Effect.scoped);
221
+ });
222
+ /**
223
+ * Handle a WebSocket connection for a document (using document ID directly).
224
+ */
225
+ const handleConnectionWithDocumentId = (socket, documentId) => effect_Effect.gen(function* () {
226
+ const config = yield* require_MimicConfig.MimicServerConfigTag;
227
+ const authService = yield* require_MimicAuthService.MimicAuthServiceTag;
228
+ const documentManager = yield* require_DocumentManager.DocumentManagerTag;
229
+ const presenceManager = yield* require_PresenceManager.PresenceManagerTag;
230
+ const connectionId = crypto.randomUUID();
231
+ let state = {
232
+ documentId,
233
+ connectionId,
234
+ authenticated: false
235
+ };
236
+ let hasPresence = false;
237
+ const write = yield* socket.writer;
238
+ const sendMessage = (message) => write(encodeServerMessage(message));
239
+ const sendPresenceSnapshot = effect_Effect.gen(function* () {
240
+ if (!config.presence) return;
241
+ yield* sendMessage({
242
+ type: "presence_snapshot",
243
+ selfId: connectionId,
244
+ presences: (yield* presenceManager.getSnapshot(documentId)).presences
245
+ });
246
+ });
247
+ const handleAuth = (token) => effect_Effect.gen(function* () {
248
+ const result = yield* authService.authenticate(token);
249
+ if (result.success) {
250
+ state = require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, state), {}, {
251
+ authenticated: true,
252
+ userId: result.userId
253
+ });
254
+ yield* sendMessage({
255
+ type: "auth_result",
256
+ success: true
257
+ });
258
+ yield* sendPresenceSnapshot;
259
+ } else yield* sendMessage({
260
+ type: "auth_result",
261
+ success: false,
262
+ error: result.error
263
+ });
264
+ });
265
+ const handlePresenceSet = (data) => effect_Effect.gen(function* () {
266
+ if (!state.authenticated) return;
267
+ if (!config.presence) return;
268
+ const validated = _voidhash_mimic.Presence.validateSafe(config.presence, data);
269
+ if (validated === void 0) {
270
+ yield* effect_Effect.logWarning("Invalid presence data received", {
271
+ connectionId,
272
+ data
273
+ });
274
+ return;
275
+ }
276
+ yield* presenceManager.set(documentId, connectionId, {
277
+ data: validated,
278
+ userId: state.userId
279
+ });
280
+ hasPresence = true;
281
+ });
282
+ const handlePresenceClear = effect_Effect.gen(function* () {
283
+ if (!state.authenticated) return;
284
+ if (!config.presence) return;
285
+ yield* presenceManager.remove(documentId, connectionId);
286
+ hasPresence = false;
287
+ });
288
+ const handleMessage = (message) => effect_Effect.gen(function* () {
289
+ switch (message.type) {
290
+ case "auth":
291
+ yield* handleAuth(message.token);
292
+ break;
293
+ case "ping":
294
+ yield* sendMessage({ type: "pong" });
295
+ break;
296
+ case "submit":
297
+ if (!state.authenticated) {
298
+ yield* sendMessage({
299
+ type: "error",
300
+ transactionId: message.transaction.id,
301
+ reason: "Not authenticated"
302
+ });
303
+ return;
304
+ }
305
+ const submitResult = yield* documentManager.submit(documentId, message.transaction);
306
+ if (!submitResult.success) yield* sendMessage({
307
+ type: "error",
308
+ transactionId: message.transaction.id,
309
+ reason: submitResult.reason
310
+ });
311
+ break;
312
+ case "request_snapshot":
313
+ if (!state.authenticated) return;
314
+ yield* sendMessage(yield* documentManager.getSnapshot(documentId));
315
+ break;
316
+ case "presence_set":
317
+ yield* handlePresenceSet(message.data);
318
+ break;
319
+ case "presence_clear":
320
+ yield* handlePresenceClear;
321
+ break;
322
+ }
323
+ });
324
+ const subscribeFiber = yield* effect_Effect.fork(effect_Effect.gen(function* () {
325
+ while (!state.authenticated) yield* effect_Effect.sleep(effect_Duration.millis(100));
326
+ const broadcastStream = yield* documentManager.subscribe(documentId);
327
+ yield* effect_Stream.runForEach(broadcastStream, (broadcast) => sendMessage(broadcast));
328
+ }).pipe(effect_Effect.scoped));
329
+ const presenceFiber = yield* effect_Effect.fork(effect_Effect.gen(function* () {
330
+ if (!config.presence) return;
331
+ while (!state.authenticated) yield* effect_Effect.sleep(effect_Duration.millis(100));
332
+ const presenceStream = yield* presenceManager.subscribe(documentId);
333
+ yield* effect_Stream.runForEach(presenceStream, (event) => effect_Effect.gen(function* () {
334
+ if (event.id === connectionId) return;
335
+ if (event.type === "presence_update") yield* sendMessage({
336
+ type: "presence_update",
337
+ id: event.id,
338
+ data: event.data,
339
+ userId: event.userId
340
+ });
341
+ else if (event.type === "presence_remove") yield* sendMessage({
342
+ type: "presence_remove",
343
+ id: event.id
344
+ });
345
+ }));
346
+ }).pipe(effect_Effect.scoped));
347
+ yield* effect_Effect.addFinalizer(() => effect_Effect.gen(function* () {
348
+ yield* effect_Fiber.interrupt(subscribeFiber);
349
+ yield* effect_Fiber.interrupt(presenceFiber);
350
+ if (hasPresence && config.presence) yield* presenceManager.remove(documentId, connectionId);
351
+ }));
352
+ yield* socket.runRaw((data) => effect_Effect.gen(function* () {
353
+ yield* handleMessage(yield* parseClientMessage(data));
354
+ }).pipe(effect_Effect.catchAll((error) => effect_Effect.logError("Message handling error", error))));
355
+ });
356
+
357
+ //#endregion
358
+ Object.defineProperty(exports, 'WebSocketHandler_exports', {
359
+ enumerable: true,
360
+ get: function () {
361
+ return WebSocketHandler_exports;
362
+ }
363
+ });
364
+ exports.extractDocumentId = extractDocumentId;
365
+ exports.handleConnection = handleConnection;
366
+ exports.makeHandler = makeHandler;
@@ -0,0 +1,34 @@
1
+ import { MimicServerConfigTag } from "./MimicConfig.cjs";
2
+ import { DocumentManagerTag } from "./DocumentManager.cjs";
3
+ import { MimicAuthServiceTag } from "./MimicAuthService.cjs";
4
+ import { PresenceManagerTag } from "./PresenceManager.cjs";
5
+ import { MessageParseError, MissingDocumentIdError } from "./errors.cjs";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Socket from "@effect/platform/Socket";
8
+ import * as Scope from "effect/Scope";
9
+
10
+ //#region src/WebSocketHandler.d.ts
11
+ declare namespace WebSocketHandler_d_exports {
12
+ export { extractDocumentId, handleConnection, makeHandler };
13
+ }
14
+ /**
15
+ * Extract document ID from URL path.
16
+ * Expected format: /doc/{documentId}
17
+ */
18
+ declare const extractDocumentId: (path: string) => Effect.Effect<string, MissingDocumentIdError>;
19
+ /**
20
+ * Handle a WebSocket connection for a document.
21
+ *
22
+ * @param socket - The Effect Platform Socket
23
+ * @param path - The URL path (e.g., "/doc/my-document-id")
24
+ * @returns An Effect that handles the connection lifecycle
25
+ */
26
+ declare const handleConnection: (socket: Socket.Socket, path: string) => Effect.Effect<void, Socket.SocketError | MissingDocumentIdError | MessageParseError, MimicServerConfigTag | MimicAuthServiceTag | DocumentManagerTag | PresenceManagerTag | Scope.Scope>;
27
+ /**
28
+ * Create a handler function for the WebSocket server.
29
+ * Returns a function that takes a socket and document ID.
30
+ */
31
+ declare const makeHandler: Effect.Effect<(socket: Socket.Socket, documentId: string) => Effect.Effect<void, MessageParseError | Socket.SocketError, never>, never, DocumentManagerTag | MimicAuthServiceTag | MimicServerConfigTag | PresenceManagerTag>;
32
+ //#endregion
33
+ export { WebSocketHandler_d_exports };
34
+ //# sourceMappingURL=WebSocketHandler.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebSocketHandler.d.cts","names":[],"sources":["../src/WebSocketHandler.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAyJa,cAAA,iBAEY,EAAA,CAAA,IAAA,EAAA,MAAA,EAAA,GAAtB,MAAA,CAAO,MAAA,CAAA,MAAM,EAAS,sBAAT,CAAA;AAuEhB;;;;;;;AAM+C,cANlC,gBAMkC,EAAA,CAAA,MAAA,EALrC,MAAA,CAAO,MAK8B,EAAA,IAAA,EAAA,MAAA,EAAA,GAH5C,MAAA,CAAO,MAGqC,CAAA,IAAA,EAD7C,MAAA,CAAO,WACsC,GADxB,sBACwB,GADC,iBACD,EAA7C,oBAA6C,GAAtB,mBAAsB,GAAA,kBAAA,GAAqB,kBAArB,GAA0C,KAAA,CAAM,KAAhD,CAAA;;;;;AA2PlC,cAAA,WAcX,EAdsB,MAAA,CAAA,MActB,CAAA,CAAA,MAAA,EARgB,MAAA,CAAO,MAQvB,EAAA,UAAA,EAAA,MAAA,EAAA,GARiD,MAAA,CAAA,MAQjD,CAAA,IAAA,EARiD,iBAQjD,GARiD,MAAA,CAAA,WAQjD,EAAA,KAAA,CAAA,EAAA,KAAA,EARiD,kBAQjD,GARiD,mBAQjD,GARiD,oBAQjD,GARiD,kBAQjD,CAAA"}
@@ -0,0 +1,34 @@
1
+ import { MimicServerConfigTag } from "./MimicConfig.mjs";
2
+ import { DocumentManagerTag } from "./DocumentManager.mjs";
3
+ import { MimicAuthServiceTag } from "./MimicAuthService.mjs";
4
+ import { PresenceManagerTag } from "./PresenceManager.mjs";
5
+ import { MessageParseError, MissingDocumentIdError } from "./errors.mjs";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Socket from "@effect/platform/Socket";
8
+ import * as Scope from "effect/Scope";
9
+
10
+ //#region src/WebSocketHandler.d.ts
11
+ declare namespace WebSocketHandler_d_exports {
12
+ export { extractDocumentId, handleConnection, makeHandler };
13
+ }
14
+ /**
15
+ * Extract document ID from URL path.
16
+ * Expected format: /doc/{documentId}
17
+ */
18
+ declare const extractDocumentId: (path: string) => Effect.Effect<string, MissingDocumentIdError>;
19
+ /**
20
+ * Handle a WebSocket connection for a document.
21
+ *
22
+ * @param socket - The Effect Platform Socket
23
+ * @param path - The URL path (e.g., "/doc/my-document-id")
24
+ * @returns An Effect that handles the connection lifecycle
25
+ */
26
+ declare const handleConnection: (socket: Socket.Socket, path: string) => Effect.Effect<void, Socket.SocketError | MissingDocumentIdError | MessageParseError, MimicServerConfigTag | MimicAuthServiceTag | DocumentManagerTag | PresenceManagerTag | Scope.Scope>;
27
+ /**
28
+ * Create a handler function for the WebSocket server.
29
+ * Returns a function that takes a socket and document ID.
30
+ */
31
+ declare const makeHandler: Effect.Effect<(socket: Socket.Socket, documentId: string) => Effect.Effect<void, MessageParseError | Socket.SocketError, never>, never, DocumentManagerTag | MimicAuthServiceTag | MimicServerConfigTag | PresenceManagerTag>;
32
+ //#endregion
33
+ export { WebSocketHandler_d_exports };
34
+ //# sourceMappingURL=WebSocketHandler.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebSocketHandler.d.mts","names":[],"sources":["../src/WebSocketHandler.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;AAyJa,cAAA,iBAEY,EAAA,CAAA,IAAA,EAAA,MAAA,EAAA,GAAtB,MAAA,CAAO,MAAA,CAAA,MAAM,EAAS,sBAAT,CAAA;AAuEhB;;;;;;;AAM+C,cANlC,gBAMkC,EAAA,CAAA,MAAA,EALrC,MAAA,CAAO,MAK8B,EAAA,IAAA,EAAA,MAAA,EAAA,GAH5C,MAAA,CAAO,MAGqC,CAAA,IAAA,EAD7C,MAAA,CAAO,WACsC,GADxB,sBACwB,GADC,iBACD,EAA7C,oBAA6C,GAAtB,mBAAsB,GAAA,kBAAA,GAAqB,kBAArB,GAA0C,KAAA,CAAM,KAAhD,CAAA;;;;;AA2PlC,cAAA,WAcX,EAdsB,MAAA,CAAA,MActB,CAAA,CAAA,MAAA,EARgB,MAAA,CAAO,MAQvB,EAAA,UAAA,EAAA,MAAA,EAAA,GARiD,MAAA,CAAA,MAQjD,CAAA,IAAA,EARiD,iBAQjD,GARiD,MAAA,CAAA,WAQjD,EAAA,KAAA,CAAA,EAAA,KAAA,EARiD,kBAQjD,GARiD,mBAQjD,GARiD,oBAQjD,GARiD,kBAQjD,CAAA"}