@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.
- package/README.md +26 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts +26 -0
- package/dist/channel/message-channel-internal.d.ts.map +1 -0
- package/dist/channel/message-channel-internal.js +217 -0
- package/dist/channel/message-channel-internal.js.map +1 -0
- package/dist/channel/message-channel.d.ts +21 -19
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +132 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +3 -3
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +38 -19
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +36 -14
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +7 -4
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +71 -5
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +55 -6
- package/dist/mesh-schema.js.map +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +2 -2
- package/dist/mod.js.map +1 -1
- package/dist/node.d.ts +43 -21
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +271 -95
- package/dist/node.js.map +1 -1
- package/dist/node.test.d.ts +1 -1
- package/dist/node.test.d.ts.map +1 -1
- package/dist/node.test.js +391 -156
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +6 -7
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +21 -26
- package/dist/websocket-connection.js.map +1 -1
- package/dist/websocket-edge.d.ts +50 -0
- package/dist/websocket-edge.d.ts.map +1 -0
- package/dist/websocket-edge.js +69 -0
- package/dist/websocket-edge.js.map +1 -0
- package/dist/websocket-server.d.ts.map +1 -1
- package/dist/websocket-server.js +23 -9
- package/dist/websocket-server.js.map +1 -1
- package/package.json +7 -6
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +190 -310
- package/src/channel/proxy-channel.ts +259 -231
- package/src/common.ts +12 -13
- package/src/mesh-schema.ts +62 -6
- package/src/mod.ts +2 -2
- package/src/node.test.ts +554 -189
- package/src/node.ts +417 -134
- package/src/websocket-edge.ts +159 -0
- package/src/websocket-server.ts +26 -9
- package/tmp/pack.tgz +0 -0
- 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
|
+
)
|
package/src/websocket-server.ts
CHANGED
|
@@ -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 {
|
|
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*
|
|
40
|
+
const { webChannel, from } = yield* makeWebSocketEdge(socket, { _tag: 'relay' })
|
|
24
41
|
|
|
25
|
-
yield* node.
|
|
26
|
-
yield* Effect.log(`WS Relay ${relayNodeName}: added
|
|
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.
|
|
31
|
-
yield* Effect.log(`WS Relay ${relayNodeName}: removed
|
|
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
|
-
})
|