@livestore/webmesh 0.0.0-snapshot-aed277ba0960f72b8d464508961ab4aec1881230 → 0.0.0-snapshot-7bcbc24bb8873481e482d982636957f0c1f791f6
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 +2 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/channel/message-channel-internal.d.ts.map +1 -1
- package/dist/channel/message-channel-internal.js +17 -2
- package/dist/channel/message-channel-internal.js.map +1 -1
- package/dist/channel/message-channel.d.ts.map +1 -1
- package/dist/channel/message-channel.js +18 -11
- package/dist/channel/message-channel.js.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +1 -0
- package/dist/node.js.map +1 -1
- package/dist/node.test.js +78 -44
- package/dist/node.test.js.map +1 -1
- package/package.json +3 -3
- package/src/channel/message-channel-internal.ts +21 -2
- package/src/channel/message-channel.ts +21 -10
- package/src/node.test.ts +124 -71
- package/src/node.ts +2 -0
|
@@ -90,6 +90,9 @@ export const makeMessageChannelInternal = ({
|
|
|
90
90
|
const deferred = yield* makeDeferredResult()
|
|
91
91
|
|
|
92
92
|
const span = yield* OtelTracer.currentOtelSpan.pipe(Effect.catchAll(() => Effect.succeed(undefined)))
|
|
93
|
+
// const span = {
|
|
94
|
+
// addEvent: (...msg: any[]) => console.log(`${nodeName}→${channelName}→${target}[${channelVersion}]`, ...msg),
|
|
95
|
+
// }
|
|
93
96
|
|
|
94
97
|
const schema = {
|
|
95
98
|
send: Schema.Union(schema_.send, MeshSchema.MessageChannelPing, MeshSchema.MessageChannelPong),
|
|
@@ -111,8 +114,10 @@ export const makeMessageChannelInternal = ({
|
|
|
111
114
|
packetChannelVersion: Predicate.hasProperty('channelVersion')(packet) ? packet.channelVersion : undefined,
|
|
112
115
|
})
|
|
113
116
|
|
|
117
|
+
// const reqIdStr =
|
|
118
|
+
// Predicate.hasProperty('reqId')(packet) && packet.reqId !== undefined ? ` for ${packet.reqId}` : ''
|
|
114
119
|
// yield* Effect.log(
|
|
115
|
-
// `${nodeName}→${channelName}→${target}:process packet ${packet._tag} [${
|
|
120
|
+
// `${nodeName}→${channelName}→${target}[${channelVersion}]:process packet ${packet._tag} [${packet.id}${reqIdStr}], channel state: ${channelState._tag}`,
|
|
116
121
|
// )
|
|
117
122
|
|
|
118
123
|
if (channelState._tag === 'Initial') return shouldNeverHappen()
|
|
@@ -222,6 +227,8 @@ export const makeMessageChannelInternal = ({
|
|
|
222
227
|
|
|
223
228
|
channelStateRef.current = { _tag: 'winner:ResponseSent', channel, otherSourceId: packet.sourceId }
|
|
224
229
|
|
|
230
|
+
// span?.addEvent(`winner side: waiting for ping`)
|
|
231
|
+
|
|
225
232
|
// Now we wait for the other side to respond via the channel
|
|
226
233
|
yield* channel.listen.pipe(
|
|
227
234
|
Stream.flatten(),
|
|
@@ -230,6 +237,8 @@ export const makeMessageChannelInternal = ({
|
|
|
230
237
|
Stream.runDrain,
|
|
231
238
|
)
|
|
232
239
|
|
|
240
|
+
// span?.addEvent(`winner side: sending pong`)
|
|
241
|
+
|
|
233
242
|
yield* channel.send(MeshSchema.MessageChannelPong.make({}))
|
|
234
243
|
|
|
235
244
|
span?.addEvent(`winner side: established`)
|
|
@@ -266,7 +275,17 @@ export const makeMessageChannelInternal = ({
|
|
|
266
275
|
Effect.fork,
|
|
267
276
|
)
|
|
268
277
|
|
|
269
|
-
|
|
278
|
+
// span?.addEvent(`loser side: sending ping`)
|
|
279
|
+
|
|
280
|
+
// There seems to be some scenario where the initial ping message is lost.
|
|
281
|
+
// As a workaround until we find the root cause, we're retrying the ping a few times.
|
|
282
|
+
// TODO write a test that reproduces this issue and fix the root cause ()
|
|
283
|
+
// https://github.com/livestorejs/livestore/issues/262
|
|
284
|
+
yield* channel
|
|
285
|
+
.send(MeshSchema.MessageChannelPing.make({}))
|
|
286
|
+
.pipe(Effect.timeout(10), Effect.retry({ times: 2 }))
|
|
287
|
+
|
|
288
|
+
// span?.addEvent(`loser side: waiting for pong`)
|
|
270
289
|
|
|
271
290
|
yield* waitForPongFiber
|
|
272
291
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
Effect,
|
|
5
5
|
Either,
|
|
6
6
|
Exit,
|
|
7
|
+
Option,
|
|
7
8
|
Queue,
|
|
8
9
|
Schema,
|
|
9
10
|
Scope,
|
|
@@ -55,8 +56,9 @@ export const makeMessageChannel = ({
|
|
|
55
56
|
const debugInfo = {
|
|
56
57
|
pendingSends: 0,
|
|
57
58
|
totalSends: 0,
|
|
58
|
-
connectCounter:
|
|
59
|
+
connectCounter: 0,
|
|
59
60
|
isConnected: false,
|
|
61
|
+
innerChannelRef: { current: undefined as WebChannel.WebChannel<any, any> | undefined },
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// #region reconnect-loop
|
|
@@ -112,26 +114,33 @@ export const makeMessageChannel = ({
|
|
|
112
114
|
newConnectionAvailablePubSub,
|
|
113
115
|
sendPacket,
|
|
114
116
|
scope: makeMessageChannelScope,
|
|
115
|
-
}).pipe(
|
|
117
|
+
}).pipe(
|
|
118
|
+
Scope.extend(makeMessageChannelScope),
|
|
119
|
+
Effect.forkIn(makeMessageChannelScope),
|
|
120
|
+
// Given we only call `Effect.exit` later when joining the fiber,
|
|
121
|
+
// we don't want Effect to produce a "unhandled error" log message
|
|
122
|
+
Effect.withUnhandledErrorLogLevel(Option.none()),
|
|
123
|
+
)
|
|
116
124
|
|
|
117
|
-
const
|
|
125
|
+
const raceResult = yield* Effect.raceFirst(makeChannel, waitForNewConnectionFiber.pipe(Effect.disconnect))
|
|
118
126
|
|
|
119
|
-
if (
|
|
127
|
+
if (raceResult === 'new-connection') {
|
|
120
128
|
yield* Scope.close(makeMessageChannelScope, Exit.fail('new-connection'))
|
|
121
129
|
// We'll try again
|
|
122
130
|
} else {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
yield* Scope.close(makeMessageChannelScope,
|
|
131
|
+
const channelExit = yield* raceResult.pipe(Effect.exit)
|
|
132
|
+
if (channelExit._tag === 'Failure') {
|
|
133
|
+
yield* Scope.close(makeMessageChannelScope, channelExit)
|
|
126
134
|
|
|
127
135
|
if (
|
|
128
|
-
Cause.isFailType(
|
|
129
|
-
Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(
|
|
136
|
+
Cause.isFailType(channelExit.cause) &&
|
|
137
|
+
Schema.is(WebmeshSchema.MessageChannelResponseNoTransferables)(channelExit.cause.error)
|
|
130
138
|
) {
|
|
139
|
+
// Only retry when there is a new connection available
|
|
131
140
|
yield* waitForNewConnectionFiber.pipe(Effect.exit)
|
|
132
141
|
}
|
|
133
142
|
} else {
|
|
134
|
-
const channel =
|
|
143
|
+
const channel = channelExit.value
|
|
135
144
|
|
|
136
145
|
yield* Deferred.succeed(resultDeferred, { channel, makeMessageChannelScope, channelVersion })
|
|
137
146
|
break
|
|
@@ -144,6 +153,7 @@ export const makeMessageChannel = ({
|
|
|
144
153
|
|
|
145
154
|
yield* Effect.spanEvent(`Connected#${channelVersion}`)
|
|
146
155
|
debugInfo.isConnected = true
|
|
156
|
+
debugInfo.innerChannelRef.current = channel
|
|
147
157
|
|
|
148
158
|
yield* Deferred.succeed(initialConnectionDeferred, void 0)
|
|
149
159
|
|
|
@@ -176,6 +186,7 @@ export const makeMessageChannel = ({
|
|
|
176
186
|
|
|
177
187
|
yield* Effect.spanEvent(`Disconnected#${channelVersion}`)
|
|
178
188
|
debugInfo.isConnected = false
|
|
189
|
+
debugInfo.innerChannelRef.current = undefined
|
|
179
190
|
}).pipe(
|
|
180
191
|
Effect.scoped, // Additionally scoping here to clean up finalizers after each loop run
|
|
181
192
|
Effect.forever,
|
package/src/node.test.ts
CHANGED
|
@@ -106,80 +106,79 @@ const propTestTimeout = IS_CI ? 60_000 : 20_000
|
|
|
106
106
|
// TODO also make work without `Vitest.scopedLive` (i.e. with `Vitest.scoped`)
|
|
107
107
|
// probably requires controlling the clocks
|
|
108
108
|
Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
|
|
117
|
-
)
|
|
109
|
+
const Delay = Schema.UndefinedOr(Schema.Literal(0, 1, 10, 50))
|
|
110
|
+
// 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 NodeNames = Schema.Union(
|
|
113
|
+
Schema.Tuple(Schema.Literal('A'), Schema.Literal('B')),
|
|
114
|
+
Schema.Tuple(Schema.Literal('B'), Schema.Literal('A')),
|
|
115
|
+
)
|
|
118
116
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
case 'messagechannel': {
|
|
130
|
-
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
|
|
131
|
-
}
|
|
132
|
-
case 'messagechannel.proxy': {
|
|
133
|
-
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
|
|
134
|
-
}
|
|
135
|
-
}
|
|
117
|
+
const fromChannelType = (
|
|
118
|
+
channelType: typeof ChannelType.Type,
|
|
119
|
+
): {
|
|
120
|
+
mode: 'messagechannel' | 'proxy'
|
|
121
|
+
connectNodes: typeof connectNodesViaMessageChannel | typeof connectNodesViaBroadcastChannel
|
|
122
|
+
} => {
|
|
123
|
+
switch (channelType) {
|
|
124
|
+
case 'proxy': {
|
|
125
|
+
return { mode: 'proxy', connectNodes: connectNodesViaBroadcastChannel }
|
|
136
126
|
}
|
|
127
|
+
case 'messagechannel': {
|
|
128
|
+
return { mode: 'messagechannel', connectNodes: connectNodesViaMessageChannel }
|
|
129
|
+
}
|
|
130
|
+
case 'messagechannel.proxy': {
|
|
131
|
+
return { mode: 'proxy', connectNodes: connectNodesViaMessageChannel }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const exchangeMessages = ({
|
|
137
|
+
nodeX,
|
|
138
|
+
nodeY,
|
|
139
|
+
channelType,
|
|
140
|
+
// numberOfMessages = 1,
|
|
141
|
+
delays,
|
|
142
|
+
}: {
|
|
143
|
+
nodeX: MeshNode
|
|
144
|
+
nodeY: MeshNode
|
|
145
|
+
channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
|
|
146
|
+
numberOfMessages?: number
|
|
147
|
+
delays?: { x?: number; y?: number; connect?: number }
|
|
148
|
+
}) =>
|
|
149
|
+
Effect.gen(function* () {
|
|
150
|
+
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
|
|
151
|
+
const { mode, connectNodes } = fromChannelType(channelType)
|
|
152
|
+
|
|
153
|
+
const nodeXCode = Effect.gen(function* () {
|
|
154
|
+
const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
|
|
155
|
+
|
|
156
|
+
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
157
|
+
// console.log('channelXToY', channelXToY.debugInfo)
|
|
158
|
+
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
159
|
+
// expect(channelXToY.debugInfo.connectCounter).toBe(1)
|
|
160
|
+
})
|
|
137
161
|
|
|
138
|
-
const
|
|
139
|
-
nodeX,
|
|
140
|
-
nodeY,
|
|
141
|
-
channelType,
|
|
142
|
-
// numberOfMessages = 1,
|
|
143
|
-
delays,
|
|
144
|
-
}: {
|
|
145
|
-
nodeX: MeshNode
|
|
146
|
-
nodeY: MeshNode
|
|
147
|
-
channelType: 'messagechannel' | 'proxy' | 'messagechannel.proxy'
|
|
148
|
-
numberOfMessages?: number
|
|
149
|
-
delays?: { x?: number; y?: number; connect?: number }
|
|
150
|
-
}) =>
|
|
151
|
-
Effect.gen(function* () {
|
|
152
|
-
const nodeLabel = { x: nodeX.nodeName, y: nodeY.nodeName }
|
|
153
|
-
const { mode, connectNodes } = fromChannelType(channelType)
|
|
154
|
-
|
|
155
|
-
const nodeXCode = Effect.gen(function* () {
|
|
156
|
-
const channelXToY = yield* createChannel(nodeX, nodeY.nodeName, { mode })
|
|
157
|
-
|
|
158
|
-
yield* channelXToY.send({ message: `${nodeLabel.x}1` })
|
|
159
|
-
// console.log('channelXToY', channelXToY.debugInfo)
|
|
160
|
-
expect(yield* getFirstMessage(channelXToY)).toEqual({ message: `${nodeLabel.y}1` })
|
|
161
|
-
// expect(channelXToY.debugInfo.connectCounter).toBe(1)
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
const nodeYCode = Effect.gen(function* () {
|
|
165
|
-
const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
|
|
166
|
-
|
|
167
|
-
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
168
|
-
// console.log('channelYToX', channelYToX.debugInfo)
|
|
169
|
-
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
170
|
-
// expect(channelYToX.debugInfo.connectCounter).toBe(1)
|
|
171
|
-
})
|
|
162
|
+
const nodeYCode = Effect.gen(function* () {
|
|
163
|
+
const channelYToX = yield* createChannel(nodeY, nodeX.nodeName, { mode })
|
|
172
164
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
],
|
|
179
|
-
{ concurrency: 'unbounded' },
|
|
180
|
-
).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
|
|
181
|
-
})
|
|
165
|
+
yield* channelYToX.send({ message: `${nodeLabel.y}1` })
|
|
166
|
+
// console.log('channelYToX', channelYToX.debugInfo)
|
|
167
|
+
expect(yield* getFirstMessage(channelYToX)).toEqual({ message: `${nodeLabel.x}1` })
|
|
168
|
+
// expect(channelYToX.debugInfo.connectCounter).toBe(1)
|
|
169
|
+
})
|
|
182
170
|
|
|
171
|
+
yield* Effect.all(
|
|
172
|
+
[
|
|
173
|
+
connectNodes(nodeX, nodeY).pipe(maybeDelay(delays?.connect, 'connectNodes')),
|
|
174
|
+
nodeXCode.pipe(maybeDelay(delays?.x, `node${nodeLabel.x}Code`)),
|
|
175
|
+
nodeYCode.pipe(maybeDelay(delays?.y, `node${nodeLabel.y}Code`)),
|
|
176
|
+
],
|
|
177
|
+
{ concurrency: 'unbounded' },
|
|
178
|
+
).pipe(Effect.withSpan(`exchangeMessages(${nodeLabel.x}↔${nodeLabel.y})`))
|
|
179
|
+
})
|
|
180
|
+
Vitest.describe('A <> B', () => {
|
|
181
|
+
Vitest.describe('prop tests', { timeout: propTestTimeout }, () => {
|
|
183
182
|
// const delayX = 40
|
|
184
183
|
// const delayY = undefined
|
|
185
184
|
// const connectDelay = undefined
|
|
@@ -455,6 +454,40 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
455
454
|
})
|
|
456
455
|
})
|
|
457
456
|
|
|
457
|
+
Vitest.describe('message channel specific tests', () => {
|
|
458
|
+
Vitest.scopedLive('differing initial connection counter', (test) =>
|
|
459
|
+
Effect.gen(function* () {
|
|
460
|
+
const nodeA = yield* makeMeshNode('A')
|
|
461
|
+
const nodeB = yield* makeMeshNode('B')
|
|
462
|
+
|
|
463
|
+
yield* connectNodesViaMessageChannel(nodeA, nodeB)
|
|
464
|
+
|
|
465
|
+
const messageCount = 3
|
|
466
|
+
|
|
467
|
+
const bFiber = yield* Effect.gen(function* () {
|
|
468
|
+
const channelBToA = yield* createChannel(nodeB, 'A')
|
|
469
|
+
yield* channelBToA.listen.pipe(
|
|
470
|
+
Stream.flatten(),
|
|
471
|
+
Stream.tap((msg) => channelBToA.send({ message: `resp:${msg.message}` })),
|
|
472
|
+
Stream.take(messageCount),
|
|
473
|
+
Stream.runDrain,
|
|
474
|
+
)
|
|
475
|
+
}).pipe(Effect.scoped, Effect.fork)
|
|
476
|
+
|
|
477
|
+
// yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
478
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
479
|
+
// // yield* createChannel(nodeA, 'B').pipe(Effect.andThen(WebChannel.shutdown))
|
|
480
|
+
yield* Effect.gen(function* () {
|
|
481
|
+
const channelAToB = yield* createChannel(nodeA, 'B')
|
|
482
|
+
yield* channelAToB.send({ message: 'A' })
|
|
483
|
+
expect(yield* getFirstMessage(channelAToB)).toEqual({ message: 'resp:A' })
|
|
484
|
+
}).pipe(Effect.scoped, Effect.repeatN(messageCount))
|
|
485
|
+
|
|
486
|
+
yield* bFiber
|
|
487
|
+
}).pipe(withCtx(test)),
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
458
491
|
Vitest.scopedLive('manual debug test', (test) =>
|
|
459
492
|
Effect.gen(function* () {
|
|
460
493
|
const nodeA = yield* makeMeshNode('A')
|
|
@@ -587,7 +620,7 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
587
620
|
}).pipe(withCtx(test)),
|
|
588
621
|
)
|
|
589
622
|
|
|
590
|
-
Vitest.scopedLive('should fail', (test) =>
|
|
623
|
+
Vitest.scopedLive('should fail with timeout due to missing connection', (test) =>
|
|
591
624
|
Effect.gen(function* () {
|
|
592
625
|
const nodeA = yield* makeMeshNode('A')
|
|
593
626
|
const nodeB = yield* makeMeshNode('B')
|
|
@@ -610,6 +643,27 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
610
643
|
}).pipe(withCtx(test)),
|
|
611
644
|
)
|
|
612
645
|
|
|
646
|
+
Vitest.scopedLive('should fail with timeout due no transferable', (test) =>
|
|
647
|
+
Effect.gen(function* () {
|
|
648
|
+
const nodeA = yield* makeMeshNode('A')
|
|
649
|
+
const nodeB = yield* makeMeshNode('B')
|
|
650
|
+
|
|
651
|
+
yield* connectNodesViaBroadcastChannel(nodeA, nodeB)
|
|
652
|
+
|
|
653
|
+
const nodeACode = Effect.gen(function* () {
|
|
654
|
+
const err = yield* createChannel(nodeA, 'B').pipe(Effect.timeout(200), Effect.flip)
|
|
655
|
+
expect(err._tag).toBe('TimeoutException')
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
const nodeBCode = Effect.gen(function* () {
|
|
659
|
+
const err = yield* createChannel(nodeB, 'A').pipe(Effect.timeout(200), Effect.flip)
|
|
660
|
+
expect(err._tag).toBe('TimeoutException')
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
yield* Effect.all([nodeACode, nodeBCode], { concurrency: 'unbounded' })
|
|
664
|
+
}).pipe(withCtx(test)),
|
|
665
|
+
)
|
|
666
|
+
|
|
613
667
|
Vitest.scopedLive('reconnect with re-created node', (test) =>
|
|
614
668
|
Effect.gen(function* () {
|
|
615
669
|
const nodeCgen1Scope = yield* Scope.make()
|
|
@@ -705,7 +759,6 @@ Vitest.describe('webmesh node', { timeout: testTimeout }, () => {
|
|
|
705
759
|
}).pipe(withCtx(test)),
|
|
706
760
|
)
|
|
707
761
|
|
|
708
|
-
// TODO this currently fails but should work. probably needs some more guarding internally.
|
|
709
762
|
Vitest.scopedLive('should work for messagechannels', (test) =>
|
|
710
763
|
Effect.gen(function* () {
|
|
711
764
|
const nodeA = yield* makeMeshNode('A')
|
package/src/node.ts
CHANGED
|
@@ -156,6 +156,8 @@ export const makeMeshNode = (nodeName: MeshNodeName): Effect.Effect<MeshNode, ne
|
|
|
156
156
|
|
|
157
157
|
const sendPacket = (packet: typeof MeshSchema.Packet.Type) =>
|
|
158
158
|
Effect.gen(function* () {
|
|
159
|
+
// yield* Effect.log(`${nodeName}: sendPacket:${packet._tag} [${packet.id}]`)
|
|
160
|
+
|
|
159
161
|
if (Schema.is(MeshSchema.NetworkConnectionAdded)(packet)) {
|
|
160
162
|
yield* Effect.spanEvent('NetworkConnectionAdded', { packet, nodeName })
|
|
161
163
|
yield* PubSub.publish(newConnectionAvailablePubSub, packet.target)
|