@livestore/webmesh 0.3.0-dev.36 → 0.3.0-dev.38

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 (46) hide show
  1. package/README.md +19 -3
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/channel/{message-channel-internal.d.ts → direct-channel-internal.d.ts} +7 -7
  4. package/dist/channel/direct-channel-internal.d.ts.map +1 -0
  5. package/dist/channel/{message-channel-internal.js → direct-channel-internal.js} +22 -22
  6. package/dist/channel/direct-channel-internal.js.map +1 -0
  7. package/dist/channel/{message-channel.d.ts → direct-channel.d.ts} +3 -3
  8. package/dist/channel/direct-channel.d.ts.map +1 -0
  9. package/dist/channel/{message-channel.js → direct-channel.js} +17 -17
  10. package/dist/channel/direct-channel.js.map +1 -0
  11. package/dist/channel/proxy-channel.d.ts.map +1 -1
  12. package/dist/channel/proxy-channel.js +84 -21
  13. package/dist/channel/proxy-channel.js.map +1 -1
  14. package/dist/common.d.ts +11 -5
  15. package/dist/common.d.ts.map +1 -1
  16. package/dist/common.js +6 -1
  17. package/dist/common.js.map +1 -1
  18. package/dist/mesh-schema.d.ts +15 -15
  19. package/dist/mesh-schema.d.ts.map +1 -1
  20. package/dist/mesh-schema.js +9 -9
  21. package/dist/mesh-schema.js.map +1 -1
  22. package/dist/node.d.ts +10 -5
  23. package/dist/node.d.ts.map +1 -1
  24. package/dist/node.js +68 -30
  25. package/dist/node.js.map +1 -1
  26. package/dist/node.test.js +114 -17
  27. package/dist/node.test.js.map +1 -1
  28. package/dist/websocket-edge.d.ts +2 -1
  29. package/dist/websocket-edge.d.ts.map +1 -1
  30. package/dist/websocket-edge.js +6 -2
  31. package/dist/websocket-edge.js.map +1 -1
  32. package/package.json +8 -4
  33. package/src/channel/{message-channel-internal.ts → direct-channel-internal.ts} +29 -29
  34. package/src/channel/{message-channel.ts → direct-channel.ts} +20 -20
  35. package/src/channel/proxy-channel.ts +107 -25
  36. package/src/common.ts +12 -4
  37. package/src/mesh-schema.ts +16 -19
  38. package/src/node.test.ts +185 -17
  39. package/src/node.ts +97 -35
  40. package/src/websocket-edge.ts +7 -1
  41. package/dist/channel/message-channel-internal.d.ts.map +0 -1
  42. package/dist/channel/message-channel-internal.js.map +0 -1
  43. package/dist/channel/message-channel.d.ts.map +0 -1
  44. package/dist/channel/message-channel.js.map +0 -1
  45. package/tmp/pack.tgz +0 -0
  46. package/tsconfig.json +0 -11
@@ -17,13 +17,13 @@ const defaultPacketFields = {
17
17
  const remainingHopsUndefined = Schema.Undefined.pipe(Schema.optional)
18
18
 
19
19
  /**
20
- * Needs to go through already existing MessageChannel edges, times out otherwise
20
+ * Needs to go through already existing DirectChannel edges, times out otherwise
21
21
  *
22
22
  * Can't yet contain the `port` because the request might be duplicated while forwarding to multiple nodes.
23
23
  * We need a clear path back to the sender to avoid this, thus we respond with a separate
24
- * `MessageChannelResponseSuccess` which contains the `port`.
24
+ * `DirectChannelResponseSuccess` which contains the `port`.
25
25
  */
26
- export class MessageChannelRequest extends Schema.TaggedStruct('MessageChannelRequest', {
26
+ export class DirectChannelRequest extends Schema.TaggedStruct('DirectChannelRequest', {
27
27
  ...defaultPacketFields,
28
28
  remainingHops: Schema.Array(Schema.String).pipe(Schema.optional),
29
29
  channelVersion: Schema.Number,
@@ -36,7 +36,7 @@ export class MessageChannelRequest extends Schema.TaggedStruct('MessageChannelRe
36
36
  sourceId: Schema.String,
37
37
  }) {}
38
38
 
39
- export class MessageChannelResponseSuccess extends Schema.TaggedStruct('MessageChannelResponseSuccess', {
39
+ export class DirectChannelResponseSuccess extends Schema.TaggedStruct('DirectChannelResponseSuccess', {
40
40
  ...defaultPacketFields,
41
41
  reqId: Schema.String,
42
42
  port: Transferable.MessagePort,
@@ -45,14 +45,11 @@ export class MessageChannelResponseSuccess extends Schema.TaggedStruct('MessageC
45
45
  channelVersion: Schema.Number,
46
46
  }) {}
47
47
 
48
- export class MessageChannelResponseNoTransferables extends Schema.TaggedStruct(
49
- 'MessageChannelResponseNoTransferables',
50
- {
51
- ...defaultPacketFields,
52
- reqId: Schema.String,
53
- remainingHops: Schema.Array(Schema.String),
54
- },
55
- ) {}
48
+ export class DirectChannelResponseNoTransferables extends Schema.TaggedStruct('DirectChannelResponseNoTransferables', {
49
+ ...defaultPacketFields,
50
+ reqId: Schema.String,
51
+ remainingHops: Schema.Array(Schema.String),
52
+ }) {}
56
53
 
57
54
  export class ProxyChannelRequest extends Schema.TaggedStruct('ProxyChannelRequest', {
58
55
  ...defaultPacketFields,
@@ -124,10 +121,10 @@ export const BroadcastChannelPacket = Schema.TaggedStruct('BroadcastChannelPacke
124
121
  target: Schema.Literal('-'),
125
122
  })
126
123
 
127
- export class MessageChannelPacket extends Schema.Union(
128
- MessageChannelRequest,
129
- MessageChannelResponseSuccess,
130
- MessageChannelResponseNoTransferables,
124
+ export class DirectChannelPacket extends Schema.Union(
125
+ DirectChannelRequest,
126
+ DirectChannelResponseSuccess,
127
+ DirectChannelResponseNoTransferables,
131
128
  ) {}
132
129
 
133
130
  export class ProxyChannelPacket extends Schema.Union(
@@ -138,7 +135,7 @@ export class ProxyChannelPacket extends Schema.Union(
138
135
  ) {}
139
136
 
140
137
  export class Packet extends Schema.Union(
141
- MessageChannelPacket,
138
+ DirectChannelPacket,
142
139
  ProxyChannelPacket,
143
140
  NetworkEdgeAdded,
144
141
  NetworkTopologyRequest,
@@ -146,5 +143,5 @@ export class Packet extends Schema.Union(
146
143
  BroadcastChannelPacket,
147
144
  ) {}
148
145
 
149
- export class MessageChannelPing extends Schema.TaggedStruct('MessageChannelPing', {}) {}
150
- export class MessageChannelPong extends Schema.TaggedStruct('MessageChannelPong', {}) {}
146
+ export class DirectChannelPing extends Schema.TaggedStruct('DirectChannelPing', {}) {}
147
+ export class DirectChannelPong extends Schema.TaggedStruct('DirectChannelPong', {}) {}
package/src/node.test.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  identity,
10
10
  Layer,
11
11
  Logger,
12
+ LogLevel,
12
13
  Schema,
13
14
  Scope,
14
15
  Stream,
@@ -80,7 +81,7 @@ const createChannel = (source: MeshNode, target: string, options?: Partial<Param
80
81
  channelName: options?.channelName ?? 'test',
81
82
  schema: ExampleSchema,
82
83
  // transferables: options?.transferables ?? 'prefer',
83
- mode: options?.mode ?? 'messagechannel',
84
+ mode: options?.mode ?? 'direct',
84
85
  timeout: options?.timeout ?? 200,
85
86
  })
86
87
 
@@ -108,7 +109,7 @@ const propTestTimeout = IS_CI ? 60_000 : 20_000
108
109
  Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
109
110
  const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
110
111
  // NOTE for message channels, we test both with and without transferables (i.e. proxying)
111
- const ChannelType = Schema.Literal('messagechannel', 'messagechannel.proxy', 'proxy')
112
+ const ChannelType = Schema.Literal('direct', 'proxy(via-messagechannel-edge)', 'proxy')
112
113
  const NodeNames = Schema.Union(
113
114
  Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
114
115
  Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
@@ -117,17 +118,17 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
117
118
  const fromChannelType = (
118
119
  channelType: typeof ChannelType.Type,
119
120
  ): {
120
- mode: 'messagechannel' | 'proxy'
121
+ mode: 'direct' | 'proxy'
121
122
  connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
122
123
  } => {
123
124
  switch (channelType) {
124
125
  case 'proxy': {
125
126
  return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
126
127
  }
127
- case 'messagechannel': {
128
- return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
128
+ case 'direct': {
129
+ return { mode: 'direct', connectNodes: connectNodesViaMessageChannel }
129
130
  }
130
- case 'messagechannel.proxy': {
131
+ case 'proxy(via-messagechannel-edge)': {
131
132
  return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
132
133
  }
133
134
  }
@@ -142,7 +143,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
142
143
  }: {
143
144
  nodeX: MeshNode
144
145
  nodeY: MeshNode
145
- channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
146
+ channelType: 'direct' | 'proxy' | 'proxy(via-messagechannel-edge)'
146
147
  numberOfMessages?: number
147
148
  delays?: { x?: number; y?: number; connect?: number }
148
149
  }) =>
@@ -177,12 +178,13 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
177
178
  { concurrency: 'unbounded' },
178
179
  ).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
179
180
  })
181
+
180
182
  Vitest.describe('A <> B', () => {
181
183
  Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
182
184
  // const delayX = 40
183
185
  // const delayY = undefined
184
186
  // const connectDelay = undefined
185
- // const channelType = 'messagechannel'
187
+ // const channelType = 'direct'
186
188
  // const nodeNames = ['B', 'A'] as const
187
189
  // Vitest.scopedLive(
188
190
  // 'a / b connect at different times with different channel types',
@@ -218,7 +220,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
218
220
  {
219
221
  // const waitForOfflineDelay = undefined
220
222
  // const sleepDelay = 0
221
- // const channelType = 'messagechannel'
223
+ // const channelType = 'direct'
222
224
  // Vitest.scopedLive(
223
225
  // 'b reconnects',
224
226
  // (test) =>
@@ -342,22 +344,22 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
342
344
  }).pipe(withCtx(test)),
343
345
  )
344
346
 
345
- const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'messagechannel')
347
+ const ChannelTypeWithoutMessageChannelProxy = Schema.Literal('proxy', 'direct')
346
348
  // TODO there seems to be a flaky case here which gets hit sometimes (e.g. 2025-02-28-17:11)
347
349
  // Log output:
348
350
  // test: { seed: -964670352, path: "1", endOnFailure: true }
349
- // test: Counterexample: ["messagechannel",["A","B"]]
351
+ // test: Counterexample: ["direct",["A","B"]]
350
352
  // test: Shrunk 0 time(s)
351
353
  // test: Got AssertionError: expected { _tag: 'MessageChannelPing' } to deeply equal { message: 'A1' }
352
354
  // test: at next (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:376:59)
353
- // test: at prop tests:replace edge while keeping the channel:channelType=messagechannel nodeNames=A,B (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:801:14)
355
+ // test: at prop tests:replace edge while keeping the channel:channelType=direct nodeNames=A,B (/Users/schickling/Code/overtone/submodules/livestore/packages/@livestore/webmesh/src/node.test.ts:801:14)
354
356
  // test: Hint: Enable verbose mode in order to have the list of all failing values encountered during the run
355
357
  // test: ✓ webmesh node > A <> B > prop tests > TODO improve latency > concurrent messages 2110ms
356
358
  // test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
357
359
  // test: FAIL src/node.test.ts > webmesh node > A <> B > prop tests > replace edge while keeping the channel
358
360
  // test: Error: Property failed after 2 tests
359
361
  // test: { seed: -964670352, path: "1", endOnFailure: true }
360
- // test: Counterexample: ["messagechannel",["A","B"]]
362
+ // test: Counterexample: ["direct",["A","B"]]
361
363
  Vitest.scopedLive.prop(
362
364
  'replace edge while keeping the channel',
363
365
  [ChannelTypeWithoutMessageChannelProxy, NodeNames],
@@ -540,7 +542,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
540
542
 
541
543
  yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
542
544
 
543
- const err = yield* createChannel(nodeA, 'B', { mode: 'messagechannel' }).pipe(Effect.timeout(200), Effect.flip)
545
+ const err = yield* createChannel(nodeA, 'B', { mode: 'direct' }).pipe(Effect.timeout(200), Effect.flip)
544
546
  expect(err._tag).toBe('TimeoutException')
545
547
  }).pipe(withCtx(test)),
546
548
  )
@@ -805,9 +807,9 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
805
807
  )
806
808
  })
807
809
 
808
- Vitest.describe('mixture of messagechannel and proxy edge connections', () => {
810
+ Vitest.describe('mixture of direct and proxy edge connections', () => {
809
811
  // TODO test case to better guard against case where side A tries to create a proxy channel to B
810
- // and side B tries to create a messagechannel to A
812
+ // and side B tries to create a direct to A
811
813
  Vitest.scopedLive('should work for proxy channels', (test) =>
812
814
  Effect.gen(function* () {
813
815
  const nodeA = yield* makeMeshNode('A')
@@ -820,7 +822,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
820
822
  }).pipe(withCtx(test)),
821
823
  )
822
824
 
823
- Vitest.scopedLive('should work for messagechannels', (test) =>
825
+ Vitest.scopedLive('should work for directs', (test) =>
824
826
  Effect.gen(function* () {
825
827
  const nodeA = yield* makeMeshNode('A')
826
828
  const nodeB = yield* makeMeshNode('B')
@@ -846,6 +848,171 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
846
848
  )
847
849
  })
848
850
 
851
+ Vitest.describe('listenForChannel', () => {
852
+ Vitest.scopedLive('connect later', (test) =>
853
+ Effect.gen(function* () {
854
+ const nodeA = yield* makeMeshNode('A')
855
+
856
+ const mode = 'direct' as 'proxy' | 'direct'
857
+ const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel
858
+
859
+ const nodeACode = Effect.gen(function* () {
860
+ const channelAToB = yield* createChannel(nodeA, 'B', { channelName: 'test', mode })
861
+ yield* channelAToB.send({ message: 'A1' })
862
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
863
+ })
864
+
865
+ const nodeBCode = Effect.gen(function* () {
866
+ const nodeB = yield* makeMeshNode('B')
867
+ yield* connect(nodeA, nodeB)
868
+
869
+ yield* nodeB.listenForChannel.pipe(
870
+ Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode),
871
+ Stream.tap(
872
+ Effect.fn(function* (channelInfo) {
873
+ const channel = yield* createChannel(nodeB, channelInfo.source, {
874
+ channelName: channelInfo.channelName,
875
+ mode,
876
+ })
877
+ yield* channel.send({ message: 'B1' })
878
+ expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' })
879
+ }),
880
+ ),
881
+ Stream.take(1),
882
+ Stream.runDrain,
883
+ )
884
+ })
885
+
886
+ yield* Effect.all([nodeACode, nodeBCode.pipe(Effect.delay(500))], { concurrency: 'unbounded' })
887
+ }).pipe(withCtx(test)),
888
+ )
889
+
890
+ // TODO provide a way to allow for reconnecting in the `listenForChannel` case
891
+ Vitest.scopedLive.skip('reconnect', (test) =>
892
+ Effect.gen(function* () {
893
+ const nodeA = yield* makeMeshNode('A')
894
+
895
+ const mode = 'direct' as 'proxy' | 'direct'
896
+ const connect = mode === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel
897
+
898
+ const nodeACode = Effect.gen(function* () {
899
+ const channelAToB = yield* createChannel(nodeA, 'B', { channelName: 'test', mode })
900
+ yield* channelAToB.send({ message: 'A1' })
901
+ expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'B1' })
902
+ })
903
+
904
+ const nodeBCode = Effect.gen(function* () {
905
+ const nodeB = yield* makeMeshNode('B')
906
+ yield* connect(nodeA, nodeB)
907
+
908
+ yield* nodeB.listenForChannel.pipe(
909
+ Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode),
910
+ Stream.tap(
911
+ Effect.fn(function* (channelInfo) {
912
+ const channel = yield* createChannel(nodeB, channelInfo.source, {
913
+ channelName: channelInfo.channelName,
914
+ mode,
915
+ })
916
+ yield* channel.send({ message: 'B1' })
917
+ expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' })
918
+ }),
919
+ ),
920
+ Stream.take(1),
921
+ Stream.runDrain,
922
+ )
923
+ }).pipe(
924
+ Effect.withSpan('nodeBCode:gen1'),
925
+ Effect.andThen(
926
+ Effect.gen(function* () {
927
+ const nodeB = yield* makeMeshNode('B')
928
+ yield* connect(nodeA, nodeB, { replaceIfExists: true })
929
+
930
+ yield* nodeB.listenForChannel.pipe(
931
+ Stream.filter((_) => _.channelName === 'test' && _.source === 'A' && _.mode === mode),
932
+ Stream.tap(
933
+ Effect.fn(function* (channelInfo) {
934
+ const channel = yield* createChannel(nodeB, channelInfo.source, {
935
+ channelName: channelInfo.channelName,
936
+ mode,
937
+ })
938
+ console.log('recreated channel', channel)
939
+ // yield* channel.send({ message: 'B1' })
940
+ // expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' })
941
+ }),
942
+ ),
943
+ Stream.take(1),
944
+ Stream.runDrain,
945
+ )
946
+ }).pipe(Effect.withSpan('nodeBCode:gen2')),
947
+ ),
948
+ )
949
+
950
+ yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
951
+ }).pipe(withCtx(test)),
952
+ )
953
+
954
+ Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
955
+ Vitest.scopedLive.prop(
956
+ 'listenForChannel A <> B <> C',
957
+ [Delay, Delay, Delay, Delay, ChannelType],
958
+ ([delayNodeA, delayNodeC, delayConnectAB, delayConnectBC, channelType], test) =>
959
+ Effect.gen(function* () {
960
+ const nodeA = yield* makeMeshNode('A')
961
+ const nodeB = yield* makeMeshNode('B')
962
+ const nodeC = yield* makeMeshNode('C')
963
+
964
+ const mode = channelType.includes('proxy') ? 'proxy' : 'direct'
965
+ const connect = channelType === 'direct' ? connectNodesViaMessageChannel : connectNodesViaBroadcastChannel
966
+ yield* connect(nodeA, nodeB).pipe(maybeDelay(delayConnectAB, 'delayConnectAB'))
967
+ yield* connect(nodeB, nodeC).pipe(maybeDelay(delayConnectBC, 'delayConnectBC'))
968
+
969
+ const nodeACode = Effect.gen(function* () {
970
+ const _channel2AToC = yield* createChannel(nodeA, 'C', { channelName: 'test-2', mode })
971
+
972
+ const channelAToC = yield* createChannel(nodeA, 'C', { channelName: 'test-1', mode })
973
+ yield* channelAToC.send({ message: 'A1' })
974
+ expect(yield* getFirstMessage(channelAToC)).toEqual({ message: 'C1' })
975
+ })
976
+
977
+ const nodeCCode = Effect.gen(function* () {
978
+ const _channel2CToA = yield* createChannel(nodeC, 'A', { channelName: 'test-2', mode })
979
+
980
+ yield* nodeC.listenForChannel.pipe(
981
+ Stream.filter((_) => _.channelName === 'test-1' && _.source === 'A' && _.mode === mode),
982
+ Stream.tap(
983
+ Effect.fn(function* (channelInfo) {
984
+ const channel = yield* createChannel(nodeC, channelInfo.source, {
985
+ channelName: channelInfo.channelName,
986
+ mode,
987
+ })
988
+ yield* channel.send({ message: 'C1' })
989
+ expect(yield* getFirstMessage(channel)).toEqual({ message: 'A1' })
990
+ }),
991
+ ),
992
+ Stream.take(1),
993
+ Stream.runDrain,
994
+ )
995
+ })
996
+
997
+ yield* Effect.all(
998
+ [
999
+ nodeACode.pipe(maybeDelay(delayNodeA, 'nodeACode')),
1000
+ nodeCCode.pipe(maybeDelay(delayNodeC, 'nodeCCode')),
1001
+ ],
1002
+ { concurrency: 'unbounded' },
1003
+ )
1004
+ }).pipe(
1005
+ withCtx(test, {
1006
+ skipOtel: true,
1007
+ suffix: `delayNodeA=${delayNodeA} delayNodeC=${delayNodeC} delayConnectAB=${delayConnectAB} delayConnectBC=${delayConnectBC} channelType=${channelType}`,
1008
+ timeout: testTimeout * 2,
1009
+ }),
1010
+ ),
1011
+ { fastCheck: { numRuns: 10 } },
1012
+ )
1013
+ })
1014
+ })
1015
+
849
1016
  Vitest.describe('broadcast channel', () => {
850
1017
  Vitest.scopedLive('should work', (test) =>
851
1018
  Effect.gen(function* () {
@@ -893,6 +1060,7 @@ const withCtx =
893
1060
  self.pipe(
894
1061
  Effect.timeout(timeout),
895
1062
  Effect.provide(Logger.pretty),
1063
+ Logger.withMinimumLogLevel(LogLevel.Debug),
896
1064
  Effect.scoped, // We need to scope the effect manually here because otherwise the span is not closed
897
1065
  Effect.withSpan(`${testContext.task.suite?.name}:${testContext.task.name}${suffix ? `:${suffix}` : ''}`),
898
1066
  skipOtel ? identity : Effect.provide(otelLayer),