@livestore/webmesh 0.3.0-dev.10 → 0.3.0-dev.12
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 +20 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel copy.d.ts +9 -0
- package/dist/channel/message-channel copy.d.ts.map +1 -0
- package/dist/channel/message-channel copy.js +137 -0
- package/dist/channel/message-channel copy.js.map +1 -0
- package/dist/channel/message-channel-internal copy.d.ts +42 -0
- package/dist/channel/message-channel-internal copy.d.ts.map +1 -0
- package/dist/channel/message-channel-internal copy.js +239 -0
- package/dist/channel/message-channel-internal copy.js.map +1 -0
- 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 +128 -162
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +2 -2
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +7 -5
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +8 -4
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +2 -1
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +23 -1
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +21 -2
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +12 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +40 -9
- 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 +300 -147
- package/dist/node.test.js.map +1 -1
- package/dist/websocket-connection.d.ts +1 -2
- package/dist/websocket-connection.d.ts.map +1 -1
- package/dist/websocket-connection.js +5 -4
- package/dist/websocket-connection.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +356 -0
- package/src/channel/message-channel.ts +183 -311
- package/src/channel/proxy-channel.ts +238 -230
- package/src/common.ts +3 -1
- package/src/mesh-schema.ts +20 -2
- package/src/node.test.ts +426 -177
- package/src/node.ts +70 -12
- package/src/websocket-connection.ts +83 -79
package/src/node.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
|
1
|
+
import { indent, LS_DEV, shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import type { Scope } from '@livestore/utils/effect'
|
|
3
3
|
import {
|
|
4
4
|
Cause,
|
|
@@ -29,6 +29,8 @@ export interface MeshNode {
|
|
|
29
29
|
|
|
30
30
|
debug: {
|
|
31
31
|
print: () => void
|
|
32
|
+
/** Sends a ping message to all connected nodes and channels */
|
|
33
|
+
ping: (payload?: string) => void
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -58,7 +60,16 @@ export interface MeshNode {
|
|
|
58
60
|
/**
|
|
59
61
|
* Tries to broker a MessageChannel connection between the nodes, otherwise will proxy messages via hop-nodes
|
|
60
62
|
*
|
|
61
|
-
* For a channel to successfully open, both sides need to have a connection and call `makeChannel
|
|
63
|
+
* For a channel to successfully open, both sides need to have a connection and call `makeChannel`.
|
|
64
|
+
*
|
|
65
|
+
* Example:
|
|
66
|
+
* ```ts
|
|
67
|
+
* // Code on node A
|
|
68
|
+
* const channel = nodeA.makeChannel({ target: 'B', channelName: 'my-channel', schema: ... })
|
|
69
|
+
*
|
|
70
|
+
* // Code on node B
|
|
71
|
+
* const channel = nodeB.makeChannel({ target: 'A', channelName: 'my-channel', schema: ... })
|
|
72
|
+
* ```
|
|
62
73
|
*/
|
|
63
74
|
makeChannel: <MsgListen, MsgSend>(args: {
|
|
64
75
|
target: MeshNodeName
|
|
@@ -145,6 +156,8 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
145
156
|
|
|
146
157
|
const sendPacket = (packet: typeof MeshSchema.Packet.Type) =>
|
|
147
158
|
Effect.gen(function* () {
|
|
159
|
+
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
160
|
+
|
|
148
161
|
if (Schema.is(MeshSchema.NetworkConnectionAdded)(packet)) {
|
|
149
162
|
yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
|
|
150
163
|
yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
|
|
@@ -330,7 +343,12 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
330
343
|
const schema = WebChannel.mapSchema(inputSchema)
|
|
331
344
|
const channelKey = `${target}-${channelName}` satisfies ChannelKey
|
|
332
345
|
|
|
333
|
-
if (
|
|
346
|
+
if (channelMap.has(channelKey)) {
|
|
347
|
+
const existingChannel = channelMap.get(channelKey)!.debugInfo?.channel
|
|
348
|
+
if (existingChannel) {
|
|
349
|
+
shouldNeverHappen(`Channel ${channelKey} already exists`, existingChannel)
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
334
352
|
const queue = yield* Queue.unbounded<MessageQueueItem | ProxyQueueItem>().pipe(
|
|
335
353
|
Effect.acquireRelease(Queue.shutdown),
|
|
336
354
|
)
|
|
@@ -342,12 +360,23 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
342
360
|
yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
|
|
343
361
|
|
|
344
362
|
if (mode === 'messagechannel') {
|
|
345
|
-
|
|
363
|
+
const incomingPacketsQueue = yield* Queue.unbounded<any>()
|
|
364
|
+
|
|
365
|
+
// We're we're draining the queue into another new queue.
|
|
366
|
+
// It's a bit of a mystery why this is needed, since the unit tests also work without it.
|
|
367
|
+
// But for the LiveStore devtools to actually work, we need to do this.
|
|
368
|
+
// We should figure out some day why this is needed and further simplify if possible.
|
|
369
|
+
yield* Queue.takeBetween(queue, 1, 10).pipe(
|
|
370
|
+
Effect.tap((_) => Queue.offerAll(incomingPacketsQueue, _)),
|
|
371
|
+
Effect.forever,
|
|
372
|
+
Effect.tapCauseLogPretty,
|
|
373
|
+
Effect.forkScoped,
|
|
374
|
+
)
|
|
346
375
|
|
|
347
376
|
// NOTE already retries internally when transferables are required
|
|
348
|
-
const
|
|
377
|
+
const { webChannel, initialConnectionDeferred } = yield* makeMessageChannel({
|
|
349
378
|
nodeName,
|
|
350
|
-
|
|
379
|
+
incomingPacketsQueue,
|
|
351
380
|
newConnectionAvailablePubSub,
|
|
352
381
|
target,
|
|
353
382
|
channelName,
|
|
@@ -356,9 +385,11 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
356
385
|
checkTransferableConnections,
|
|
357
386
|
})
|
|
358
387
|
|
|
359
|
-
channelMap.set(channelKey, { queue, debugInfo: { channel, target } })
|
|
388
|
+
channelMap.set(channelKey, { queue, debugInfo: { channel: webChannel, target } })
|
|
360
389
|
|
|
361
|
-
|
|
390
|
+
yield* initialConnectionDeferred
|
|
391
|
+
|
|
392
|
+
return webChannel
|
|
362
393
|
} else {
|
|
363
394
|
const channel = yield* makeProxyChannel({
|
|
364
395
|
nodeName,
|
|
@@ -375,6 +406,7 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
375
406
|
return channel
|
|
376
407
|
}
|
|
377
408
|
}).pipe(
|
|
409
|
+
// Effect.timeout(timeout),
|
|
378
410
|
Effect.withSpanScoped(`makeChannel:${nodeName}→${target}(${channelName})`, {
|
|
379
411
|
attributes: { target, channelName, mode, timeout },
|
|
380
412
|
}),
|
|
@@ -393,15 +425,41 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
393
425
|
console.log('Channels:', channelMap.size)
|
|
394
426
|
for (const [key, value] of channelMap) {
|
|
395
427
|
console.log(
|
|
396
|
-
|
|
397
|
-
` Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`,
|
|
398
|
-
value.queue,
|
|
428
|
+
indent(key, 2),
|
|
399
429
|
'\n',
|
|
400
|
-
|
|
430
|
+
Object.entries({
|
|
431
|
+
target: value.debugInfo?.target,
|
|
432
|
+
supportsTransferables: value.debugInfo?.channel.supportsTransferables,
|
|
433
|
+
...value.debugInfo?.channel.debugInfo,
|
|
434
|
+
})
|
|
435
|
+
.map(([key, value]) => indent(`${key}=${value}`, 4))
|
|
436
|
+
.join('\n'),
|
|
437
|
+
' ',
|
|
401
438
|
value.debugInfo?.channel,
|
|
439
|
+
'\n',
|
|
440
|
+
indent(`Queue: ${value.queue.unsafeSize().pipe(Option.getOrUndefined)}`, 4),
|
|
441
|
+
value.queue,
|
|
402
442
|
)
|
|
403
443
|
}
|
|
404
444
|
},
|
|
445
|
+
ping: (payload) => {
|
|
446
|
+
Effect.gen(function* () {
|
|
447
|
+
const msg = (via: string) => {
|
|
448
|
+
console.log(`sending message to ${via}`)
|
|
449
|
+
return WebChannel.DebugPingMessage.make({ message: `ping from ${nodeName} via ${via}`, payload })
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
yield* Effect.forEach(connectionChannels, ([channelName, con]) =>
|
|
453
|
+
con.channel.send(msg(`connection ${channelName}`) as any),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
yield* Effect.forEach(
|
|
457
|
+
channelMap,
|
|
458
|
+
([channelKey, channel]) =>
|
|
459
|
+
channel.debugInfo?.channel.send(msg(`channel ${channelKey}`) as any) ?? Effect.void,
|
|
460
|
+
)
|
|
461
|
+
}).pipe(Effect.runFork)
|
|
462
|
+
},
|
|
405
463
|
}
|
|
406
464
|
|
|
407
465
|
return { nodeName, addConnection, removeConnection, makeChannel, connectionKeys, debug } satisfies MeshNode
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { Scope } from '@livestore/utils/effect'
|
|
2
1
|
import {
|
|
3
2
|
Deferred,
|
|
4
3
|
Effect,
|
|
5
4
|
Either,
|
|
5
|
+
Exit,
|
|
6
6
|
FiberHandle,
|
|
7
7
|
Queue,
|
|
8
8
|
Schedule,
|
|
9
9
|
Schema,
|
|
10
|
+
Scope,
|
|
10
11
|
Stream,
|
|
11
12
|
WebChannel,
|
|
12
13
|
WebSocket,
|
|
@@ -77,82 +78,85 @@ export const makeWebSocketConnection = (
|
|
|
77
78
|
never,
|
|
78
79
|
Scope.Scope
|
|
79
80
|
> =>
|
|
80
|
-
Effect.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Stream.
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
Effect.scopeWithCloseable((scope) =>
|
|
82
|
+
Effect.gen(function* () {
|
|
83
|
+
socket.binaryType = 'arraybuffer'
|
|
84
|
+
|
|
85
|
+
const fromDeferred = yield* Deferred.make<string>()
|
|
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(MeshSchema.Packet)(msg.payload)
|
|
96
|
+
yield* Queue.offer(listenQueue, decodedPayload)
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
100
|
+
Stream.runDrain,
|
|
101
|
+
Effect.tapCauseLogPretty,
|
|
102
|
+
Effect.forkScoped,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
const listenQueue = yield* Queue.unbounded<typeof MeshSchema.Packet.Type>().pipe(
|
|
106
|
+
Effect.acquireRelease(Queue.shutdown),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
const initHandshake = (from: string) =>
|
|
110
|
+
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }))
|
|
111
|
+
|
|
112
|
+
if (socketType._tag === 'leaf') {
|
|
113
|
+
initHandshake(socketType.from)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const deferredResult = yield* fromDeferred
|
|
117
|
+
const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
|
|
118
|
+
|
|
119
|
+
if (socketType._tag === 'relay') {
|
|
120
|
+
initHandshake(from)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isConnectedLatch = yield* Effect.makeLatch(true)
|
|
124
|
+
|
|
125
|
+
const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
|
|
126
|
+
|
|
127
|
+
yield* Effect.eventListener<any>(
|
|
128
|
+
socket,
|
|
129
|
+
'close',
|
|
130
|
+
() =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
yield* isConnectedLatch.close
|
|
133
|
+
yield* Deferred.succeed(closedDeferred, undefined)
|
|
134
|
+
}),
|
|
135
|
+
{ once: true },
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
const send = (message: typeof MeshSchema.Packet.Type) =>
|
|
89
139
|
Effect.gen(function* () {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
})
|
|
140
|
+
yield* isConnectedLatch.await
|
|
141
|
+
const payload = yield* Schema.encode(MeshSchema.Packet)(message)
|
|
142
|
+
socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
|
|
146
|
+
|
|
147
|
+
const webChannel = {
|
|
148
|
+
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
149
|
+
send,
|
|
150
|
+
listen,
|
|
151
|
+
closedDeferred,
|
|
152
|
+
schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
|
|
153
|
+
supportsTransferables: false,
|
|
154
|
+
shutdown: Scope.close(scope, Exit.void),
|
|
155
|
+
} satisfies WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
webChannel: webChannel as WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>,
|
|
159
|
+
from,
|
|
160
|
+
}
|
|
161
|
+
}).pipe(Effect.withSpanScoped('makeWebSocketConnection')),
|
|
162
|
+
)
|