@livestore/webmesh 0.3.0-dev.3 → 0.3.0-dev.31

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 (64) 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 +273 -100
  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/utils.d.ts +4 -4
  36. package/dist/utils.d.ts.map +1 -1
  37. package/dist/utils.js +7 -1
  38. package/dist/utils.js.map +1 -1
  39. package/dist/websocket-edge.d.ts +52 -0
  40. package/dist/websocket-edge.d.ts.map +1 -0
  41. package/dist/websocket-edge.js +85 -0
  42. package/dist/websocket-edge.js.map +1 -0
  43. package/package.json +5 -6
  44. package/src/channel/message-channel-internal.ts +356 -0
  45. package/src/channel/message-channel.ts +190 -310
  46. package/src/channel/proxy-channel.ts +259 -231
  47. package/src/common.ts +12 -13
  48. package/src/mesh-schema.ts +62 -6
  49. package/src/mod.ts +2 -2
  50. package/src/node.test.ts +554 -189
  51. package/src/node.ts +421 -138
  52. package/src/utils.ts +13 -2
  53. package/src/websocket-edge.ts +177 -0
  54. package/tmp/pack.tgz +0 -0
  55. package/dist/websocket-connection.d.ts +0 -51
  56. package/dist/websocket-connection.d.ts.map +0 -1
  57. package/dist/websocket-connection.js +0 -74
  58. package/dist/websocket-connection.js.map +0 -1
  59. package/dist/websocket-server.d.ts +0 -7
  60. package/dist/websocket-server.d.ts.map +0 -1
  61. package/dist/websocket-server.js +0 -24
  62. package/dist/websocket-server.js.map +0 -1
  63. package/src/websocket-connection.ts +0 -158
  64. package/src/websocket-server.ts +0 -40
package/src/utils.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
- import { Duration } from '@livestore/utils/effect'
2
+ import { Duration, Effect } from '@livestore/utils/effect'
3
3
 
4
4
  /**
5
5
  * A set of values that expire after a given timeout
@@ -10,10 +10,19 @@ export class TimeoutSet<V> {
10
10
  private timeoutHandle: NodeJS.Timeout | undefined
11
11
  private readonly timeoutMs: number
12
12
 
13
- constructor({ timeout }: { timeout: Duration.DurationInput }) {
13
+ private constructor({ timeout }: { timeout: Duration.DurationInput }) {
14
14
  this.timeoutMs = Duration.toMillis(timeout)
15
15
  }
16
16
 
17
+ static make = (timeout: Duration.DurationInput) =>
18
+ Effect.gen(function* () {
19
+ const timeoutSet = new TimeoutSet({ timeout })
20
+
21
+ yield* Effect.addFinalizer(() => Effect.sync(() => timeoutSet.onShutdown()))
22
+
23
+ return timeoutSet
24
+ })
25
+
17
26
  add(value: V): void {
18
27
  this.values.set(value, Date.now())
19
28
  this.scheduleCleanup()
@@ -44,4 +53,6 @@ export class TimeoutSet<V> {
44
53
  }
45
54
  }
46
55
  }
56
+
57
+ onShutdown = () => clearTimeout(this.timeoutHandle)
47
58
  }
@@ -0,0 +1,177 @@
1
+ import type { HttpClient } from '@livestore/utils/effect'
2
+ import {
3
+ Deferred,
4
+ Effect,
5
+ Either,
6
+ Exit,
7
+ Layer,
8
+ Queue,
9
+ Schedule,
10
+ Schema,
11
+ Scope,
12
+ Socket,
13
+ Stream,
14
+ WebChannel,
15
+ } from '@livestore/utils/effect'
16
+
17
+ import * as WebmeshSchema from './mesh-schema.js'
18
+ import type { MeshNode } from './node.js'
19
+
20
+ export class WSEdgeInit extends Schema.TaggedStruct('WSEdgeInit', {
21
+ from: Schema.String,
22
+ }) {}
23
+
24
+ export class WSEdgePayload extends Schema.TaggedStruct('WSEdgePayload', {
25
+ from: Schema.String,
26
+ payload: Schema.Any,
27
+ }) {}
28
+
29
+ export class WSEdgeMessage extends Schema.Union(WSEdgeInit, WSEdgePayload) {}
30
+
31
+ export const MessageMsgPack = Schema.MsgPack(WSEdgeMessage)
32
+
33
+ export type SocketType =
34
+ | {
35
+ _tag: 'leaf'
36
+ from: string
37
+ }
38
+ | {
39
+ _tag: 'relay'
40
+ }
41
+
42
+ export const connectViaWebSocket = ({
43
+ node,
44
+ url,
45
+ }: {
46
+ node: MeshNode
47
+ url: string
48
+ }): Effect.Effect<void, never, Scope.Scope | HttpClient.HttpClient> =>
49
+ Effect.gen(function* () {
50
+ const disconnected = yield* Deferred.make<void>()
51
+
52
+ const socket = yield* Socket.makeWebSocket(url, { openTimeout: 50 })
53
+
54
+ const edgeChannel = yield* makeWebSocketEdge({
55
+ socket,
56
+ socketType: { _tag: 'leaf', from: node.nodeName },
57
+ })
58
+
59
+ yield* node
60
+ .addEdge({ target: 'ws', edgeChannel: edgeChannel.webChannel, replaceIfExists: true })
61
+ .pipe(Effect.acquireRelease(() => node.removeEdge('ws').pipe(Effect.orDie)))
62
+
63
+ yield* disconnected
64
+ }).pipe(Effect.scoped, Effect.forever, Effect.interruptible, Effect.provide(binaryWebSocketConstructorLayer))
65
+
66
+ const binaryWebSocketConstructorLayer = Layer.succeed(Socket.WebSocketConstructor, (url, protocols) => {
67
+ const socket = new globalThis.WebSocket(url, protocols)
68
+ socket.binaryType = 'arraybuffer'
69
+ return socket
70
+ })
71
+
72
+ export const makeWebSocketEdge = ({
73
+ socket,
74
+ socketType,
75
+ }: {
76
+ socket: Socket.Socket
77
+ socketType: SocketType
78
+ }): Effect.Effect<
79
+ {
80
+ webChannel: WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
81
+ from: string
82
+ },
83
+ never,
84
+ Scope.Scope | HttpClient.HttpClient
85
+ > =>
86
+ Effect.scopeWithCloseable((scope) =>
87
+ Effect.gen(function* () {
88
+ const fromDeferred = yield* Deferred.make<string>()
89
+
90
+ const listenQueue = yield* Queue.unbounded<typeof WebmeshSchema.Packet.Type>().pipe(
91
+ Effect.acquireRelease(Queue.shutdown),
92
+ )
93
+
94
+ const schema = WebChannel.mapSchema(WebmeshSchema.Packet)
95
+
96
+ const isConnectedLatch = yield* Effect.makeLatch(true)
97
+
98
+ const closedDeferred = yield* Deferred.make<void>().pipe(Effect.acquireRelease(Deferred.done(Exit.void)))
99
+
100
+ const retryOpenTimeoutSchedule = Schedule.exponential(100).pipe(
101
+ Schedule.whileInput((_: Socket.SocketError) => _.reason === 'OpenTimeout'),
102
+ )
103
+
104
+ yield* Stream.never.pipe(
105
+ Stream.pipeThroughChannel(Socket.toChannel(socket)),
106
+ Stream.catchTag(
107
+ 'SocketError',
108
+ Effect.fn(function* (error) {
109
+ if (error.reason === 'Close') {
110
+ yield* isConnectedLatch.close
111
+ yield* Deferred.succeed(closedDeferred, undefined)
112
+ return yield* Effect.interrupt
113
+ } else {
114
+ return yield* Effect.fail(error)
115
+ }
116
+ }),
117
+ ),
118
+ Stream.retry(retryOpenTimeoutSchedule),
119
+ Stream.tap(
120
+ Effect.fn(function* (bytes) {
121
+ const msg = yield* Schema.decode(MessageMsgPack)(new Uint8Array(bytes))
122
+ if (msg._tag === 'WSEdgeInit') {
123
+ yield* Deferred.succeed(fromDeferred, msg.from)
124
+ } else {
125
+ const decodedPayload = yield* Schema.decode(schema.listen)(msg.payload)
126
+ yield* Queue.offer(listenQueue, decodedPayload)
127
+ }
128
+ }),
129
+ ),
130
+ Stream.runDrain,
131
+ Effect.interruptible,
132
+ Effect.withSpan('makeWebSocketEdge:listen'),
133
+ Effect.tapCauseLogPretty,
134
+ Effect.forkScoped,
135
+ )
136
+
137
+ const sendToSocket = yield* socket.writer
138
+
139
+ const initHandshake = (from: string) =>
140
+ sendToSocket(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgeInit', from }))
141
+
142
+ if (socketType._tag === 'leaf') {
143
+ yield* initHandshake(socketType.from)
144
+ }
145
+
146
+ const deferredResult = yield* fromDeferred
147
+ const from = socketType._tag === 'leaf' ? socketType.from : deferredResult
148
+
149
+ if (socketType._tag === 'relay') {
150
+ yield* initHandshake(from)
151
+ }
152
+
153
+ const send = (message: typeof WebmeshSchema.Packet.Type) =>
154
+ Effect.gen(function* () {
155
+ yield* isConnectedLatch.await
156
+ const payload = yield* Schema.encode(schema.send)(message)
157
+ yield* sendToSocket(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSEdgePayload', payload, from }))
158
+ }).pipe(Effect.orDie)
159
+
160
+ const listen = Stream.fromQueue(listenQueue).pipe(
161
+ Stream.map(Either.right),
162
+ WebChannel.listenToDebugPing('websocket-edge'),
163
+ )
164
+
165
+ const webChannel = {
166
+ [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
167
+ send,
168
+ listen,
169
+ closedDeferred,
170
+ schema,
171
+ supportsTransferables: false,
172
+ shutdown: Scope.close(scope, Exit.void),
173
+ } satisfies WebChannel.WebChannel<typeof WebmeshSchema.Packet.Type, typeof WebmeshSchema.Packet.Type>
174
+
175
+ return { webChannel, from }
176
+ }).pipe(Effect.withSpanScoped('makeWebSocketEdge'), Effect.orDie),
177
+ )
package/tmp/pack.tgz ADDED
Binary file
@@ -1,51 +0,0 @@
1
- import type { Scope } from '@livestore/utils/effect';
2
- import { Effect, Schedule, Schema, WebChannel } from '@livestore/utils/effect';
3
- import type NodeWebSocket from 'ws';
4
- import * as MeshSchema from './mesh-schema.js';
5
- import type { MeshNode } from './node.js';
6
- declare const WSConnectionInit_base: Schema.TaggedStruct<"WSConnectionInit", {
7
- from: typeof Schema.String;
8
- }>;
9
- export declare class WSConnectionInit extends WSConnectionInit_base {
10
- }
11
- declare const WSConnectionPayload_base: Schema.TaggedStruct<"WSConnectionPayload", {
12
- from: typeof Schema.String;
13
- payload: typeof Schema.Any;
14
- }>;
15
- export declare class WSConnectionPayload extends WSConnectionPayload_base {
16
- }
17
- declare const WSConnectionMessage_base: Schema.Union<[typeof WSConnectionInit, typeof WSConnectionPayload]>;
18
- export declare class WSConnectionMessage extends WSConnectionMessage_base {
19
- }
20
- export declare const MessageMsgPack: Schema.transform<Schema.Schema<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>, never>, Schema.Schema<{
21
- readonly _tag: "WSConnectionInit";
22
- readonly from: string;
23
- } | {
24
- readonly _tag: "WSConnectionPayload";
25
- readonly payload: any;
26
- readonly from: string;
27
- }, {
28
- readonly _tag: "WSConnectionInit";
29
- readonly from: string;
30
- } | {
31
- readonly _tag: "WSConnectionPayload";
32
- readonly payload: any;
33
- readonly from: string;
34
- }, never>>;
35
- export type SocketType = {
36
- _tag: 'leaf';
37
- from: string;
38
- } | {
39
- _tag: 'relay';
40
- };
41
- export declare const connectViaWebSocket: ({ node, url, reconnect, }: {
42
- node: MeshNode;
43
- url: string;
44
- reconnect?: Schedule.Schedule<unknown> | false;
45
- }) => Effect.Effect<void, never, Scope.Scope>;
46
- export declare const makeWebSocketConnection: (socket: globalThis.WebSocket | NodeWebSocket.WebSocket, socketType: SocketType) => Effect.Effect<{
47
- webChannel: WebChannel.WebChannel<typeof MeshSchema.Packet.Type, typeof MeshSchema.Packet.Type>;
48
- from: string;
49
- }, never, Scope.Scope>;
50
- export {};
51
- //# sourceMappingURL=websocket-connection.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-connection.d.ts","sourceRoot":"","sources":["../src/websocket-connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAEL,MAAM,EAIN,QAAQ,EACR,MAAM,EAEN,UAAU,EAEX,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,aAAa,MAAM,IAAI,CAAA;AAEnC,OAAO,KAAK,UAAU,MAAM,kBAAkB,CAAA;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;;;;AAEzC,qBAAa,gBAAiB,SAAQ,qBAEpC;CAAG;;;;;AAEL,qBAAa,mBAAoB,SAAQ,wBAGvC;CAAG;;AAEL,qBAAa,mBAAoB,SAAQ,wBAAmD;CAAG;AAE/F,eAAO,MAAM,cAAc;;;;;;;;;;;;;;UAAsC,CAAA;AAEjE,MAAM,MAAM,UAAU,GAClB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb,GACD;IACE,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AAEL,eAAO,MAAM,mBAAmB,8BAI7B;IACD,IAAI,EAAE,QAAQ,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,CAAC,EAAE,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,KAAK,CAAA;CAC/C,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAkBtC,CAAA;AAEJ,eAAO,MAAM,uBAAuB,WAC1B,UAAU,CAAC,SAAS,GAAG,aAAa,CAAC,SAAS,cAC1C,UAAU,KACrB,MAAM,CAAC,MAAM,CACd;IACE,UAAU,EAAE,UAAU,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC/F,IAAI,EAAE,MAAM,CAAA;CACb,EACD,KAAK,EACL,KAAK,CAAC,KAAK,CAgFT,CAAA"}
@@ -1,74 +0,0 @@
1
- import { Deferred, Effect, Either, FiberHandle, Queue, Schedule, Schema, Stream, WebChannel, WebSocket, } from '@livestore/utils/effect';
2
- import * as MeshSchema from './mesh-schema.js';
3
- export class WSConnectionInit extends Schema.TaggedStruct('WSConnectionInit', {
4
- from: Schema.String,
5
- }) {
6
- }
7
- export class WSConnectionPayload extends Schema.TaggedStruct('WSConnectionPayload', {
8
- from: Schema.String,
9
- payload: Schema.Any,
10
- }) {
11
- }
12
- export class WSConnectionMessage extends Schema.Union(WSConnectionInit, WSConnectionPayload) {
13
- }
14
- export const MessageMsgPack = Schema.MsgPack(WSConnectionMessage);
15
- export const connectViaWebSocket = ({ node, url, reconnect = Schedule.exponential(100), }) => Effect.gen(function* () {
16
- const fiberHandle = yield* FiberHandle.make();
17
- const connect = Effect.gen(function* () {
18
- const socket = yield* WebSocket.makeWebSocket({ url, reconnect });
19
- // NOTE we want to use `runFork` here so this Effect is not part of the fiber that will be interrupted
20
- socket.addEventListener('close', () => FiberHandle.run(fiberHandle, connect).pipe(Effect.runFork));
21
- const connection = yield* makeWebSocketConnection(socket, { _tag: 'leaf', from: node.nodeName });
22
- yield* node.addConnection({ target: 'ws', connectionChannel: connection.webChannel, replaceIfExists: true });
23
- yield* Effect.never;
24
- }).pipe(Effect.scoped, Effect.withSpan('@livestore/webmesh:websocket-connection:connect'));
25
- yield* FiberHandle.run(fiberHandle, connect);
26
- });
27
- export const makeWebSocketConnection = (socket, socketType) => Effect.gen(function* () {
28
- socket.binaryType = 'arraybuffer';
29
- const fromDeferred = yield* Deferred.make();
30
- yield* Stream.fromEventListener(socket, 'message').pipe(Stream.map((msg) => Schema.decodeUnknownEither(MessageMsgPack)(new Uint8Array(msg.data))), Stream.flatten(), Stream.tap((msg) => Effect.gen(function* () {
31
- if (msg._tag === 'WSConnectionInit') {
32
- yield* Deferred.succeed(fromDeferred, msg.from);
33
- }
34
- else {
35
- const decodedPayload = yield* Schema.decode(MeshSchema.Packet)(msg.payload);
36
- yield* Queue.offer(listenQueue, decodedPayload);
37
- }
38
- })), Stream.runDrain, Effect.tapCauseLogPretty, Effect.forkScoped);
39
- const listenQueue = yield* Queue.unbounded().pipe(Effect.acquireRelease(Queue.shutdown));
40
- const initHandshake = (from) => socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionInit', from }));
41
- if (socketType._tag === 'leaf') {
42
- initHandshake(socketType.from);
43
- }
44
- const deferredResult = yield* fromDeferred;
45
- const from = socketType._tag === 'leaf' ? socketType.from : deferredResult;
46
- if (socketType._tag === 'relay') {
47
- initHandshake(from);
48
- }
49
- const isConnectedLatch = yield* Effect.makeLatch(true);
50
- const closedDeferred = yield* Deferred.make();
51
- yield* Effect.eventListener(socket, 'close', () => Effect.gen(function* () {
52
- yield* isConnectedLatch.close;
53
- yield* Deferred.succeed(closedDeferred, undefined);
54
- }), { once: true });
55
- const send = (message) => Effect.gen(function* () {
56
- yield* isConnectedLatch.await;
57
- const payload = yield* Schema.encode(MeshSchema.Packet)(message);
58
- socket.send(Schema.encodeSync(MessageMsgPack)({ _tag: 'WSConnectionPayload', payload, from }));
59
- });
60
- const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right));
61
- const webChannel = {
62
- [WebChannel.WebChannelSymbol]: WebChannel.WebChannelSymbol,
63
- send,
64
- listen,
65
- closedDeferred,
66
- schema: { listen: MeshSchema.Packet, send: MeshSchema.Packet },
67
- supportsTransferables: false,
68
- };
69
- return {
70
- webChannel: webChannel,
71
- from,
72
- };
73
- });
74
- //# sourceMappingURL=websocket-connection.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-connection.js","sourceRoot":"","sources":["../src/websocket-connection.ts"],"names":[],"mappings":"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"}
@@ -1,7 +0,0 @@
1
- import type { Scope } from '@livestore/utils/effect';
2
- import { Effect } from '@livestore/utils/effect';
3
- import * as WebSocket from 'ws';
4
- export declare const makeWebSocketServer: ({ relayNodeName, }: {
5
- relayNodeName: string;
6
- }) => Effect.Effect<WebSocket.WebSocketServer, never, Scope.Scope>;
7
- //# sourceMappingURL=websocket-server.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-server.d.ts","sourceRoot":"","sources":["../src/websocket-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,KAAK,SAAS,MAAM,IAAI,CAAA;AAK/B,eAAO,MAAM,mBAAmB,uBAE7B;IACD,aAAa,EAAE,MAAM,CAAA;CACtB,KAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,eAAe,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CA4B3D,CAAA"}
@@ -1,24 +0,0 @@
1
- import { Effect } from '@livestore/utils/effect';
2
- import * as WebSocket from 'ws';
3
- import { makeMeshNode } from './node.js';
4
- import { makeWebSocketConnection } from './websocket-connection.js';
5
- export const makeWebSocketServer = ({ relayNodeName, }) => Effect.gen(function* () {
6
- const server = new WebSocket.WebSocketServer({ noServer: true });
7
- const node = yield* makeMeshNode(relayNodeName);
8
- const runtime = yield* Effect.runtime();
9
- // TODO handle node disconnects (i.e. remove respective connection)
10
- server.on('connection', (socket) => {
11
- Effect.gen(function* () {
12
- const { webChannel, from } = yield* makeWebSocketConnection(socket, { _tag: 'relay' });
13
- yield* node.addConnection({ target: from, connectionChannel: webChannel, replaceIfExists: true });
14
- yield* Effect.log(`WS Relay ${relayNodeName}: added connection from '${from}'`);
15
- socket.addEventListener('close', () => Effect.gen(function* () {
16
- yield* node.removeConnection(from);
17
- yield* Effect.log(`WS Relay ${relayNodeName}: removed connection from '${from}'`);
18
- }).pipe(Effect.provide(runtime), Effect.runFork));
19
- yield* Effect.never;
20
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork);
21
- });
22
- return server;
23
- });
24
- //# sourceMappingURL=websocket-server.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"websocket-server.js","sourceRoot":"","sources":["../src/websocket-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAChD,OAAO,KAAK,SAAS,MAAM,IAAI,CAAA;AAE/B,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AAEnE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,aAAa,GAGd,EAAgE,EAAE,CACjE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;IAClB,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IAEhE,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,CAAA;IAE/C,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAS,CAAA;IAE9C,mEAAmE;IACnE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAM,EAAE,EAAE;QACjC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;YAClB,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC,uBAAuB,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;YAEtF,KAAK,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,EAAE,UAAU,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAA;YACjG,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,aAAa,4BAA4B,IAAI,GAAG,CAAC,CAAA;YAE/E,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CACpC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAClB,KAAK,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAA;gBAClC,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,aAAa,8BAA8B,IAAI,GAAG,CAAC,CAAA;YACnF,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CACjD,CAAA;YAED,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;QACrB,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;IAEF,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA"}
@@ -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
- })
@@ -1,40 +0,0 @@
1
- import type { Scope } from '@livestore/utils/effect'
2
- import { Effect } from '@livestore/utils/effect'
3
- import * as WebSocket from 'ws'
4
-
5
- import { makeMeshNode } from './node.js'
6
- import { makeWebSocketConnection } from './websocket-connection.js'
7
-
8
- export const makeWebSocketServer = ({
9
- relayNodeName,
10
- }: {
11
- relayNodeName: string
12
- }): Effect.Effect<WebSocket.WebSocketServer, never, Scope.Scope> =>
13
- Effect.gen(function* () {
14
- const server = new WebSocket.WebSocketServer({ noServer: true })
15
-
16
- const node = yield* makeMeshNode(relayNodeName)
17
-
18
- const runtime = yield* Effect.runtime<never>()
19
-
20
- // TODO handle node disconnects (i.e. remove respective connection)
21
- server.on('connection', (socket) => {
22
- Effect.gen(function* () {
23
- const { webChannel, from } = yield* makeWebSocketConnection(socket, { _tag: 'relay' })
24
-
25
- yield* node.addConnection({ target: from, connectionChannel: webChannel, replaceIfExists: true })
26
- yield* Effect.log(`WS Relay ${relayNodeName}: added connection from '${from}'`)
27
-
28
- socket.addEventListener('close', () =>
29
- 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),
33
- )
34
-
35
- yield* Effect.never
36
- }).pipe(Effect.scoped, Effect.tapCauseLogPretty, Effect.provide(runtime), Effect.runFork)
37
- })
38
-
39
- return server
40
- })