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

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 (58) 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 +3 -3
  12. package/dist/channel/proxy-channel.d.ts.map +1 -1
  13. package/dist/channel/proxy-channel.js +38 -19
  14. package/dist/channel/proxy-channel.js.map +1 -1
  15. package/dist/common.d.ts +36 -14
  16. package/dist/common.d.ts.map +1 -1
  17. package/dist/common.js +7 -4
  18. package/dist/common.js.map +1 -1
  19. package/dist/mesh-schema.d.ts +71 -5
  20. package/dist/mesh-schema.d.ts.map +1 -1
  21. package/dist/mesh-schema.js +55 -6
  22. package/dist/mesh-schema.js.map +1 -1
  23. package/dist/mod.d.ts +2 -2
  24. package/dist/mod.d.ts.map +1 -1
  25. package/dist/mod.js +2 -2
  26. package/dist/mod.js.map +1 -1
  27. package/dist/node.d.ts +43 -21
  28. package/dist/node.d.ts.map +1 -1
  29. package/dist/node.js +271 -95
  30. package/dist/node.js.map +1 -1
  31. package/dist/node.test.d.ts +1 -1
  32. package/dist/node.test.d.ts.map +1 -1
  33. package/dist/node.test.js +391 -156
  34. package/dist/node.test.js.map +1 -1
  35. package/dist/websocket-connection.d.ts +6 -7
  36. package/dist/websocket-connection.d.ts.map +1 -1
  37. package/dist/websocket-connection.js +21 -26
  38. package/dist/websocket-connection.js.map +1 -1
  39. package/dist/websocket-edge.d.ts +50 -0
  40. package/dist/websocket-edge.d.ts.map +1 -0
  41. package/dist/websocket-edge.js +69 -0
  42. package/dist/websocket-edge.js.map +1 -0
  43. package/dist/websocket-server.d.ts.map +1 -1
  44. package/dist/websocket-server.js +23 -9
  45. package/dist/websocket-server.js.map +1 -1
  46. package/package.json +7 -6
  47. package/src/channel/message-channel-internal.ts +356 -0
  48. package/src/channel/message-channel.ts +190 -310
  49. package/src/channel/proxy-channel.ts +259 -231
  50. package/src/common.ts +12 -13
  51. package/src/mesh-schema.ts +62 -6
  52. package/src/mod.ts +2 -2
  53. package/src/node.test.ts +554 -189
  54. package/src/node.ts +417 -134
  55. package/src/websocket-edge.ts +159 -0
  56. package/src/websocket-server.ts +26 -9
  57. package/tmp/pack.tgz +0 -0
  58. package/src/websocket-connection.ts +0 -158
@@ -0,0 +1,159 @@
1
+ import {
2
+ Deferred,
3
+ Effect,
4
+ Either,
5
+ Exit,
6
+ Queue,
7
+ Schedule,
8
+ Schema,
9
+ Scope,
10
+ Stream,
11
+ WebChannel,
12
+ WebSocket,
13
+ } from '@livestore/utils/effect'
14
+ import type * as NodeWebSocket from 'ws'
15
+
16
+ import * as WebmeshSchema from './mesh-schema.js'
17
+ import type { MeshNode } from './node.js'
18
+
19
+ export class WSEdgeInit extends Schema.TaggedStruct('WSEdgeInit', {
20
+ from: Schema.String,
21
+ }) {}
22
+
23
+ export class WSEdgePayload extends Schema.TaggedStruct('WSEdgePayload', {
24
+ from: Schema.String,
25
+ payload: Schema.Any,
26
+ }) {}
27
+
28
+ export class WSEdgeMessage extends Schema.Union(WSEdgeInit, WSEdgePayload) {}
29
+
30
+ export const MessageMsgPack = Schema.MsgPack(WSEdgeMessage)
31
+
32
+ export type SocketType =
33
+ | {
34
+ _tag: 'leaf'
35
+ from: string
36
+ }
37
+ | {
38
+ _tag: 'relay'
39
+ }
40
+
41
+ export const connectViaWebSocket = ({
42
+ node,
43
+ url,
44
+ reconnect = Schedule.exponential(100),
45
+ }: {
46
+ node: MeshNode
47
+ url: string
48
+ reconnect?: Schedule.Schedule<unknown> | false
49
+ }): Effect.Effect<void, never, Scope.Scope> =>
50
+ Effect.gen(function* () {
51
+ const disconnected = yield* Deferred.make<void>()
52
+
53
+ const socket = yield* WebSocket.makeWebSocket({ url, reconnect })
54
+
55
+ socket.addEventListener('close', () => Deferred.unsafeDone(disconnected, Exit.void))
56
+
57
+ const edgeChannel = yield* makeWebSocketEdge(socket, { _tag: 'leaf', from: node.nodeName })
58
+
59
+ yield* node.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
60
+
61
+ yield* disconnected
62
+ }).pipe(Effect.scoped, Effect.forever, Effect.catchTag('WebSocketError', Effect.orDie))
63
+
64
+ export const makeWebSocketEdge = (
65
+ socket: globalThis.WebSocket | NodeWebSocket.WebSocket,
66
+ socketType: SocketType,
67
+ ): Effect.Effect<
68
+ {
69
+ webChannel: WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
70
+ from: string
71
+ },
72
+ never,
73
+ Scope.Scope
74
+ > =>
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 === 'WSEdgeInit') {
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: 'WSEdgeInit', 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) =>
136
+ Effect.gen(function* () {
137
+ yield* isConnectedLatch.await
138
+ const payload = yield* Schema.encode(schema.send)(message)
139
+ socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgePayload', payload, from }))
140
+ })
141
+
142
+ const listen = Stream.fromQueue(listenQueue).pipe(
143
+ Stream.map(Either.right),
144
+ WebChannel.listenToDebugPing('websocket-edge'),
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('makeWebSocketEdge')),
159
+ )
@@ -1,9 +1,10 @@
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'
6
- import { makeWebSocketConnection } from './websocket-connection.js'
7
+ import { makeWebSocketEdge } from './websocket-edge.js'
7
8
 
8
9
  export const makeWebSocketServer = ({
9
10
  relayNodeName,
@@ -13,27 +14,43 @@ 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* () {
23
- const { webChannel, from } = yield* makeWebSocketConnection(socket, { _tag: 'relay' })
40
+ const { webChannel, from } = yield* makeWebSocketEdge(socket, { _tag: 'relay' })
24
41
 
25
- yield* node.addConnection({ target: from, connectionChannel: webChannel, replaceIfExists: true })
26
- yield* Effect.log(`WS Relay ${relayNodeName}: added connection from '${from}'`)
42
+ yield* node.addEdge({ target: from, edgeChannel: webChannel, replaceIfExists: true })
43
+ yield* Effect.log(`WS Relay ${relayNodeName}: added edge from '${from}'`)
27
44
 
28
45
  socket.addEventListener('close', () =>
29
46
  Effect.gen(function* () {
30
- yield* node.removeConnection(from)
31
- yield* Effect.log(`WS Relay ${relayNodeName}: removed connection from '${from}'`)
32
- }).pipe(Effect.provide(runtime), Effect.runFork),
47
+ yield* node.removeEdge(from)
48
+ yield* Effect.log(`WS Relay ${relayNodeName}: removed edge from '${from}'`)
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
@@ -1,158 +0,0 @@
1
- import type { Scope } from '@livestore/utils/effect'
2
- import {
3
- Deferred,
4
- Effect,
5
- Either,
6
- FiberHandle,
7
- Queue,
8
- Schedule,
9
- Schema,
10
- Stream,
11
- WebChannel,
12
- WebSocket,
13
- } from '@livestore/utils/effect'
14
- import type NodeWebSocket from 'ws'
15
-
16
- import * as MeshSchema from './mesh-schema.js'
17
- import type { MeshNode } from './node.js'
18
-
19
- export class WSConnectionInit extends Schema.TaggedStruct('WSConnectionInit', {
20
- from: Schema.String,
21
- }) {}
22
-
23
- export class WSConnectionPayload extends Schema.TaggedStruct('WSConnectionPayload', {
24
- from: Schema.String,
25
- payload: Schema.Any,
26
- }) {}
27
-
28
- export class WSConnectionMessage extends Schema.Union(WSConnectionInit, WSConnectionPayload) {}
29
-
30
- export const MessageMsgPack = Schema.MsgPack(WSConnectionMessage)
31
-
32
- export type SocketType =
33
- | {
34
- _tag: 'leaf'
35
- from: string
36
- }
37
- | {
38
- _tag: 'relay'
39
- }
40
-
41
- export const connectViaWebSocket = ({
42
- node,
43
- url,
44
- reconnect = Schedule.exponential(100),
45
- }: {
46
- node: MeshNode
47
- url: string
48
- reconnect?: Schedule.Schedule<unknown> | false
49
- }): Effect.Effect<void, never, Scope.Scope> =>
50
- Effect.gen(function* () {
51
- const fiberHandle = yield* FiberHandle.make()
52
-
53
- const connect = Effect.gen(function* () {
54
- const socket = yield* WebSocket.makeWebSocket({ url, reconnect })
55
-
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))
58
-
59
- const connection = yield* makeWebSocketConnection(socket, { _tag: 'leaf', from: node.nodeName })
60
-
61
- yield* node.addConnection({ target: 'ws', connectionChannel: connection.webChannel, replaceIfExists: true })
62
-
63
- yield* Effect.never
64
- }).pipe(Effect.scoped, Effect.withSpan('@livestore/webmesh:websocket-connection:connect'))
65
-
66
- yield* FiberHandle.run(fiberHandle, connect)
67
- })
68
-
69
- export const makeWebSocketConnection = (
70
- socket: globalThis.WebSocket | NodeWebSocket.WebSocket,
71
- socketType: SocketType,
72
- ): Effect.Effect<
73
- {
74
- webChannel: WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
75
- from: string
76
- },
77
- never,
78
- Scope.Scope
79
- > =>
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) =>
89
- 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
- })