@livestore/webmesh 0.3.0-dev.2 → 0.3.0-dev.21

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 (48) hide show
  1. package/README.md +26 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/message-channel-internal.d.ts +26 -0
  4. package/dist/channel/message-channel-internal.d.ts.map +1 -0
  5. package/dist/channel/message-channel-internal.js +217 -0
  6. package/dist/channel/message-channel-internal.js.map +1 -0
  7. package/dist/channel/message-channel.d.ts +21 -19
  8. package/dist/channel/message-channel.d.ts.map +1 -1
  9. package/dist/channel/message-channel.js +132 -162
  10. package/dist/channel/message-channel.js.map +1 -1
  11. package/dist/channel/proxy-channel.d.ts +2 -2
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +30 -11
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +32 -5
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +2 -1
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +68 -2
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +53 -4
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/node.d.ts +31 -9
  24. package/dist/node.d.ts.map +1 -1
  25. package/dist/node.js +225 -49
  26. package/dist/node.js.map +1 -1
  27. package/dist/node.test.d.ts +1 -1
  28. package/dist/node.test.d.ts.map +1 -1
  29. package/dist/node.test.js +384 -149
  30. package/dist/node.test.js.map +1 -1
  31. package/dist/websocket-connection.d.ts +5 -6
  32. package/dist/websocket-connection.d.ts.map +1 -1
  33. package/dist/websocket-connection.js +21 -26
  34. package/dist/websocket-connection.js.map +1 -1
  35. package/dist/websocket-server.d.ts.map +1 -1
  36. package/dist/websocket-server.js +17 -3
  37. package/dist/websocket-server.js.map +1 -1
  38. package/package.json +7 -6
  39. package/src/channel/message-channel-internal.ts +356 -0
  40. package/src/channel/message-channel.ts +190 -310
  41. package/src/channel/proxy-channel.ts +257 -229
  42. package/src/common.ts +4 -2
  43. package/src/mesh-schema.ts +60 -4
  44. package/src/node.test.ts +544 -179
  45. package/src/node.ts +363 -69
  46. package/src/websocket-connection.ts +96 -95
  47. package/src/websocket-server.ts +20 -3
  48. package/tmp/pack.tgz +0 -0
@@ -1,19 +1,19 @@
1
- import type { Scope } from '@livestore/utils/effect'
2
1
  import {
3
2
  Deferred,
4
3
  Effect,
5
4
  Either,
6
- FiberHandle,
5
+ Exit,
7
6
  Queue,
8
7
  Schedule,
9
8
  Schema,
9
+ Scope,
10
10
  Stream,
11
11
  WebChannel,
12
12
  WebSocket,
13
13
  } from '@livestore/utils/effect'
14
- import type NodeWebSocket from 'ws'
14
+ import type * as NodeWebSocket from 'ws'
15
15
 
16
- import * as MeshSchema from './mesh-schema.js'
16
+ import * as WebmeshSchema from './mesh-schema.js'
17
17
  import type { MeshNode } from './node.js'
18
18
 
19
19
  export class WSConnectionInit extends Schema.TaggedStruct('WSConnectionInit', {
@@ -48,111 +48,112 @@ export const connectViaWebSocket = ({
48
48
  reconnect?: Schedule.Schedule<unknown> | false
49
49
  }): Effect.Effect<void, never, Scope.Scope> =>
50
50
  Effect.gen(function* () {
51
- const fiberHandle = yield* FiberHandle.make()
51
+ const disconnected = yield* Deferred.make<void>()
52
52
 
53
- const connect = Effect.gen(function* () {
54
- const socket = yield* WebSocket.makeWebSocket({ url, reconnect })
53
+ const socket = yield* WebSocket.makeWebSocket({ url, reconnect })
55
54
 
56
- // NOTE we want to use `runFork` here so this Effect is not part of the fiber that will be interrupted
57
- socket.addEventListener('close', () => FiberHandle.run(fiberHandle, connect).pipe(Effect.runFork))
55
+ socket.addEventListener('close', () => Deferred.unsafeDone(disconnected, Exit.void))
58
56
 
59
- const connection = yield* makeWebSocketConnection(socket, { _tag: 'leaf', from: node.nodeName })
57
+ const connection = yield* makeWebSocketConnection(socket, { _tag: 'leaf', from: node.nodeName })
60
58
 
61
- yield* node.addConnection({ target: 'ws', connectionChannel: connection.webChannel, replaceIfExists: true })
59
+ yield* node.addConnection({ target: 'ws', connectionChannel: connection.webChannel, replaceIfExists: true })
62
60
 
63
- yield* Effect.never
64
- }).pipe(Effect.scoped, Effect.withSpan('@livestore/webmesh:websocket-connection:connect'))
65
-
66
- yield* FiberHandle.run(fiberHandle, connect)
67
- })
61
+ yield* disconnected
62
+ }).pipe(Effect.scoped, Effect.forever, Effect.catchTag('WebSocketError', Effect.orDie))
68
63
 
69
64
  export const makeWebSocketConnection = (
70
65
  socket: globalThis.WebSocket | NodeWebSocket.WebSocket,
71
66
  socketType: SocketType,
72
67
  ): Effect.Effect<
73
68
  {
74
- webChannel: WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
69
+ webChannel: WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
75
70
  from: string
76
71
  },
77
72
  never,
78
73
  Scope.Scope
79
74
  > =>
80
- Effect.gen(function* () {
81
- socket.binaryType = 'arraybuffer'
82
-
83
- const fromDeferred = yield* Deferred.make<string>()
84
-
85
- yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
86
- Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
87
- Stream.flatten(),
88
- Stream.tap((msg) =>
75
+ Effect.scopeWithCloseable((scope) =>
76
+ Effect.gen(function* () {
77
+ socket.binaryType = 'arraybuffer'
78
+
79
+ const fromDeferred = yield* Deferred.make<string>()
80
+
81
+ const listenQueue = yield* Queue.unbounded<typeof WebmeshSchema.Packet.Type>().pipe(
82
+ Effect.acquireRelease(Queue.shutdown),
83
+ )
84
+
85
+ const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
86
+
87
+ yield* Stream.fromEventListener<MessageEvent>(socket as any, 'message').pipe(
88
+ Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))),
89
+ Stream.flatten(),
90
+ Stream.tap((msg) =>
91
+ Effect.gen(function* () {
92
+ if (msg._tag === 'WSConnectionInit') {
93
+ yield* Deferred.succeed(fromDeferred, msg.from)
94
+ } else {
95
+ const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
96
+ yield* Queue.offer(listenQueue, decodedPayload)
97
+ }
98
+ }),
99
+ ),
100
+ Stream.runDrain,
101
+ Effect.interruptible,
102
+ Effect.tapCauseLogPretty,
103
+ Effect.forkScoped,
104
+ )
105
+
106
+ const initHandshake = (from: string) =>
107
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
108
+
109
+ if (socketType._tag === 'leaf') {
110
+ initHandshake(socketType.from)
111
+ }
112
+
113
+ const deferredResult = yield* fromDeferred
114
+ const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
115
+
116
+ if (socketType._tag === 'relay') {
117
+ initHandshake(from)
118
+ }
119
+
120
+ const isConnectedLatch = yield* Effect.makeLatch(true)
121
+
122
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
123
+
124
+ yield* Effect.eventListener<any>(
125
+ socket,
126
+ 'close',
127
+ () =>
128
+ Effect.gen(function* () {
129
+ yield* isConnectedLatch.close
130
+ yield* Deferred.succeed(closedDeferred, undefined)
131
+ }),
132
+ { once: true },
133
+ )
134
+
135
+ const send = (message: typeof WebmeshSchema.Packet.Type) =>
89
136
  Effect.gen(function* () {
90
- if (msg._tag === 'WSConnectionInit') {
91
- yield* Deferred.succeed(fromDeferred, msg.from)
92
- } else {
93
- const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload)
94
- yield* Queue.offer(listenQueue, decodedPayload)
95
- }
96
- }),
97
- ),
98
- Stream.runDrain,
99
- Effect.tapCauseLogPretty,
100
- Effect.forkScoped,
101
- )
102
-
103
- const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
104
- Effect.acquireRelease(Queue.shutdown),
105
- )
106
-
107
- const initHandshake = (from: string) =>
108
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
109
-
110
- if (socketType._tag === 'leaf') {
111
- initHandshake(socketType.from)
112
- }
113
-
114
- const deferredResult = yield* fromDeferred
115
- const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
116
-
117
- if (socketType._tag === 'relay') {
118
- initHandshake(from)
119
- }
120
-
121
- const isConnectedLatch = yield* Effect.makeLatch(true)
122
-
123
- const closedDeferred = yield* Deferred.make<void>()
124
-
125
- yield* Effect.eventListener<any>(
126
- socket,
127
- 'close',
128
- () =>
129
- Effect.gen(function* () {
130
- yield* isConnectedLatch.close
131
- yield* Deferred.succeed(closedDeferred, undefined)
132
- }),
133
- { once: true },
134
- )
135
-
136
- const send = (message: typeof MeshSchema.Packet.Type) =>
137
- Effect.gen(function* () {
138
- yield* isConnectedLatch.await
139
- const payload = yield* Schema.encode(MeshSchema.Packet)(message)
140
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
141
- })
142
-
143
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
144
-
145
- const webChannel = {
146
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
147
- send,
148
- listen,
149
- closedDeferred,
150
- schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
151
- supportsTransferables: false,
152
- } satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
153
-
154
- return {
155
- webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
156
- from,
157
- }
158
- })
137
+ yield* isConnectedLatch.await
138
+ const payload = yield* Schema.encode(schema.send)(message)
139
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
140
+ })
141
+
142
+ const listen = Stream.fromQueue(listenQueue).pipe(
143
+ Stream.map(Either.right),
144
+ WebChannel.listenToDebugPing('websocket-connection'),
145
+ )
146
+
147
+ const webChannel = {
148
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
149
+ send,
150
+ listen,
151
+ closedDeferred,
152
+ schema,
153
+ supportsTransferables: false,
154
+ shutdown: Scope.close(scope, Exit.void),
155
+ } satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
156
+
157
+ return { webChannel, from }
158
+ }).pipe(Effect.withSpanScoped('makeWebSocketConnection')),
159
+ )
@@ -1,5 +1,6 @@
1
+ import { UnexpectedError } from '@livestore/common'
1
2
  import type { Scope } from '@livestore/utils/effect'
2
- import { Effect } from '@livestore/utils/effect'
3
+ import { Effect, FiberSet } from '@livestore/utils/effect'
3
4
  import * as WebSocket from 'ws'
4
5
 
5
6
  import { makeMeshNode } from './node.js'
@@ -13,10 +14,26 @@ export const makeWebSocketServer = ({
13
14
  Effect.gen(function* () {
14
15
  const server = new WebSocket.WebSocketServer({ noServer: true })
15
16
 
17
+ yield* Effect.addFinalizer(() =>
18
+ Effect.async<void, UnexpectedError>((cb) => {
19
+ server.close((cause) => {
20
+ if (cause) {
21
+ cb(Effect.fail(UnexpectedError.make({ cause })))
22
+ } else {
23
+ server.removeAllListeners()
24
+ server.clients.forEach((client) => client.terminate())
25
+ cb(Effect.succeed(undefined))
26
+ }
27
+ })
28
+ }).pipe(Effect.orDie),
29
+ )
30
+
16
31
  const node = yield* makeMeshNode(relayNodeName)
17
32
 
18
33
  const runtime = yield* Effect.runtime<never>()
19
34
 
35
+ const fiberSet = yield* FiberSet.make()
36
+
20
37
  // TODO handle node disconnects (i.e. remove respective connection)
21
38
  server.on('connection', (socket) => {
22
39
  Effect.gen(function* () {
@@ -29,11 +46,11 @@ export const makeWebSocketServer = ({
29
46
  Effect.gen(function* () {
30
47
  yield* node.removeConnection(from)
31
48
  yield* Effect.log(`WS Relay ${relayNodeName}: removed connection from '${from}'`)
32
- }).pipe(Effect.provide(runtime), Effect.runFork),
49
+ }).pipe(Effect.provide(runtime), Effect.tapCauseLogPretty, Effect.runFork),
33
50
  )
34
51
 
35
52
  yield* Effect.never
36
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
53
+ }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.provide(runtime), FiberSet.run(fiberSet), Effect.runFork)
37
54
  })
38
55
 
39
56
  return server
package/tmp/pack.tgz ADDED
Binary file