@livestore/webmesh 0.4.0-dev.9 → 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/README.md +5 -3
- 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 +35 -38
- 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 +22 -23
- package/dist/channel/direct-channel.js.map +1 -1
- package/dist/channel/proxy-channel.d.ts +29 -5
- package/dist/channel/proxy-channel.d.ts.map +1 -1
- package/dist/channel/proxy-channel.js +87 -37
- 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 +7 -7
- 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 +12 -6
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +47 -44
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +253 -63
- 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 +20 -18
- package/dist/websocket-edge.d.ts.map +1 -1
- package/dist/websocket-edge.js +33 -41
- package/dist/websocket-edge.js.map +1 -1
- package/dist/websocket-edge.test.d.ts +7 -0
- package/dist/websocket-edge.test.d.ts.map +1 -0
- package/dist/websocket-edge.test.js +74 -0
- package/dist/websocket-edge.test.js.map +1 -0
- 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 +48 -12
- package/src/channel/direct-channel-internal.ts +41 -47
- package/src/channel/direct-channel.ts +28 -25
- package/src/channel/proxy-channel.ts +120 -57
- package/src/common.ts +9 -7
- package/src/mesh-schema.ts +34 -33
- package/src/node.test.ts +356 -80
- package/src/node.ts +63 -55
- package/src/utils.ts +3 -3
- package/src/websocket-edge.test.ts +98 -0
- package/src/websocket-edge.ts +52 -48
- package/src/worker/mod.ts +89 -0
- package/src/worker/schema.ts +8 -0
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
|
| {
|
|
@@ -50,7 +52,7 @@ export const connectViaWebSocket = ({
|
|
|
50
52
|
openTimeout?: number
|
|
51
53
|
}): Effect.Effect<void, never, Scope.Scope | HttpClient.HttpClient> =>
|
|
52
54
|
Effect.gen(function* () {
|
|
53
|
-
const socket = yield* Socket.makeWebSocket(url, { openTimeout })
|
|
55
|
+
const socket = yield* Socket.makeWebSocket(url, openTimeout !== undefined ? { openTimeout } : undefined)
|
|
54
56
|
|
|
55
57
|
const edgeChannel = yield* makeWebSocketEdge({
|
|
56
58
|
socket,
|
|
@@ -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) => {
|
|
@@ -78,7 +81,7 @@ export const makeWebSocketEdge = ({
|
|
|
78
81
|
}: {
|
|
79
82
|
socket: Socket.Socket
|
|
80
83
|
socketType: SocketType
|
|
81
|
-
debug?: { id?: string }
|
|
84
|
+
debug?: { id?: string | undefined } | undefined
|
|
82
85
|
}): Effect.Effect<
|
|
83
86
|
{
|
|
84
87
|
webChannel: WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|
|
@@ -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
|
|
|
114
|
+
const sendToSocket = yield* socket.writer
|
|
115
|
+
|
|
108
116
|
yield* Stream.never.pipe(
|
|
109
117
|
Stream.pipeThroughChannel(Socket.toChannel(socket)),
|
|
110
|
-
Stream.catchTag(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
),
|
|
125
|
-
Stream.retry(retryOpenTimeoutSchedule),
|
|
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),
|
|
126
133
|
Stream.tap(
|
|
127
134
|
Effect.fn(function* (bytes) {
|
|
128
|
-
const msg = yield* Schema.
|
|
135
|
+
const msg = yield* Schema.decodeEffect(MessageMsgpack)(new Uint8Array(bytes))
|
|
129
136
|
if (msg._tag === 'WSEdgeInit') {
|
|
130
137
|
yield* Deferred.succeed(fromDeferred, msg.from)
|
|
131
138
|
} else {
|
|
132
|
-
const decodedPayload = yield* Schema.
|
|
133
|
-
// yield* Effect.logDebug(`[websocket-edge] recv from ${msg.from}: ${decodedPayload._tag}`, decodedPayload)
|
|
139
|
+
const decodedPayload = yield* Schema.decodeEffect(schema.listen)(msg.payload)
|
|
134
140
|
yield* Queue.offer(listenQueue, decodedPayload)
|
|
135
141
|
}
|
|
136
142
|
}),
|
|
@@ -148,16 +154,14 @@ export const makeWebSocketEdge = ({
|
|
|
148
154
|
Effect.forkScoped,
|
|
149
155
|
)
|
|
150
156
|
|
|
151
|
-
const sendToSocket = yield* socket.writer
|
|
152
|
-
|
|
153
157
|
const initHandshake = (from: string) =>
|
|
154
|
-
sendToSocket(Schema.encodeSync(
|
|
158
|
+
sendToSocket(Schema.encodeSync(MessageMsgpack)({ _tag: 'WSEdgeInit', from }))
|
|
155
159
|
|
|
156
160
|
if (socketType._tag === 'leaf') {
|
|
157
161
|
yield* initHandshake(socketType.from)
|
|
158
162
|
}
|
|
159
163
|
|
|
160
|
-
const deferredResult = yield* fromDeferred
|
|
164
|
+
const deferredResult = yield* Deferred.await(fromDeferred)
|
|
161
165
|
const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
|
|
162
166
|
|
|
163
167
|
if (socketType._tag === 'relay') {
|
|
@@ -167,12 +171,12 @@ export const makeWebSocketEdge = ({
|
|
|
167
171
|
const send = (message: typeof WebmeshSchema.Packet.Type) =>
|
|
168
172
|
Effect.gen(function* () {
|
|
169
173
|
yield* isConnectedLatch.await
|
|
170
|
-
const payload = yield* Schema.
|
|
171
|
-
yield* sendToSocket(Schema.
|
|
174
|
+
const payload = yield* Schema.encodeEffect(schema.send)(message)
|
|
175
|
+
yield* sendToSocket(yield* Schema.encodeEffect(MessageMsgpack)({ _tag: 'WSEdgePayload', payload, from }))
|
|
172
176
|
}).pipe(Effect.orDie)
|
|
173
177
|
|
|
174
178
|
const listen = Stream.fromQueue(listenQueue).pipe(
|
|
175
|
-
Stream.map(
|
|
179
|
+
Stream.map(Result.succeed),
|
|
176
180
|
WebChannel.listenToDebugPing('websocket-edge'),
|
|
177
181
|
)
|
|
178
182
|
|
|
@@ -184,7 +188,7 @@ export const makeWebSocketEdge = ({
|
|
|
184
188
|
schema,
|
|
185
189
|
supportsTransferables: false,
|
|
186
190
|
shutdown: Scope.close(scope, Exit.void),
|
|
187
|
-
debugInfo,
|
|
191
|
+
...(debugInfo !== undefined ? { debugInfo } : {}),
|
|
188
192
|
} satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
|
|
189
193
|
|
|
190
194
|
return { webChannel, from }
|
|
@@ -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])
|