@livestore/webmesh 0.4.0 → 0.5.0-dev.0
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/dist/.tsbuildinfo +1 -1
- package/dist/channel/direct-channel-internal.d.ts +2 -3
- package/dist/channel/direct-channel-internal.d.ts.map +1 -1
- package/dist/channel/direct-channel-internal.js +23 -18
- package/dist/channel/direct-channel-internal.js.map +1 -1
- package/dist/channel/direct-channel.d.ts.map +1 -1
- package/dist/channel/direct-channel.js +20 -21
- package/dist/channel/direct-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +7 -8
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +26 -22
- package/dist/channel/proxy-channel.js.map +1 -1
- package/dist/common.d.ts +33 -35
- package/dist/common.d.ts.map +1 -1
- package/dist/common.js +3 -2
- package/dist/common.js.map +1 -1
- package/dist/mesh-schema.d.ts +262 -136
- package/dist/mesh-schema.d.ts.map +1 -1
- package/dist/mesh-schema.js +45 -44
- package/dist/mesh-schema.js.map +1 -1
- package/dist/node.d.ts +6 -6
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +22 -21
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +74 -71
- package/dist/node.test.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/websocket-edge.d.ts +18 -16
- package/dist/websocket-edge.d.ts.map +1 -1
- package/dist/websocket-edge.js +30 -36
- package/dist/websocket-edge.js.map +1 -1
- package/dist/websocket-edge.test.js +14 -14
- package/dist/websocket-edge.test.js.map +1 -1
- package/dist/worker/mod.d.ts +24 -0
- package/dist/worker/mod.d.ts.map +1 -0
- package/dist/worker/mod.js +44 -0
- package/dist/worker/mod.js.map +1 -0
- package/dist/worker/schema.d.ts +10 -0
- package/dist/worker/schema.d.ts.map +1 -0
- package/dist/worker/schema.js +7 -0
- package/dist/worker/schema.js.map +1 -0
- package/package.json +20 -37
- package/src/channel/direct-channel-internal.ts +30 -22
- package/src/channel/direct-channel.ts +26 -23
- package/src/channel/proxy-channel.ts +39 -36
- package/src/common.ts +6 -3
- package/src/mesh-schema.ts +34 -33
- package/src/node.test.ts +99 -86
- package/src/node.ts +32 -32
- package/src/utils.ts +3 -3
- package/src/websocket-edge.test.ts +14 -14
- package/src/websocket-edge.ts +47 -41
- package/src/worker/mod.ts +89 -0
- package/src/worker/schema.ts +8 -0
package/src/node.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
Effect,
|
|
7
7
|
Exit,
|
|
8
8
|
Fiber,
|
|
9
|
-
Option,
|
|
10
9
|
PubSub,
|
|
11
10
|
Queue,
|
|
12
11
|
Schema,
|
|
@@ -61,7 +60,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
61
60
|
}): Effect.Effect<void, EdgeAlreadyExistsError, Scope.Scope>
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.
|
|
63
|
+
removeEdge: (targetNodeName: MeshNodeName) => Effect.Effect<void, Cause.NoSuchElementError>
|
|
65
64
|
|
|
66
65
|
hasChannel: ({
|
|
67
66
|
target,
|
|
@@ -93,10 +92,10 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
93
92
|
*/
|
|
94
93
|
channelName: string
|
|
95
94
|
schema:
|
|
96
|
-
| Schema.
|
|
95
|
+
| Schema.Codec<MsgListen | MsgSend, any>
|
|
97
96
|
| {
|
|
98
|
-
listen: Schema.
|
|
99
|
-
send: Schema.
|
|
97
|
+
listen: Schema.Codec<MsgListen, any>
|
|
98
|
+
send: Schema.Codec<MsgSend, any>
|
|
100
99
|
}
|
|
101
100
|
/**
|
|
102
101
|
* If possible, prefer using a DirectChannel with transferables (i.e. transferring memory instead of copying it).
|
|
@@ -107,7 +106,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
107
106
|
*
|
|
108
107
|
* @default 1 second
|
|
109
108
|
*/
|
|
110
|
-
timeout?: Duration.
|
|
109
|
+
timeout?: Duration.Input
|
|
111
110
|
/**
|
|
112
111
|
* If true, will close an existing channel if it exists.
|
|
113
112
|
*
|
|
@@ -129,7 +128,7 @@ export interface MeshNode<TName extends MeshNodeName = MeshNodeName> {
|
|
|
129
128
|
*/
|
|
130
129
|
makeBroadcastChannel: <Msg>(args: {
|
|
131
130
|
channelName: string
|
|
132
|
-
schema: Schema.
|
|
131
|
+
schema: Schema.Codec<Msg, any>
|
|
133
132
|
}) => Effect.Effect<WebChannel.WebChannel<Msg, Msg>, never, Scope.Scope>
|
|
134
133
|
}
|
|
135
134
|
|
|
@@ -137,16 +136,17 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
137
136
|
nodeName: TName,
|
|
138
137
|
): Effect.Effect<MeshNode<TName>, never, Scope.Scope> =>
|
|
139
138
|
Effect.gen(function* () {
|
|
140
|
-
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.
|
|
139
|
+
const edgeChannels = new Map<MeshNodeName, { channel: EdgeChannel; listenFiber: Fiber.Fiber<void> }>()
|
|
141
140
|
|
|
142
141
|
// To avoid unbounded memory growth, we automatically forget about packet ids after a while
|
|
143
142
|
const handledPacketIds = yield* TimeoutSet.make(Duration.minutes(1))
|
|
144
143
|
|
|
145
|
-
const newEdgeAvailablePubSub = yield* PubSub.unbounded<MeshNodeName>()
|
|
144
|
+
const newEdgeAvailablePubSub = yield* Effect.acquireRelease(PubSub.unbounded<MeshNodeName>(), PubSub.shutdown)
|
|
146
145
|
|
|
147
|
-
// const proxyPacketsToProcess = yield* Queue.unbounded<ProxyQueueItem>()
|
|
148
|
-
// const messagePacketsToProcess = yield*
|
|
149
|
-
//
|
|
146
|
+
// const proxyPacketsToProcess = yield* Effect.acquireRelease(Queue.unbounded<ProxyQueueItem>(), Queue.shutdown)
|
|
147
|
+
// const messagePacketsToProcess = yield* Effect.acquireRelease(
|
|
148
|
+
// Queue.unbounded<MessageQueueItem>(),
|
|
149
|
+
// Queue.shutdown,
|
|
150
150
|
// )
|
|
151
151
|
|
|
152
152
|
const channelMap = new Map<
|
|
@@ -163,9 +163,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
163
163
|
}
|
|
164
164
|
>()
|
|
165
165
|
|
|
166
|
-
const channelRequestsQueue = yield* Queue.unbounded<ListenForChannelResult>().
|
|
167
|
-
Effect.acquireRelease(Queue.shutdown),
|
|
168
|
-
)
|
|
166
|
+
const channelRequestsQueue = yield* Effect.acquireRelease(Queue.unbounded<ListenForChannelResult>(), Queue.shutdown)
|
|
169
167
|
|
|
170
168
|
type RequestId = string
|
|
171
169
|
const topologyRequestsMap = new Map<RequestId, Map<MeshNodeName, Set<MeshNodeName>>>()
|
|
@@ -364,10 +362,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
364
362
|
|
|
365
363
|
// TODO use a priority queue instead to prioritize network-changes/edge-requests over payloads
|
|
366
364
|
const listenFiber = yield* edgeChannel.listen.pipe(
|
|
367
|
-
Stream.
|
|
365
|
+
Stream.mapEffect(Effect.fromResult),
|
|
368
366
|
Stream.tap((message) =>
|
|
369
367
|
Effect.gen(function* () {
|
|
370
|
-
const packet = yield* Schema.
|
|
368
|
+
const packet = yield* Schema.decodeUnknownEffect(WebmeshSchema.Packet)(message)
|
|
371
369
|
|
|
372
370
|
// console.debug(nodeName, 'recv', packet._tag, packet.source, packet.target)
|
|
373
371
|
|
|
@@ -387,8 +385,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
387
385
|
const channelKey = `target:${packet.source}, channelName:${packet.channelName}` satisfies ChannelKey
|
|
388
386
|
|
|
389
387
|
if (channelMap.has(channelKey) === false) {
|
|
390
|
-
const channelQueue = yield*
|
|
391
|
-
|
|
388
|
+
const channelQueue = yield* Effect.acquireRelease(
|
|
389
|
+
Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
|
|
390
|
+
Queue.shutdown,
|
|
392
391
|
)
|
|
393
392
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
394
393
|
}
|
|
@@ -462,7 +461,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
462
461
|
const removeEdge: MeshNode['removeEdge'] = (targetNodeName) =>
|
|
463
462
|
Effect.gen(function* () {
|
|
464
463
|
if (edgeChannels.has(targetNodeName) === false) {
|
|
465
|
-
return yield* new Cause.
|
|
464
|
+
return yield* new Cause.NoSuchElementError(`No edge found for ${targetNodeName}`)
|
|
466
465
|
}
|
|
467
466
|
|
|
468
467
|
yield* Fiber.interrupt(edgeChannels.get(targetNodeName)!.listenFiber)
|
|
@@ -502,8 +501,9 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
502
501
|
}
|
|
503
502
|
|
|
504
503
|
if (channelMap.has(channelKey) === false) {
|
|
505
|
-
const channelQueue = yield*
|
|
506
|
-
|
|
504
|
+
const channelQueue = yield* Effect.acquireRelease(
|
|
505
|
+
Queue.unbounded<MessageQueueItem | ProxyQueueItem>(),
|
|
506
|
+
Queue.shutdown,
|
|
507
507
|
)
|
|
508
508
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: undefined })
|
|
509
509
|
}
|
|
@@ -513,7 +513,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
513
513
|
yield* Effect.addFinalizer(() => Effect.sync(() => channelMap.delete(channelKey)))
|
|
514
514
|
|
|
515
515
|
if (mode === 'direct') {
|
|
516
|
-
const incomingPacketsQueue = yield* Queue.unbounded<any>()
|
|
516
|
+
const incomingPacketsQueue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
|
|
517
517
|
|
|
518
518
|
// We're we're draining the queue into another new queue.
|
|
519
519
|
// It's a bit of a mystery why this is needed, since the unit tests also work without it.
|
|
@@ -541,7 +541,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
541
541
|
|
|
542
542
|
channelMap.set(channelKey, { queue: channelQueue, debugInfo: { channel: webChannel, target } })
|
|
543
543
|
|
|
544
|
-
yield* initialEdgeDeferred
|
|
544
|
+
yield* Deferred.await(initialEdgeDeferred)
|
|
545
545
|
|
|
546
546
|
return webChannel
|
|
547
547
|
} else {
|
|
@@ -605,12 +605,12 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
605
605
|
|
|
606
606
|
const debugInfo = {}
|
|
607
607
|
|
|
608
|
-
const queue = yield* Queue.unbounded<any>()
|
|
608
|
+
const queue = yield* Effect.acquireRelease(Queue.unbounded<any>(), Queue.shutdown)
|
|
609
609
|
broadcastChannelListenQueueMap.set(channelName, queue)
|
|
610
610
|
|
|
611
611
|
const send = (message: any) =>
|
|
612
612
|
Effect.gen(function* () {
|
|
613
|
-
const payload = yield* Schema.
|
|
613
|
+
const payload = yield* Schema.encodeEffect(schema)(message)
|
|
614
614
|
const packet = WebmeshSchema.BroadcastChannelPacket.make({
|
|
615
615
|
channelName,
|
|
616
616
|
payload,
|
|
@@ -624,10 +624,10 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
624
624
|
|
|
625
625
|
const listen = Stream.fromQueue(queue).pipe(
|
|
626
626
|
Stream.filter(Schema.is(WebmeshSchema.BroadcastChannelPacket)),
|
|
627
|
-
Stream.map((_) => Schema.
|
|
627
|
+
Stream.map((_) => Schema.decodeResult(schema)(_.payload)),
|
|
628
628
|
)
|
|
629
629
|
|
|
630
|
-
const closedDeferred = yield* Deferred.make<void>()
|
|
630
|
+
const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
|
|
631
631
|
|
|
632
632
|
return {
|
|
633
633
|
[WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
|
|
@@ -644,7 +644,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
644
644
|
|
|
645
645
|
const edgeKeys: MeshNode['edgeKeys'] = Effect.sync(() => new Set(edgeChannels.keys()))
|
|
646
646
|
|
|
647
|
-
const
|
|
647
|
+
const services = yield* Effect.context()
|
|
648
648
|
|
|
649
649
|
const debug: MeshNode['debug'] = {
|
|
650
650
|
print: () => {
|
|
@@ -670,7 +670,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
670
670
|
' ',
|
|
671
671
|
value.debugInfo?.channel,
|
|
672
672
|
'\n',
|
|
673
|
-
indent(`Queue: ${value.queue
|
|
673
|
+
indent(`Queue: ${Queue.sizeUnsafe(value.queue)}`, 4),
|
|
674
674
|
value.queue,
|
|
675
675
|
)
|
|
676
676
|
}
|
|
@@ -699,7 +699,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
699
699
|
yield* Effect.logDebug(`sending ping via channel ${channelKey}`)
|
|
700
700
|
yield* channel.debugInfo.channel.send(msg(`channel ${channelKey}`) as any)
|
|
701
701
|
}
|
|
702
|
-
}).pipe(Effect.provide(
|
|
702
|
+
}).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runFork)
|
|
703
703
|
},
|
|
704
704
|
requestTopology: (timeoutMs = 1000) =>
|
|
705
705
|
Effect.gen(function* () {
|
|
@@ -722,7 +722,7 @@ export const makeMeshNode = <TName extends MeshNodeName>(
|
|
|
722
722
|
for (const [key, value] of item) {
|
|
723
723
|
yield* Effect.logDebug(` node '${key}' has edge to: ${Array.from(value.values()).join(', ')}`)
|
|
724
724
|
}
|
|
725
|
-
}).pipe(Effect.provide(
|
|
725
|
+
}).pipe(Effect.provide(services), Effect.tapCauseLogPretty, Effect.runPromise),
|
|
726
726
|
}
|
|
727
727
|
|
|
728
728
|
return {
|
package/src/utils.ts
CHANGED
|
@@ -6,14 +6,14 @@ import { Duration, Effect } from '@livestore/utils/effect'
|
|
|
6
6
|
*/
|
|
7
7
|
export class TimeoutSet<V> {
|
|
8
8
|
private values = new Map<V, number>()
|
|
9
|
-
private timeoutHandle:
|
|
9
|
+
private timeoutHandle: ReturnType<typeof setTimeout> | undefined
|
|
10
10
|
private readonly timeoutMs: number
|
|
11
11
|
|
|
12
|
-
private constructor({ timeout }: { timeout: Duration.
|
|
12
|
+
private constructor({ timeout }: { timeout: Duration.Input }) {
|
|
13
13
|
this.timeoutMs = Duration.toMillis(timeout)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
static make = (timeout: Duration.
|
|
16
|
+
static make = (timeout: Duration.Input) =>
|
|
17
17
|
Effect.gen(function* () {
|
|
18
18
|
const timeoutSet = new TimeoutSet({ timeout })
|
|
19
19
|
|
|
@@ -7,24 +7,24 @@
|
|
|
7
7
|
import { expect } from 'vitest'
|
|
8
8
|
|
|
9
9
|
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
10
|
-
import { Effect,
|
|
10
|
+
import { Effect, Result, Schema, WebChannel } from '@livestore/utils/effect'
|
|
11
11
|
|
|
12
12
|
import * as MeshSchema from './mesh-schema.ts'
|
|
13
|
-
import {
|
|
13
|
+
import { MessageMsgpack, WSEdgeInit, WSEdgePayload } from './websocket-edge.ts'
|
|
14
14
|
|
|
15
15
|
Vitest.describe('websocket-edge', () => {
|
|
16
16
|
/**
|
|
17
|
-
* Test that WSEdgeInit messages can be encoded/decoded via
|
|
17
|
+
* Test that WSEdgeInit messages can be encoded/decoded via MessageMsgpack.
|
|
18
18
|
*/
|
|
19
|
-
Vitest.
|
|
19
|
+
Vitest.live('should encode/decode WSEdgeInit', (test) =>
|
|
20
20
|
Effect.gen(function* () {
|
|
21
21
|
const initMessage = WSEdgeInit.make({ from: 'test-node' })
|
|
22
22
|
|
|
23
23
|
// Encode to msgpack
|
|
24
|
-
const encoded = yield* Schema.
|
|
24
|
+
const encoded = yield* Schema.encodeEffect(MessageMsgpack)(initMessage)
|
|
25
25
|
|
|
26
26
|
// Decode back
|
|
27
|
-
const decoded = yield* Schema.
|
|
27
|
+
const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
|
|
28
28
|
|
|
29
29
|
expect(decoded._tag).toBe('WSEdgeInit')
|
|
30
30
|
if (decoded._tag === 'WSEdgeInit') {
|
|
@@ -36,7 +36,7 @@ Vitest.describe('websocket-edge', () => {
|
|
|
36
36
|
/**
|
|
37
37
|
* Test that WSEdgePayload messages with valid Packet payloads work correctly.
|
|
38
38
|
*/
|
|
39
|
-
Vitest.
|
|
39
|
+
Vitest.live('should encode/decode WSEdgePayload with Packet', (test) =>
|
|
40
40
|
Effect.gen(function* () {
|
|
41
41
|
const packet = {
|
|
42
42
|
_tag: 'NetworkEdgeAdded' as const,
|
|
@@ -47,10 +47,10 @@ Vitest.describe('websocket-edge', () => {
|
|
|
47
47
|
const wsMessage = WSEdgePayload.make({ from: 'test-node', payload: packet })
|
|
48
48
|
|
|
49
49
|
// Encode to msgpack
|
|
50
|
-
const encoded = yield* Schema.
|
|
50
|
+
const encoded = yield* Schema.encodeEffect(MessageMsgpack)(wsMessage)
|
|
51
51
|
|
|
52
52
|
// Decode back
|
|
53
|
-
const decoded = yield* Schema.
|
|
53
|
+
const decoded = yield* Schema.decodeEffect(MessageMsgpack)(encoded)
|
|
54
54
|
|
|
55
55
|
expect(decoded._tag).toBe('WSEdgePayload')
|
|
56
56
|
if (decoded._tag === 'WSEdgePayload') {
|
|
@@ -64,23 +64,23 @@ Vitest.describe('websocket-edge', () => {
|
|
|
64
64
|
* Test that mapSchema(Packet) includes WebChannel internal messages.
|
|
65
65
|
* This is important because channels can send/receive WebChannel.Ping/Pong/DebugPing.
|
|
66
66
|
*/
|
|
67
|
-
Vitest.
|
|
67
|
+
Vitest.live('mapSchema should include WebChannel messages in schema', (test) =>
|
|
68
68
|
Effect.gen(function* () {
|
|
69
69
|
const schema = WebChannel.mapSchema(MeshSchema.Packet)
|
|
70
70
|
|
|
71
71
|
// WebChannel.Ping should be decodable via the wrapped listen schema
|
|
72
72
|
const pingPayload = { _tag: 'WebChannel.Ping' as const, requestId: 'test-123' }
|
|
73
|
-
const result = Schema.
|
|
73
|
+
const result = Schema.decodeUnknownResult(schema.listen)(pingPayload)
|
|
74
74
|
|
|
75
75
|
// mapSchema adds WebChannel messages to the schema
|
|
76
|
-
expect(
|
|
76
|
+
expect(Result.isSuccess(result)).toBe(true)
|
|
77
77
|
}).pipe(Vitest.withTestCtx(test)),
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* Test that valid webmesh packets can be decoded.
|
|
82
82
|
*/
|
|
83
|
-
Vitest.
|
|
83
|
+
Vitest.live('should decode valid webmesh packets', (test) =>
|
|
84
84
|
Effect.gen(function* () {
|
|
85
85
|
const schema = WebChannel.mapSchema(MeshSchema.Packet)
|
|
86
86
|
|
|
@@ -91,7 +91,7 @@ Vitest.describe('websocket-edge', () => {
|
|
|
91
91
|
target: 'node-b',
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const result = yield* Schema.
|
|
94
|
+
const result = yield* Schema.decodeUnknownEffect(schema.listen)(packet)
|
|
95
95
|
expect(result._tag).toBe('NetworkEdgeAdded')
|
|
96
96
|
}).pipe(Vitest.withTestCtx(test)),
|
|
97
97
|
)
|
package/src/websocket-edge.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { HttpClient } from '@livestore/utils/effect'
|
|
2
1
|
import {
|
|
2
|
+
type HttpClient,
|
|
3
3
|
Deferred,
|
|
4
|
+
Duration,
|
|
4
5
|
Effect,
|
|
5
|
-
|
|
6
|
+
Result,
|
|
6
7
|
Exit,
|
|
8
|
+
Latch,
|
|
7
9
|
Layer,
|
|
8
|
-
|
|
10
|
+
Msgpack,
|
|
9
11
|
Queue,
|
|
10
12
|
Schedule,
|
|
11
13
|
Schema,
|
|
@@ -18,18 +20,18 @@ import {
|
|
|
18
20
|
import * as WebmeshSchema from './mesh-schema.ts'
|
|
19
21
|
import type { MeshNode } from './node.ts'
|
|
20
22
|
|
|
21
|
-
export
|
|
23
|
+
export const WSEdgeInit = Schema.TaggedStruct('WSEdgeInit', {
|
|
22
24
|
from: Schema.String,
|
|
23
|
-
})
|
|
25
|
+
})
|
|
24
26
|
|
|
25
|
-
export
|
|
27
|
+
export const WSEdgePayload = Schema.TaggedStruct('WSEdgePayload', {
|
|
26
28
|
from: Schema.String,
|
|
27
29
|
payload: Schema.Any,
|
|
28
|
-
})
|
|
30
|
+
})
|
|
29
31
|
|
|
30
|
-
export
|
|
32
|
+
export const WSEdgeMessage = Schema.Union([WSEdgeInit, WSEdgePayload])
|
|
31
33
|
|
|
32
|
-
export const
|
|
34
|
+
export const MessageMsgpack = Msgpack.schema(WSEdgeMessage)
|
|
33
35
|
|
|
34
36
|
export type SocketType =
|
|
35
37
|
| {
|
|
@@ -58,11 +60,12 @@ export const connectViaWebSocket = ({
|
|
|
58
60
|
debug: { id: `node:${node.nodeName}` },
|
|
59
61
|
})
|
|
60
62
|
|
|
61
|
-
yield*
|
|
62
|
-
.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
|
|
63
|
-
|
|
63
|
+
yield* Effect.acquireRelease(
|
|
64
|
+
node.addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true }),
|
|
65
|
+
() => node.removeEdge('ws').pipe(Effect.orDie),
|
|
66
|
+
)
|
|
64
67
|
|
|
65
|
-
yield* edgeChannel.webChannel.closedDeferred
|
|
68
|
+
yield* Deferred.await(edgeChannel.webChannel.closedDeferred)
|
|
66
69
|
}).pipe(Effect.scoped, Effect.forever, Effect.interruptible, Effect.provide(binaryWebSocketConstructorLayer))
|
|
67
70
|
|
|
68
71
|
const binaryWebSocketConstructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) => {
|
|
@@ -91,46 +94,49 @@ export const makeWebSocketEdge = ({
|
|
|
91
94
|
Effect.gen(function* () {
|
|
92
95
|
const fromDeferred = yield* Deferred.make<string>()
|
|
93
96
|
|
|
94
|
-
const listenQueue = yield*
|
|
95
|
-
|
|
97
|
+
const listenQueue = yield* Effect.acquireRelease(
|
|
98
|
+
Queue.unbounded<typeof WebmeshSchema.Packet.Type>(),
|
|
99
|
+
Queue.shutdown,
|
|
96
100
|
)
|
|
97
101
|
|
|
98
102
|
const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
|
|
99
103
|
|
|
100
|
-
const isConnectedLatch = yield*
|
|
104
|
+
const isConnectedLatch = yield* Latch.make(true)
|
|
101
105
|
|
|
102
|
-
const closedDeferred = yield* Deferred.make<void>()
|
|
106
|
+
const closedDeferred = yield* Effect.acquireRelease(Deferred.make<void>(), Deferred.done(Exit.void))
|
|
103
107
|
|
|
104
|
-
const
|
|
105
|
-
Schedule.
|
|
108
|
+
const retryOpenErrorSchedule = Schedule.exponential(100).pipe(
|
|
109
|
+
Schedule.modifyDelay(({ duration }) => Effect.succeed(Duration.min(duration, Duration.millis(5000)))),
|
|
110
|
+
Schedule.setInputType<Socket.SocketError>(),
|
|
111
|
+
Schedule.while(({ input }) => input.reason._tag === 'SocketOpenError'),
|
|
106
112
|
)
|
|
107
113
|
|
|
108
114
|
const sendToSocket = yield* socket.writer
|
|
109
115
|
|
|
110
116
|
yield* Stream.never.pipe(
|
|
111
117
|
Stream.pipeThroughChannel(Socket.toChannel(socket)),
|
|
112
|
-
Stream.catchTag(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
),
|
|
126
|
-
Stream.retry(
|
|
118
|
+
Stream.catchTag('SocketError', (error) => {
|
|
119
|
+
// In the case of the socket being closed, close the WebChannel
|
|
120
|
+
// which can be observed from the outside.
|
|
121
|
+
if (error.reason._tag === 'SocketCloseError') {
|
|
122
|
+
return Stream.fromEffectDrain(
|
|
123
|
+
Effect.gen(function* () {
|
|
124
|
+
yield* Deferred.succeed(closedDeferred, undefined)
|
|
125
|
+
yield* isConnectedLatch.close
|
|
126
|
+
}),
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Stream.fail(error)
|
|
131
|
+
}),
|
|
132
|
+
Stream.retry(retryOpenErrorSchedule),
|
|
127
133
|
Stream.tap(
|
|
128
134
|
Effect.fn(function* (bytes) {
|
|
129
|
-
const msg = yield* Schema.
|
|
135
|
+
const msg = yield* Schema.decodeEffect(MessageMsgpack)(new Uint8Array(bytes))
|
|
130
136
|
if (msg._tag === 'WSEdgeInit') {
|
|
131
137
|
yield* Deferred.succeed(fromDeferred, msg.from)
|
|
132
138
|
} else {
|
|
133
|
-
const decodedPayload = yield* Schema.
|
|
139
|
+
const decodedPayload = yield* Schema.decodeEffect(schema.listen)(msg.payload)
|
|
134
140
|
yield* Queue.offer(listenQueue, decodedPayload)
|
|
135
141
|
}
|
|
136
142
|
}),
|
|
@@ -149,13 +155,13 @@ export const makeWebSocketEdge = ({
|
|
|
149
155
|
)
|
|
150
156
|
|
|
151
157
|
const initHandshake = (from: string) =>
|
|
152
|
-
sendToSocket(Schema.encodeSync(
|
|
158
|
+
sendToSocket(Schema.encodeSync(MessageMsgpack)({ _tag: 'WSEdgeInit', from }))
|
|
153
159
|
|
|
154
160
|
if (socketType._tag === 'leaf') {
|
|
155
161
|
yield* initHandshake(socketType.from)
|
|
156
162
|
}
|
|
157
163
|
|
|
158
|
-
const deferredResult = yield* fromDeferred
|
|
164
|
+
const deferredResult = yield* Deferred.await(fromDeferred)
|
|
159
165
|
const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
|
|
160
166
|
|
|
161
167
|
if (socketType._tag === 'relay') {
|
|
@@ -165,12 +171,12 @@ export const makeWebSocketEdge = ({
|
|
|
165
171
|
const send = (message: typeof WebmeshSchema.Packet.Type) =>
|
|
166
172
|
Effect.gen(function* () {
|
|
167
173
|
yield* isConnectedLatch.await
|
|
168
|
-
const payload = yield* Schema.
|
|
169
|
-
yield* sendToSocket(yield* Schema.
|
|
174
|
+
const payload = yield* Schema.encodeEffect(schema.send)(message)
|
|
175
|
+
yield* sendToSocket(yield* Schema.encodeEffect(MessageMsgpack)({ _tag: 'WSEdgePayload', payload, from }))
|
|
170
176
|
}).pipe(Effect.orDie)
|
|
171
177
|
|
|
172
178
|
const listen = Stream.fromQueue(listenQueue).pipe(
|
|
173
|
-
Stream.map(
|
|
179
|
+
Stream.map(Result.succeed),
|
|
174
180
|
WebChannel.listenToDebugPing('websocket-edge'),
|
|
175
181
|
)
|
|
176
182
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { LS_DEV } from '@livestore/utils'
|
|
2
|
+
import { Context, Deferred, Effect, Layer, Queue, Stream, WebChannel } from '@livestore/utils/effect'
|
|
3
|
+
|
|
4
|
+
import * as WebmeshSchema from '../mesh-schema.ts'
|
|
5
|
+
import type { MeshNode } from '../node.ts'
|
|
6
|
+
import { makeMeshNode } from '../node.ts'
|
|
7
|
+
import * as WorkerSchema from './schema.ts'
|
|
8
|
+
|
|
9
|
+
export * as Schema from './schema.ts'
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
var __debugWebmeshNode: any
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class CacheService extends Context.Service<CacheService, { node: MeshNode }>()(
|
|
16
|
+
'@livestore/webmesh:worker:CacheService',
|
|
17
|
+
) {
|
|
18
|
+
static layer = ({ nodeName }: { nodeName: string }) =>
|
|
19
|
+
Effect.gen(function* () {
|
|
20
|
+
const node = yield* makeMeshNode(nodeName)
|
|
21
|
+
|
|
22
|
+
globalThis.__debugWebmeshNode = node
|
|
23
|
+
|
|
24
|
+
return CacheService.of({ node })
|
|
25
|
+
}).pipe(Layer.effect(CacheService))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CreateConnection = ({ from, port }: typeof WorkerSchema.CreateConnection.Type) =>
|
|
29
|
+
Stream.callback<{}, never, CacheService>((emit) =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const { node } = yield* CacheService
|
|
32
|
+
|
|
33
|
+
const messagePortChannel = yield* WebChannel.messagePortChannel({ port, schema: WebmeshSchema.Packet })
|
|
34
|
+
|
|
35
|
+
yield* node.addEdge({ target: from, edgeChannel: messagePortChannel, replaceIfExists: true })
|
|
36
|
+
|
|
37
|
+
if (LS_DEV === true) {
|
|
38
|
+
yield* Effect.logDebug(`@livestore/webmesh:worker: accepted edge: ${node.nodeName} <- ${from}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// `Stream.callback` exposes a queue-like emitter in Effect v4. Emit the single success value
|
|
42
|
+
// and then end the stream to preserve the old one-response worker command behavior.
|
|
43
|
+
yield* Queue.offer(emit, {})
|
|
44
|
+
yield* Queue.end(emit)
|
|
45
|
+
|
|
46
|
+
yield* Effect.spanEvent({ connectedTo: [...node.edgeKeys] })
|
|
47
|
+
}).pipe(Effect.orDie),
|
|
48
|
+
).pipe(Stream.withSpan(`@livestore/webmesh:worker:create-connection:${from}`))
|
|
49
|
+
|
|
50
|
+
export const connectViaWorker = ({
|
|
51
|
+
node,
|
|
52
|
+
target,
|
|
53
|
+
worker,
|
|
54
|
+
}: {
|
|
55
|
+
node: MeshNode
|
|
56
|
+
target: string
|
|
57
|
+
// Effect v4 removed the serialized worker pool type this package used to reference. Webmesh only
|
|
58
|
+
// needs this structural `execute` contract; adapter code can provide any compatible pool.
|
|
59
|
+
worker: { execute: (request: typeof WorkerSchema.Request.Type) => Stream.Stream<{}> }
|
|
60
|
+
}) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const mc = new MessageChannel()
|
|
63
|
+
|
|
64
|
+
const isConnected = yield* Deferred.make<boolean>()
|
|
65
|
+
|
|
66
|
+
if (LS_DEV === true) {
|
|
67
|
+
yield* Effect.addFinalizerLog(`@livestore/webmesh:worker: closing message channel ${node.nodeName} -> ${target}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
yield* worker.execute(WorkerSchema.CreateConnection.make({ from: node.nodeName, port: mc.port1 })).pipe(
|
|
71
|
+
Stream.tap(() => Deferred.succeed(isConnected, true)),
|
|
72
|
+
Stream.runDrain,
|
|
73
|
+
Effect.tapCauseLogPretty,
|
|
74
|
+
Effect.forkScoped,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
yield* Deferred.await(isConnected)
|
|
78
|
+
|
|
79
|
+
const workerConnection = yield* WebChannel.messagePortChannel({
|
|
80
|
+
port: mc.port2,
|
|
81
|
+
schema: WebmeshSchema.Packet,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
yield* node.addEdge({ target, edgeChannel: workerConnection, replaceIfExists: true })
|
|
85
|
+
|
|
86
|
+
if (LS_DEV === true) {
|
|
87
|
+
yield* Effect.logDebug(`@livestore/webmesh:worker: initiated connection: ${node.nodeName} -> ${target}`)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Schema, Transferable } from '@livestore/utils/effect'
|
|
2
|
+
|
|
3
|
+
export const CreateConnection = Schema.TaggedStruct('WebmeshWorker.CreateConnection', {
|
|
4
|
+
from: Schema.String,
|
|
5
|
+
port: Transferable.MessagePort,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export const Request = Schema.Union([CreateConnection])
|