@livestore/sync-cf 0.4.0-dev.2 → 0.4.0-dev.5

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 (113) hide show
  1. package/README.md +60 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cf-worker/do/durable-object.d.ts +45 -0
  4. package/dist/cf-worker/do/durable-object.d.ts.map +1 -0
  5. package/dist/cf-worker/do/durable-object.js +154 -0
  6. package/dist/cf-worker/do/durable-object.js.map +1 -0
  7. package/dist/cf-worker/do/layer.d.ts +34 -0
  8. package/dist/cf-worker/do/layer.d.ts.map +1 -0
  9. package/dist/cf-worker/do/layer.js +68 -0
  10. package/dist/cf-worker/do/layer.js.map +1 -0
  11. package/dist/cf-worker/do/pull.d.ts +6 -0
  12. package/dist/cf-worker/do/pull.d.ts.map +1 -0
  13. package/dist/cf-worker/do/pull.js +39 -0
  14. package/dist/cf-worker/do/pull.js.map +1 -0
  15. package/dist/cf-worker/do/push.d.ts +14 -0
  16. package/dist/cf-worker/do/push.d.ts.map +1 -0
  17. package/dist/cf-worker/do/push.js +99 -0
  18. package/dist/cf-worker/do/push.js.map +1 -0
  19. package/dist/cf-worker/do/sqlite.d.ts +196 -0
  20. package/dist/cf-worker/do/sqlite.d.ts.map +1 -0
  21. package/dist/cf-worker/do/sqlite.js +27 -0
  22. package/dist/cf-worker/do/sqlite.js.map +1 -0
  23. package/dist/cf-worker/do/sync-storage.d.ts +17 -0
  24. package/dist/cf-worker/do/sync-storage.d.ts.map +1 -0
  25. package/dist/cf-worker/do/sync-storage.js +73 -0
  26. package/dist/cf-worker/do/sync-storage.js.map +1 -0
  27. package/dist/cf-worker/do/transport/do-rpc-server.d.ts +8 -0
  28. package/dist/cf-worker/do/transport/do-rpc-server.d.ts.map +1 -0
  29. package/dist/cf-worker/do/transport/do-rpc-server.js +45 -0
  30. package/dist/cf-worker/do/transport/do-rpc-server.js.map +1 -0
  31. package/dist/cf-worker/do/transport/http-rpc-server.d.ts +7 -0
  32. package/dist/cf-worker/do/transport/http-rpc-server.d.ts.map +1 -0
  33. package/dist/cf-worker/do/transport/http-rpc-server.js +24 -0
  34. package/dist/cf-worker/do/transport/http-rpc-server.js.map +1 -0
  35. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts +4 -0
  36. package/dist/cf-worker/do/transport/ws-rpc-server.d.ts.map +1 -0
  37. package/dist/cf-worker/do/transport/ws-rpc-server.js +21 -0
  38. package/dist/cf-worker/do/transport/ws-rpc-server.js.map +1 -0
  39. package/dist/cf-worker/mod.d.ts +4 -2
  40. package/dist/cf-worker/mod.d.ts.map +1 -1
  41. package/dist/cf-worker/mod.js +3 -2
  42. package/dist/cf-worker/mod.js.map +1 -1
  43. package/dist/cf-worker/shared.d.ts +127 -0
  44. package/dist/cf-worker/shared.d.ts.map +1 -0
  45. package/dist/cf-worker/shared.js +26 -0
  46. package/dist/cf-worker/shared.js.map +1 -0
  47. package/dist/cf-worker/worker.d.ts +36 -21
  48. package/dist/cf-worker/worker.d.ts.map +1 -1
  49. package/dist/cf-worker/worker.js +39 -32
  50. package/dist/cf-worker/worker.js.map +1 -1
  51. package/dist/client/mod.d.ts +4 -0
  52. package/dist/client/mod.d.ts.map +1 -0
  53. package/dist/client/mod.js +4 -0
  54. package/dist/client/mod.js.map +1 -0
  55. package/dist/client/transport/do-rpc-client.d.ts +40 -0
  56. package/dist/client/transport/do-rpc-client.d.ts.map +1 -0
  57. package/dist/client/transport/do-rpc-client.js +102 -0
  58. package/dist/client/transport/do-rpc-client.js.map +1 -0
  59. package/dist/client/transport/http-rpc-client.d.ts +43 -0
  60. package/dist/client/transport/http-rpc-client.d.ts.map +1 -0
  61. package/dist/client/transport/http-rpc-client.js +87 -0
  62. package/dist/client/transport/http-rpc-client.js.map +1 -0
  63. package/dist/client/transport/ws-rpc-client.d.ts +45 -0
  64. package/dist/client/transport/ws-rpc-client.d.ts.map +1 -0
  65. package/dist/client/transport/ws-rpc-client.js +94 -0
  66. package/dist/client/transport/ws-rpc-client.js.map +1 -0
  67. package/dist/common/do-rpc-schema.d.ts +76 -0
  68. package/dist/common/do-rpc-schema.d.ts.map +1 -0
  69. package/dist/common/do-rpc-schema.js +48 -0
  70. package/dist/common/do-rpc-schema.js.map +1 -0
  71. package/dist/common/http-rpc-schema.d.ts +58 -0
  72. package/dist/common/http-rpc-schema.d.ts.map +1 -0
  73. package/dist/common/http-rpc-schema.js +37 -0
  74. package/dist/common/http-rpc-schema.js.map +1 -0
  75. package/dist/common/mod.d.ts +5 -1
  76. package/dist/common/mod.d.ts.map +1 -1
  77. package/dist/common/mod.js +4 -1
  78. package/dist/common/mod.js.map +1 -1
  79. package/dist/common/sync-message-types.d.ts +236 -0
  80. package/dist/common/sync-message-types.d.ts.map +1 -0
  81. package/dist/common/sync-message-types.js +60 -0
  82. package/dist/common/sync-message-types.js.map +1 -0
  83. package/dist/common/ws-rpc-schema.d.ts +55 -0
  84. package/dist/common/ws-rpc-schema.d.ts.map +1 -0
  85. package/dist/common/ws-rpc-schema.js +32 -0
  86. package/dist/common/ws-rpc-schema.js.map +1 -0
  87. package/package.json +7 -8
  88. package/src/cf-worker/do/durable-object.ts +241 -0
  89. package/src/cf-worker/do/layer.ts +107 -0
  90. package/src/cf-worker/do/pull.ts +64 -0
  91. package/src/cf-worker/do/push.ts +162 -0
  92. package/src/cf-worker/do/sqlite.ts +28 -0
  93. package/src/cf-worker/do/sync-storage.ts +126 -0
  94. package/src/cf-worker/do/transport/do-rpc-server.ts +82 -0
  95. package/src/cf-worker/do/transport/http-rpc-server.ts +37 -0
  96. package/src/cf-worker/do/transport/ws-rpc-server.ts +34 -0
  97. package/src/cf-worker/mod.ts +4 -2
  98. package/src/cf-worker/shared.ts +95 -0
  99. package/src/cf-worker/worker.ts +72 -63
  100. package/src/client/mod.ts +3 -0
  101. package/src/client/transport/do-rpc-client.ts +171 -0
  102. package/src/client/transport/http-rpc-client.ts +205 -0
  103. package/src/client/transport/ws-rpc-client.ts +182 -0
  104. package/src/common/do-rpc-schema.ts +54 -0
  105. package/src/common/http-rpc-schema.ts +40 -0
  106. package/src/common/mod.ts +8 -1
  107. package/src/common/sync-message-types.ts +117 -0
  108. package/src/common/ws-rpc-schema.ts +36 -0
  109. package/src/cf-worker/cf-types.ts +0 -12
  110. package/src/cf-worker/durable-object.ts +0 -478
  111. package/src/common/ws-message-types.ts +0 -114
  112. package/src/sync-impl/mod.ts +0 -1
  113. package/src/sync-impl/ws-impl.ts +0 -274
@@ -1,114 +0,0 @@
1
- import { LiveStoreEvent } from '@livestore/common/schema'
2
- import { Schema } from '@livestore/utils/effect'
3
-
4
- export const PullReq = Schema.TaggedStruct('WSMessage.PullReq', {
5
- requestId: Schema.String,
6
- /** Omitting the cursor will start from the beginning */
7
- cursor: Schema.optional(Schema.Number),
8
- }).annotations({ title: '@livestore/sync-cf:PullReq' })
9
-
10
- export type PullReq = typeof PullReq.Type
11
-
12
- export const SyncMetadata = Schema.Struct({
13
- /** ISO date format */
14
- createdAt: Schema.String,
15
- }).annotations({ title: '@livestore/sync-cf:SyncMetadata' })
16
-
17
- export type SyncMetadata = typeof SyncMetadata.Type
18
-
19
- export const PullRes = Schema.TaggedStruct('WSMessage.PullRes', {
20
- batch: Schema.Array(
21
- Schema.Struct({
22
- eventEncoded: LiveStoreEvent.AnyEncodedGlobal,
23
- metadata: Schema.Option(SyncMetadata),
24
- }),
25
- ),
26
- requestId: Schema.Struct({ context: Schema.Literal('pull', 'push'), requestId: Schema.String }),
27
- remaining: Schema.Number,
28
- }).annotations({ title: '@livestore/sync-cf:PullRes' })
29
-
30
- export type PullRes = typeof PullRes.Type
31
-
32
- export const PushReq = Schema.TaggedStruct('WSMessage.PushReq', {
33
- requestId: Schema.String,
34
- batch: Schema.Array(LiveStoreEvent.AnyEncodedGlobal),
35
- }).annotations({ title: '@livestore/sync-cf:PushReq' })
36
-
37
- export type PushReq = typeof PushReq.Type
38
-
39
- export const PushAck = Schema.TaggedStruct('WSMessage.PushAck', {
40
- requestId: Schema.String,
41
- }).annotations({ title: '@livestore/sync-cf:PushAck' })
42
-
43
- export type PushAck = typeof PushAck.Type
44
-
45
- export const Error = Schema.TaggedStruct('WSMessage.Error', {
46
- requestId: Schema.String,
47
- message: Schema.String,
48
- }).annotations({ title: '@livestore/sync-cf:Error' })
49
-
50
- export type Error = typeof Error.Type
51
-
52
- export const Ping = Schema.TaggedStruct('WSMessage.Ping', {
53
- requestId: Schema.Literal('ping'),
54
- }).annotations({ title: '@livestore/sync-cf:Ping' })
55
-
56
- export type Ping = typeof Ping.Type
57
-
58
- export const Pong = Schema.TaggedStruct('WSMessage.Pong', {
59
- requestId: Schema.Literal('ping'),
60
- }).annotations({ title: '@livestore/sync-cf:Pong' })
61
-
62
- export type Pong = typeof Pong.Type
63
-
64
- export const AdminResetRoomReq = Schema.TaggedStruct('WSMessage.AdminResetRoomReq', {
65
- requestId: Schema.String,
66
- adminSecret: Schema.String,
67
- }).annotations({ title: '@livestore/sync-cf:AdminResetRoomReq' })
68
-
69
- export type AdminResetRoomReq = typeof AdminResetRoomReq.Type
70
-
71
- export const AdminResetRoomRes = Schema.TaggedStruct('WSMessage.AdminResetRoomRes', {
72
- requestId: Schema.String,
73
- }).annotations({ title: '@livestore/sync-cf:AdminResetRoomRes' })
74
-
75
- export type AdminResetRoomRes = typeof AdminResetRoomRes.Type
76
-
77
- export const AdminInfoReq = Schema.TaggedStruct('WSMessage.AdminInfoReq', {
78
- requestId: Schema.String,
79
- adminSecret: Schema.String,
80
- }).annotations({ title: '@livestore/sync-cf:AdminInfoReq' })
81
-
82
- export type AdminInfoReq = typeof AdminInfoReq.Type
83
-
84
- export const AdminInfoRes = Schema.TaggedStruct('WSMessage.AdminInfoRes', {
85
- requestId: Schema.String,
86
- info: Schema.Struct({
87
- durableObjectId: Schema.String,
88
- }),
89
- }).annotations({ title: '@livestore/sync-cf:AdminInfoRes' })
90
-
91
- export type AdminInfoRes = typeof AdminInfoRes.Type
92
-
93
- export const Message = Schema.Union(
94
- PullReq,
95
- PullRes,
96
- PushReq,
97
- PushAck,
98
- Error,
99
- Ping,
100
- Pong,
101
- AdminResetRoomReq,
102
- AdminResetRoomRes,
103
- AdminInfoReq,
104
- AdminInfoRes,
105
- ).annotations({ title: '@livestore/sync-cf:Message' })
106
-
107
- export type Message = typeof Message.Type
108
- export type MessageEncoded = typeof Message.Encoded
109
-
110
- export const BackendToClientMessage = Schema.Union(PullRes, PushAck, AdminResetRoomRes, AdminInfoRes, Error, Pong)
111
- export type BackendToClientMessage = typeof BackendToClientMessage.Type
112
-
113
- export const ClientToBackendMessage = Schema.Union(PullReq, PushReq, AdminResetRoomReq, AdminInfoReq, Ping)
114
- export type ClientToBackendMessage = typeof ClientToBackendMessage.Type
@@ -1 +0,0 @@
1
- export * from './ws-impl.ts'
@@ -1,274 +0,0 @@
1
- /// <reference lib="dom" />
2
-
3
- import type { SyncBackend, SyncBackendConstructor } from '@livestore/common'
4
- import { InvalidPullError, InvalidPushError, UnexpectedError } from '@livestore/common'
5
- import { EventSequenceNumber } from '@livestore/common/schema'
6
- import { LS_DEV, shouldNeverHappen } from '@livestore/utils'
7
- import {
8
- Deferred,
9
- Effect,
10
- Option,
11
- PubSub,
12
- Queue,
13
- Schedule,
14
- Schema,
15
- Stream,
16
- SubscriptionRef,
17
- UrlParams,
18
- WebSocket,
19
- } from '@livestore/utils/effect'
20
- import { nanoid } from '@livestore/utils/nanoid'
21
-
22
- import { SearchParamsSchema, WSMessage } from '../common/mod.ts'
23
- import type { SyncMetadata } from '../common/ws-message-types.ts'
24
-
25
- export interface WsSyncOptions {
26
- url: string
27
- }
28
-
29
- export const makeCfSync =
30
- (options: WsSyncOptions): SyncBackendConstructor<SyncMetadata> =>
31
- ({ storeId, payload }) =>
32
- Effect.gen(function* () {
33
- const urlParamsData = yield* Schema.encode(SearchParamsSchema)({
34
- storeId,
35
- payload,
36
- }).pipe(UnexpectedError.mapToUnexpectedError)
37
-
38
- const urlParams = UrlParams.fromInput(urlParamsData)
39
- const wsUrl = `${options.url}/websocket?${UrlParams.toString(urlParams)}`
40
-
41
- const { isConnected, incomingMessages, send } = yield* connect(wsUrl)
42
-
43
- /**
44
- * We need to account for the scenario where push-caused PullRes message arrive before the pull-caused PullRes message.
45
- * i.e. a scenario where the WS connection is created but before the server processed the initial pull, a push from
46
- * another client triggers a PullRes message sent to this client which we need to stash until our pull-caused
47
- * PullRes message arrives at which point we can combine the stashed events with the pull-caused events and continue.
48
- */
49
- const stashedPullBatch: WSMessage.PullRes['batch'][number][] = []
50
-
51
- // We currently only support one pull stream for a sync backend.
52
- let pullStarted = false
53
-
54
- const api = {
55
- isConnected,
56
- // Currently we're already eagerly connecting when the sync backend is created but we might want to refactor this later to clean this up
57
- connect: Effect.void,
58
- pull: (args) =>
59
- Effect.gen(function* () {
60
- if (pullStarted) {
61
- return shouldNeverHappen(`Pull already started for this sync backend.`)
62
- }
63
-
64
- pullStarted = true
65
-
66
- let pullResponseReceived = false
67
-
68
- const requestId = nanoid()
69
- const cursor = Option.getOrUndefined(args)?.cursor.global
70
-
71
- yield* send(WSMessage.PullReq.make({ cursor, requestId }))
72
-
73
- return Stream.fromPubSub(incomingMessages).pipe(
74
- Stream.tap((_) =>
75
- _._tag === 'WSMessage.Error' && _.requestId === requestId
76
- ? new InvalidPullError({ message: _.message })
77
- : Effect.void,
78
- ),
79
- Stream.filterMap((msg) => {
80
- if (msg._tag === 'WSMessage.PullRes') {
81
- if (msg.requestId.context === 'pull') {
82
- if (msg.requestId.requestId === requestId) {
83
- pullResponseReceived = true
84
-
85
- if (stashedPullBatch.length > 0 && msg.remaining === 0) {
86
- const pullResHead = msg.batch.at(-1)?.eventEncoded.seqNum ?? EventSequenceNumber.ROOT.global
87
- // Index where stashed events are greater than pullResHead
88
- const newPartialBatchIndex = stashedPullBatch.findIndex(
89
- (batchItem) => batchItem.eventEncoded.seqNum > pullResHead,
90
- )
91
- const batchWithNewStashedEvents =
92
- newPartialBatchIndex === -1 ? [] : stashedPullBatch.slice(newPartialBatchIndex)
93
- const combinedBatch = [...msg.batch, ...batchWithNewStashedEvents]
94
- return Option.some({ ...msg, batch: combinedBatch, remaining: 0 })
95
- } else {
96
- return Option.some(msg)
97
- }
98
- } else {
99
- // Ignore
100
- return Option.none()
101
- }
102
- } else {
103
- if (pullResponseReceived) {
104
- return Option.some(msg)
105
- } else {
106
- stashedPullBatch.push(...msg.batch)
107
- return Option.none()
108
- }
109
- }
110
- }
111
-
112
- return Option.none()
113
- }),
114
- )
115
- }).pipe(Stream.unwrap),
116
-
117
- push: (batch) =>
118
- Effect.gen(function* () {
119
- const pushAck = yield* Deferred.make<void, InvalidPushError>()
120
- const requestId = nanoid()
121
-
122
- yield* Stream.fromPubSub(incomingMessages).pipe(
123
- Stream.tap((_) =>
124
- _._tag === 'WSMessage.Error' && _.requestId === requestId
125
- ? Deferred.fail(pushAck, new InvalidPushError({ reason: { _tag: 'Unexpected', message: _.message } }))
126
- : Effect.void,
127
- ),
128
- Stream.filter((_) => _._tag === 'WSMessage.PushAck' && _.requestId === requestId),
129
- Stream.take(1),
130
- Stream.tap(() => Deferred.succeed(pushAck, void 0)),
131
- Stream.runDrain,
132
- Effect.tapCauseLogPretty,
133
- Effect.fork,
134
- )
135
-
136
- yield* send(WSMessage.PushReq.make({ batch, requestId }))
137
-
138
- yield* pushAck
139
- }),
140
- metadata: {
141
- name: '@livestore/cf-sync',
142
- description: 'LiveStore sync backend implementation using Cloudflare Workers & Durable Objects',
143
- protocol: 'ws',
144
- url: options.url,
145
- },
146
- } satisfies SyncBackend<SyncMetadata>
147
-
148
- return api
149
- })
150
-
151
- const connect = (wsUrl: string) =>
152
- Effect.gen(function* () {
153
- const isConnected = yield* SubscriptionRef.make(false)
154
- const socketRef: { current: globalThis.WebSocket | undefined } = { current: undefined }
155
-
156
- const incomingMessages = yield* PubSub.unbounded<Exclude<WSMessage.BackendToClientMessage, WSMessage.Pong>>().pipe(
157
- Effect.acquireRelease(PubSub.shutdown),
158
- )
159
-
160
- const waitUntilOnline = isConnected.changes.pipe(Stream.filter(Boolean), Stream.take(1), Stream.runDrain)
161
-
162
- const send = (message: WSMessage.Message) =>
163
- Effect.gen(function* () {
164
- // Wait first until we're online
165
- yield* waitUntilOnline
166
-
167
- // TODO use MsgPack instead of JSON to speed up the serialization / reduce the size of the messages
168
- socketRef.current!.send(Schema.encodeSync(Schema.parseJson(WSMessage.Message))(message))
169
-
170
- if (LS_DEV) {
171
- yield* Effect.spanEvent(
172
- `Sent message: ${message._tag}`,
173
- message._tag === 'WSMessage.PushReq'
174
- ? {
175
- seqNum: message.batch[0]!.seqNum,
176
- parentSeqNum: message.batch[0]!.parentSeqNum,
177
- batchLength: message.batch.length,
178
- }
179
- : message._tag === 'WSMessage.PullReq'
180
- ? { cursor: message.cursor ?? '-' }
181
- : {},
182
- )
183
- }
184
- })
185
-
186
- const innerConnect = Effect.gen(function* () {
187
- // If the browser already tells us we're offline, then we'll at least wait until the browser
188
- // thinks we're online again. (We'll only know for sure once the WS conneciton is established.)
189
- while (typeof navigator !== 'undefined' && navigator.onLine === false) {
190
- yield* Effect.sleep(1000)
191
- }
192
- // TODO bring this back in a cross-platform way
193
- // if (navigator.onLine === false) {
194
- // yield* Effect.async((cb) => self.addEventListener('online', () => cb(Effect.void)))
195
- // }
196
-
197
- const socket = yield* WebSocket.makeWebSocket({ url: wsUrl, reconnect: Schedule.exponential(100) })
198
- // socket.binaryType = 'arraybuffer'
199
-
200
- yield* SubscriptionRef.set(isConnected, true)
201
- socketRef.current = socket
202
-
203
- const connectionClosed = yield* Deferred.make<void>()
204
-
205
- const pongMessages = yield* Queue.unbounded<WSMessage.Pong>().pipe(Effect.acquireRelease(Queue.shutdown))
206
-
207
- yield* Effect.eventListener(socket, 'message', (event: MessageEvent) =>
208
- Effect.gen(function* () {
209
- const decodedEventRes = Schema.decodeUnknownEither(Schema.parseJson(WSMessage.BackendToClientMessage))(
210
- event.data,
211
- )
212
-
213
- if (decodedEventRes._tag === 'Left') {
214
- console.error('Sync: Invalid message received', decodedEventRes.left)
215
- return
216
- } else {
217
- if (decodedEventRes.right._tag === 'WSMessage.Pong') {
218
- yield* Queue.offer(pongMessages, decodedEventRes.right)
219
- } else {
220
- // yield* Effect.logDebug(`decodedEventRes: ${decodedEventRes.right._tag}`)
221
- yield* PubSub.publish(incomingMessages, decodedEventRes.right)
222
- }
223
- }
224
- }),
225
- )
226
-
227
- yield* Effect.eventListener(socket, 'close', () => Deferred.succeed(connectionClosed, void 0))
228
-
229
- yield* Effect.eventListener(socket, 'error', () =>
230
- Effect.gen(function* () {
231
- socket.close(3000, 'Sync: WebSocket error')
232
- yield* Deferred.succeed(connectionClosed, void 0)
233
- }),
234
- )
235
-
236
- // NOTE it seems that this callback doesn't work reliably on a worker but only via `window.addEventListener`
237
- // We might need to proxy the event from the main thread to the worker if we want this to work reliably.
238
-
239
- if (typeof self !== 'undefined' && typeof self.addEventListener === 'function') {
240
- // TODO support an Expo equivalent for this
241
-
242
- yield* Effect.eventListener(self, 'offline', () => Deferred.succeed(connectionClosed, void 0))
243
- }
244
-
245
- yield* Effect.addFinalizer(() =>
246
- Effect.gen(function* () {
247
- socketRef.current = undefined
248
- yield* SubscriptionRef.set(isConnected, false)
249
- }),
250
- )
251
-
252
- const checkPingPong = Effect.gen(function* () {
253
- // TODO include pong latency infomation in network status
254
- yield* send({ _tag: 'WSMessage.Ping', requestId: 'ping' })
255
-
256
- // NOTE those numbers might need more fine-tuning to allow for bad network conditions
257
- yield* Queue.take(pongMessages).pipe(Effect.timeout(5000))
258
-
259
- yield* Effect.sleep(25_000)
260
- }).pipe(Effect.withSpan('@livestore/sync-cf:connect:checkPingPong'), Effect.ignore)
261
-
262
- yield* waitUntilOnline.pipe(
263
- Effect.andThen(checkPingPong.pipe(Effect.forever)),
264
- Effect.tapErrorCause(() => Deferred.succeed(connectionClosed, void 0)),
265
- Effect.forkScoped,
266
- )
267
-
268
- yield* connectionClosed
269
- }).pipe(Effect.scoped, Effect.withSpan('@livestore/sync-cf:connect'))
270
-
271
- yield* innerConnect.pipe(Effect.forever, Effect.interruptible, Effect.tapCauseLogPretty, Effect.forkScoped)
272
-
273
- return { isConnected, incomingMessages, send }
274
- })