liminal 0.17.15 → 0.17.16

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 (81) hide show
  1. package/Actor.ts +22 -33
  2. package/{workerd/ActorHandle.ts → ActorHandle.ts} +7 -2
  3. package/{workerd/WorkerdActorNamespace.ts → ActorNamespace.ts} +46 -32
  4. package/{workerd/WorkerdActorRuntime.ts → ActorRuntime.ts} +71 -44
  5. package/ActorTransport.ts +2 -2
  6. package/Audition.ts +1 -1
  7. package/BrowserActorNamespace.ts +257 -0
  8. package/CHANGELOG.md +9 -0
  9. package/Client.ts +127 -76
  10. package/ClientDirectory.ts +40 -32
  11. package/ClientHandle.ts +9 -7
  12. package/Fn.ts +39 -0
  13. package/Tracing.ts +11 -3
  14. package/dist/Actor.d.ts +3 -5
  15. package/dist/Actor.js +5 -9
  16. package/dist/Actor.js.map +1 -1
  17. package/dist/{workerd/ActorHandle.d.ts → ActorHandle.d.ts} +6 -3
  18. package/dist/ActorHandle.js.map +1 -0
  19. package/dist/{workerd/WorkerdActorNamespace.d.ts → ActorNamespace.d.ts} +12 -10
  20. package/dist/{workerd/WorkerdActorNamespace.js → ActorNamespace.js} +16 -10
  21. package/dist/ActorNamespace.js.map +1 -0
  22. package/dist/{workerd/WorkerdActorRuntime.d.ts → ActorRuntime.d.ts} +10 -9
  23. package/dist/{workerd/WorkerdActorRuntime.js → ActorRuntime.js} +40 -34
  24. package/dist/ActorRuntime.js.map +1 -0
  25. package/dist/ActorTransport.d.ts +2 -2
  26. package/dist/Audition.js +1 -1
  27. package/dist/Audition.js.map +1 -1
  28. package/dist/BrowserActorNamespace.d.ts +39 -0
  29. package/dist/BrowserActorNamespace.js +134 -0
  30. package/dist/BrowserActorNamespace.js.map +1 -0
  31. package/dist/Client.d.ts +7 -4
  32. package/dist/Client.js +78 -48
  33. package/dist/Client.js.map +1 -1
  34. package/dist/ClientDirectory.d.ts +1 -1
  35. package/dist/ClientDirectory.js +11 -5
  36. package/dist/ClientDirectory.js.map +1 -1
  37. package/dist/ClientHandle.d.ts +5 -4
  38. package/dist/Fn.d.ts +8 -0
  39. package/dist/Tracing.js +6 -2
  40. package/dist/Tracing.js.map +1 -1
  41. package/dist/experimental/L/append.js +1 -1
  42. package/dist/experimental/L/append.js.map +1 -1
  43. package/dist/experimental/L/history.js +1 -1
  44. package/dist/experimental/L/history.js.map +1 -1
  45. package/dist/index.common.d.ts +12 -0
  46. package/dist/index.common.js +13 -0
  47. package/dist/index.common.js.map +1 -0
  48. package/dist/index.d.ts +4 -11
  49. package/dist/index.js +4 -11
  50. package/dist/index.js.map +1 -1
  51. package/dist/index.non-workerd.d.ts +1 -0
  52. package/dist/index.non-workerd.js +2 -0
  53. package/dist/index.non-workerd.js.map +1 -0
  54. package/dist/package.json +13 -7
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/experimental/L/append.ts +1 -1
  57. package/experimental/L/history.ts +1 -1
  58. package/index.common.ts +12 -0
  59. package/index.non-workerd.ts +1 -0
  60. package/index.ts +4 -11
  61. package/package.json +12 -9
  62. package/_util/schema.ts +0 -7
  63. package/browser/BrowserActorNamespace.ts +0 -248
  64. package/browser/index.ts +0 -1
  65. package/dist/_util/schema.d.ts +0 -4
  66. package/dist/_util/schema.js +0 -5
  67. package/dist/_util/schema.js.map +0 -1
  68. package/dist/browser/BrowserActorNamespace.d.ts +0 -16
  69. package/dist/browser/BrowserActorNamespace.js +0 -133
  70. package/dist/browser/BrowserActorNamespace.js.map +0 -1
  71. package/dist/browser/index.d.ts +0 -1
  72. package/dist/browser/index.js +0 -2
  73. package/dist/browser/index.js.map +0 -1
  74. package/dist/workerd/ActorHandle.js.map +0 -1
  75. package/dist/workerd/WorkerdActorNamespace.js.map +0 -1
  76. package/dist/workerd/WorkerdActorRuntime.js.map +0 -1
  77. package/dist/workerd/index.d.ts +0 -3
  78. package/dist/workerd/index.js +0 -4
  79. package/dist/workerd/index.js.map +0 -1
  80. package/workerd/index.ts +0 -3
  81. /package/dist/{workerd/ActorHandle.js → ActorHandle.js} +0 -0
@@ -0,0 +1,257 @@
1
+ import { BrowserWorkerRunner } from "@effect/platform-browser"
2
+ import { Effect, Exit, Layer, Option, Ref, Schema as S, Scope, Semaphore, Stream, Tracer } from "effect"
3
+ import { WorkerRunner } from "effect/unstable/workers"
4
+ import * as Boundary from "liminal-util/Boundary"
5
+ import { encodeJsonString, decodeJsonString, type TopFromString } from "liminal-util/schema"
6
+
7
+ import type { Actor } from "./Actor.ts"
8
+ import type { ActorTransport } from "./ActorTransport.ts"
9
+ import * as ClientDirectory from "./ClientDirectory.ts"
10
+ import type { ClientHandle } from "./ClientHandle.ts"
11
+ import * as Method from "./Method.ts"
12
+ import type { ProtocolDefinition } from "./Protocol.ts"
13
+ import * as Tracing from "./Tracing.ts"
14
+
15
+ export interface Introduction<Name extends TopFromString, AttachmentFields extends S.Struct.Fields> {
16
+ readonly port: MessagePort
17
+ readonly name: Name["Type"]
18
+ readonly attachments: S.Struct<AttachmentFields>["Type"]
19
+ }
20
+
21
+ export const make = Effect.fnUntraced(
22
+ function* <
23
+ ActorSelf,
24
+ ActorId extends string,
25
+ Name extends TopFromString,
26
+ AttachmentFields extends S.Struct.Fields,
27
+ ClientSelf,
28
+ ClientId extends string,
29
+ D extends ProtocolDefinition,
30
+ const Handlers extends Method.Handlers<D["external"], any>,
31
+ E,
32
+ R,
33
+ IntroductionE,
34
+ IntroductionR,
35
+ >({
36
+ actor,
37
+ handlers,
38
+ hydrate,
39
+ introductions,
40
+ }: {
41
+ readonly actor: Actor<ActorSelf, ActorId, Name, AttachmentFields, ClientSelf, ClientId, D>
42
+ readonly handlers: Handlers
43
+ readonly hydrate: Effect.Effect<S.Struct<D["state"]>["Type"], E, R>
44
+ readonly introductions: Stream.Stream<Introduction<Name, AttachmentFields>, IntroductionE, IntroductionR>
45
+ }) {
46
+ const {
47
+ definition: {
48
+ client: { protocol: P, key: expected },
49
+ name: Name,
50
+ },
51
+ } = actor
52
+ const { Client: ClientM } = P
53
+ const decodeClient = decodeJsonString(ClientM)
54
+ const encodeAuditionSuccess = encodeJsonString(P.Audition.Success)
55
+ const encodeAuditionFailure = encodeJsonString(P.Audition.Failure)
56
+ const encodeFSuccess = encodeJsonString(P.F.Success)
57
+ const encodeFFailure = encodeJsonString(P.F.Failure)
58
+ const encodeEvent = encodeJsonString(P.Event)
59
+
60
+ interface BrowserClient {
61
+ readonly port: MessagePort
62
+
63
+ readonly backing: WorkerRunner.WorkerRunner<string, string>
64
+
65
+ readonly close: Effect.Effect<void>
66
+ }
67
+
68
+ interface Entry {
69
+ readonly directory: ClientDirectory.ClientDirectory<MessagePort, BrowserClient, ActorSelf, AttachmentFields, D>
70
+ readonly mutex: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
71
+ }
72
+
73
+ const entries: Record<string, Entry> = {}
74
+
75
+ const transport: ActorTransport<MessagePort, BrowserClient, AttachmentFields, D> = {
76
+ key: ({ port }) => port,
77
+ send: ({ backing }, event) => {
78
+ const { _tag } = event.event as never
79
+ return Effect.gen(function* () {
80
+ const trace = yield* Tracing.currentTrace
81
+ yield* backing.send(
82
+ 0,
83
+ yield* encodeEvent({ ...event, ...(trace && { trace }) }).pipe(
84
+ Effect.catchTags({
85
+ SchemaError: Effect.die,
86
+ }),
87
+ ),
88
+ )
89
+ }).pipe(Boundary.span("send", import.meta.url, { attributes: { _tag }, kind: "producer" }))
90
+ },
91
+ close: ({ close }) => close,
92
+ snapshot: () => Effect.void,
93
+ }
94
+
95
+ const useEntries = yield* Semaphore.make(1).pipe(Effect.map((v) => v.withPermits(1)))
96
+
97
+ const getEntry = Effect.fnUntraced(function* (key: string) {
98
+ const existing = entries[key]
99
+ if (existing) return existing
100
+ const directory = ClientDirectory.make(actor, { transport })
101
+ const semaphore = yield* Semaphore.make(1)
102
+ const fresh = {
103
+ directory,
104
+ mutex: semaphore.withPermits(1),
105
+ }
106
+ entries[key] = fresh
107
+ return fresh
108
+ }, useEntries)
109
+
110
+ const outerScope = yield* Scope.Scope
111
+
112
+ yield* introductions.pipe(
113
+ Stream.runForEach(
114
+ Effect.fnUntraced(function* ({ name, port, attachments }) {
115
+ const stateRef = yield* Ref.make<
116
+ Option.Option<{
117
+ readonly key: string
118
+ readonly entry: Entry
119
+ readonly currentClient: ClientHandle<ActorSelf, AttachmentFields, D>
120
+ readonly ActorLive: Layer.Layer<ActorSelf>
121
+ }>
122
+ >(Option.none())
123
+
124
+ const scope = yield* Scope.fork(outerScope, "sequential")
125
+ const closeScope = Scope.close(scope, Exit.void)
126
+
127
+ const backing = yield* BrowserWorkerRunner.make(port).start<string, string>()
128
+
129
+ yield* Scope.addFinalizer(
130
+ scope,
131
+ Effect.gen(function* () {
132
+ const state = yield* Ref.get(stateRef)
133
+ if (state._tag === "Some") {
134
+ const {
135
+ key,
136
+ entry: { directory },
137
+ } = state.value
138
+ yield* directory.unregister(port)
139
+ if (directory.handles.size === 0) {
140
+ delete entries[key]
141
+ }
142
+ }
143
+ }).pipe(useEntries),
144
+ )
145
+
146
+ yield* backing
147
+ .run(
148
+ Effect.fnUntraced(
149
+ function* (_portId, raw) {
150
+ const state = yield* Ref.get(stateRef)
151
+ const message = yield* decodeClient(raw)
152
+ if (state._tag === "None") {
153
+ if (message._tag !== "Audition.Payload") {
154
+ return yield* Effect.die(undefined)
155
+ }
156
+ const { client: actual } = message
157
+ if (actual !== expected) {
158
+ yield* backing.send(
159
+ 0,
160
+ yield* encodeAuditionFailure({
161
+ _tag: "Audition.Failure",
162
+ expected,
163
+ actual,
164
+ }),
165
+ )
166
+ return yield* closeScope
167
+ }
168
+ const key = yield* S.encodeEffect(Name)(name)
169
+ const entry = yield* getEntry(key)
170
+ const currentClient = yield* entry.directory.register(
171
+ { port, backing, close: closeScope },
172
+ attachments,
173
+ )
174
+ const ActorLive = Layer.succeed(actor, {
175
+ name,
176
+ clients: entry.directory.handles,
177
+ currentClient,
178
+ })
179
+ yield* Ref.set(stateRef, Option.some({ key, entry, currentClient, ActorLive }))
180
+ const initial = yield* hydrate.pipe(
181
+ entry.mutex,
182
+ Effect.scoped,
183
+ Boundary.span("onConnect", import.meta.url),
184
+ Effect.provide(ActorLive),
185
+ )
186
+ return yield* backing.send(0, yield* encodeAuditionSuccess({ _tag: "Audition.Success", initial }))
187
+ }
188
+ const { entry, ActorLive } = state.value
189
+ if (message._tag === "Audition.Payload") {
190
+ return yield* Effect.die(undefined)
191
+ }
192
+ if (message._tag === "Disconnect") {
193
+ return yield* closeScope
194
+ }
195
+ const { id, payload } = message
196
+ const { _tag, value } = payload as never
197
+ const parent = message.trace && Tracer.externalSpan(message.trace)
198
+ const transportSpan = yield* Tracing.parent
199
+ yield* (
200
+ handlers as Method.Handlers<
201
+ D["external"],
202
+ Handlers[keyof Handlers] extends (v: never) => Effect.Effect<any, any, infer R> ? R : never
203
+ >
204
+ )[_tag]!(value).pipe(
205
+ Effect.matchEffect({
206
+ onSuccess: (value) =>
207
+ encodeFSuccess({
208
+ _tag: "F.Success",
209
+ id,
210
+ success: { _tag, value } as never,
211
+ }),
212
+ onFailure: (value) =>
213
+ encodeFFailure({
214
+ _tag: "F.Failure",
215
+ id,
216
+ failure: { _tag, value } as never,
217
+ }),
218
+ }),
219
+ Effect.andThen((v) => backing.send(0, v)),
220
+ Boundary.span("handle", import.meta.url, {
221
+ attributes: { _tag },
222
+ kind: "server",
223
+ parent,
224
+ links:
225
+ parent && transportSpan
226
+ ? [
227
+ {
228
+ span: transportSpan,
229
+ attributes: {
230
+ "liminal.link": "transport",
231
+ "liminal.transport": "worker",
232
+ },
233
+ },
234
+ ]
235
+ : undefined,
236
+ }),
237
+ Effect.scoped,
238
+ Effect.provide(ActorLive),
239
+ entry.mutex,
240
+ )
241
+ },
242
+ Boundary.span("message", import.meta.url),
243
+ ),
244
+ )
245
+ .pipe(
246
+ Effect.andThen(closeScope),
247
+ Effect.catchCause((cause) => Effect.logError(cause).pipe(Effect.andThen(closeScope))),
248
+ Boundary.span("backing", import.meta.url),
249
+ Effect.forkScoped,
250
+ Scope.provide(scope),
251
+ )
252
+ }),
253
+ ),
254
+ )
255
+ },
256
+ Boundary.span("make", import.meta.url),
257
+ )
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # liminal
2
2
 
3
+ ## 0.17.16
4
+
5
+ ### Patch Changes
6
+
7
+ - b08b6d9: Begin trusted publishing configuration.
8
+ - Updated dependencies [b08b6d9]
9
+ - effect-workerd@0.0.6
10
+ - liminal-util@0.0.10
11
+
3
12
  ## 0.17.15
4
13
 
5
14
  ### Patch Changes
package/Client.ts CHANGED
@@ -26,17 +26,15 @@ import {
26
26
  } from "effect"
27
27
  import { Socket } from "effect/unstable/socket"
28
28
  import { Worker } from "effect/unstable/workers"
29
- import * as Spanner from "liminal-util/Spanner"
29
+ import * as Boundary from "liminal-util/Boundary"
30
+ import { decodeJsonString, encodeJsonString } from "liminal-util/schema"
30
31
 
31
- import { decodeJsonString, encodeJsonString } from "./_util/schema.ts"
32
32
  import { type ClientError, AuditionError, ConnectionError, UnresolvedError } from "./errors.ts"
33
- import type { Fn, FnError } from "./Fn.ts"
33
+ import type { Fn, FnError, FnNoSelf } from "./Fn.ts"
34
34
  import { Protocol, type ProtocolDefinition } from "./Protocol.ts"
35
35
  import * as Reducer from "./Reducer.ts"
36
36
  import * as Tracing from "./Tracing.ts"
37
37
 
38
- const span = Spanner.make(import.meta.url)
39
-
40
38
  export const TypeId = "~liminal/Client" as const
41
39
 
42
40
  export interface ReplayConfig {
@@ -67,11 +65,13 @@ export type Service<ClientSelf, D extends ProtocolDefinition> = RcRef.RcRef<
67
65
  ClientError
68
66
  >
69
67
 
70
- export interface Client<Self, ClientId extends string, D extends ProtocolDefinition> extends Context.Service<
68
+ export interface Client<Self, Id extends string, D extends ProtocolDefinition> extends Context.Service<
71
69
  Self,
72
70
  Service<Self, D>
73
71
  > {
74
- new (_: never): Context.ServiceClass.Shape<ClientId, Service<Self, D>>
72
+ new (_: never): Context.ServiceClass.Shape<Id, Service<Self, D>> & {
73
+ readonly State: S.Struct<D["state"]>["Type"]
74
+ }
75
75
 
76
76
  readonly [TypeId]: typeof TypeId
77
77
 
@@ -98,6 +98,17 @@ export interface Client<Self, ClientId extends string, D extends ProtocolDefinit
98
98
  readonly reducer: <K extends keyof D["events"], R extends Reducer.Reducer<D, K>>(_tag: K, f: R) => R
99
99
  }
100
100
 
101
+ export const fn = <ClientSelf, D extends ProtocolDefinition>(service: Service<ClientSelf, D>) =>
102
+ ((_tag: keyof D["external"], ...f: Array<any>) =>
103
+ Effect.fnUntraced(
104
+ function* (payload: any) {
105
+ const { fnRaw: fn } = yield* RcRef.get(service)
106
+ return yield* fn(_tag, payload)
107
+ },
108
+ Effect.scoped,
109
+ ...(f as [any]),
110
+ )) as FnNoSelf<D["external"]>
111
+
101
112
  export const Service =
102
113
  <Self>() =>
103
114
  <Id extends string, D extends ProtocolDefinition>(id: Id, definition: D): Client<Self, Id, D> => {
@@ -105,13 +116,13 @@ export const Service =
105
116
 
106
117
  const protocol = Protocol(definition)
107
118
 
108
- const state = tag.asEffect().pipe(
119
+ const state = tag.pipe(
109
120
  Effect.flatMap(RcRef.get),
110
121
  Effect.map(({ state }) => state),
111
122
  Stream.unwrap,
112
123
  )
113
124
 
114
- const events = tag.asEffect().pipe(
125
+ const events = tag.pipe(
115
126
  Effect.flatMap(RcRef.get),
116
127
  Effect.map(({ events }) => events),
117
128
  Stream.unwrap,
@@ -120,14 +131,14 @@ export const Service =
120
131
  const fn = ((_tag: keyof D["external"], ...f: Array<any>) =>
121
132
  Effect.fnUntraced(
122
133
  function* (payload: any) {
123
- const { fnRaw: fn } = yield* tag.asEffect().pipe(Effect.flatMap(RcRef.get))
134
+ const { fnRaw: fn } = yield* tag.pipe(Effect.flatMap(RcRef.get))
124
135
  return yield* fn(_tag, payload)
125
136
  },
126
137
  Effect.scoped,
127
138
  ...(f as [any]),
128
139
  )) as Fn<Self, D["external"]>
129
140
 
130
- const invalidate = tag.asEffect().pipe(
141
+ const invalidate = tag.pipe(
131
142
  Effect.flatMap((rc) =>
132
143
  RcRef.get(rc).pipe(
133
144
  Effect.flatMap(({ end }) => end),
@@ -140,7 +151,7 @@ export const Service =
140
151
 
141
152
  const reducer = <K extends keyof D["events"], R extends Reducer.Reducer<D, K>>(_event: K, f: R) => f
142
153
 
143
- return Object.assign(tag, {
154
+ return Object.assign(tag satisfies Context.ServiceClass.Shape<Id, Service<Self, D>> as never, {
144
155
  [TypeId]: TypeId,
145
156
  definition,
146
157
  protocol,
@@ -263,6 +274,8 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers ex
263
274
  const current = yield* Ref.get(state)
264
275
  const reduced = yield* reducer(event as never)(current).pipe(
265
276
  Effect.provideService(client, rcr),
277
+ // TODO: rework error-handling
278
+ Effect.catchDefect(() => Effect.succeed(undefined)),
266
279
  ) as Effect.Effect<
267
280
  S.Struct<D["state"]>["Type"] | undefined,
268
281
  never,
@@ -275,7 +288,7 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers ex
275
288
  }).pipe(reduceTask)
276
289
  const parent = message.trace ? Tracer.externalSpan(message.trace) : undefined
277
290
  yield* publishTake([event], true).pipe(
278
- span("enqueue-event", {
291
+ Boundary.span("enqueue-event", import.meta.url, {
279
292
  attributes: { _tag },
280
293
  kind: "consumer",
281
294
  parent,
@@ -431,12 +444,12 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers ex
431
444
  Effect.flatMap(
432
445
  (exit): Effect.Effect<never, ClientError | UnresolvedError | S.SchemaError> =>
433
446
  Exit.match(exit, {
434
- onSuccess: () => new UnresolvedError().asEffect(),
447
+ onSuccess: () => new UnresolvedError(),
435
448
  onFailure: flow(
436
449
  Cause.findError,
437
450
  Result.match({
438
451
  onSuccess: Effect.fail,
439
- onFailure: () => new UnresolvedError().asEffect(),
452
+ onFailure: () => new UnresolvedError(),
440
453
  }),
441
454
  ),
442
455
  }),
@@ -444,7 +457,7 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers ex
444
457
  ),
445
458
  )
446
459
  }).pipe(
447
- span("fn", {
460
+ Boundary.span("fn", import.meta.url, {
448
461
  kind: "client",
449
462
  attributes: { _tag },
450
463
  }),
@@ -452,12 +465,25 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers ex
452
465
  )
453
466
 
454
467
  return { state, events, fnRaw, end }
455
- }).pipe(span("acquire", { attributes: { client: client.key } }), Effect.annotateLogs("client", client.key)),
468
+ }).pipe(
469
+ Boundary.span("acquire", import.meta.url, {
470
+ attributes: { client: client.key },
471
+ }),
472
+ Effect.annotateLogs("client", client.key),
473
+ ),
456
474
  })
457
475
 
458
476
  return rcr
459
477
  }).pipe(Layer.effect(client))
460
478
 
479
+ let clientId_: string | undefined
480
+ const clientId = () => {
481
+ if (!clientId_) {
482
+ clientId_ = crypto.randomUUID()
483
+ }
484
+ return clientId_
485
+ }
486
+
461
487
  export const layerSocket = <
462
488
  Self,
463
489
  Id extends string,
@@ -498,41 +524,51 @@ export const layerSocket = <
498
524
  replay,
499
525
  build: Effect.gen(function* () {
500
526
  const socket = yield* Socket.makeWebSocket(url ?? "/", {
501
- protocols: ["liminal", Encoding.encodeBase64Url(client.key), ...(protocols ? Array.ensure(protocols) : [])],
527
+ protocols: [
528
+ "liminal",
529
+ clientId(),
530
+ Encoding.encodeBase64Url(client.key),
531
+ ...(protocols ? Array.ensure(protocols) : []),
532
+ ],
502
533
  })
503
534
  return {
504
- listen: Effect.fnUntraced(function* (publish) {
505
- yield* socket
506
- .runRaw((raw) =>
507
- pipe(
508
- raw instanceof Uint8Array ? new TextDecoder().decode(raw) : raw,
509
- decodeActor,
510
- Effect.andThen(publish),
511
- ),
512
- )
513
- .pipe(
514
- Effect.catchIf(
515
- Socket.isSocketError,
516
- Effect.fnUntraced(function* (cause) {
517
- const { reason } = cause
518
- if (reason._tag === "SocketCloseError" && reason.code === 1000) {
519
- return yield* publish({ _tag: "Disconnect" })
520
- }
521
- yield* Effect.annotateLogs(Effect.logDebug(`SocketErrored.${reason._tag}`), { cause })
522
- return yield* new ConnectionError({ cause })
523
- }),
524
- ),
525
- )
526
- }, span("listen")),
535
+ listen: Effect.fnUntraced(
536
+ function* (publish) {
537
+ yield* socket
538
+ .runRaw((raw) =>
539
+ pipe(
540
+ raw instanceof Uint8Array ? new TextDecoder().decode(raw) : raw,
541
+ decodeActor,
542
+ Effect.andThen(publish),
543
+ ),
544
+ )
545
+ .pipe(
546
+ Effect.catchIf(
547
+ Socket.isSocketError,
548
+ Effect.fnUntraced(function* (cause) {
549
+ const { reason } = cause
550
+ if (reason._tag === "SocketCloseError" && reason.code === 1000) {
551
+ return yield* publish({ _tag: "Disconnect" })
552
+ }
553
+ yield* Effect.annotateLogs(Effect.logDebug(`SocketErrored.${reason._tag}`), { cause })
554
+ return yield* new ConnectionError({ cause })
555
+ }),
556
+ ),
557
+ )
558
+ },
559
+ Boundary.span("listen", import.meta.url),
560
+ ),
527
561
  send: Effect.fnUntraced(
528
562
  function* (v) {
529
563
  const write = yield* socket.writer
530
564
  const message = yield* encodeFPayload(v)
531
565
  yield* write(message).pipe(
532
- Effect.catchTag("SocketError", (cause) => new ConnectionError({ cause }).asEffect()),
566
+ Effect.catchTags({
567
+ SocketError: (cause) => new ConnectionError({ cause }),
568
+ }),
533
569
  )
534
570
  },
535
- span("send"),
571
+ Boundary.span("send", import.meta.url),
536
572
  Effect.scoped,
537
573
  ),
538
574
  }
@@ -563,52 +599,67 @@ export const layerWorker = <
563
599
  | Worker.WorkerPlatform
564
600
  | Worker.Spawner
565
601
  | T["Actor"]["DecodingServices"]
566
- | T["F"]["Payload"]["EncodingServices"]
602
+ | T["Client"]["EncodingServices"]
567
603
  | Reducer.Reducers.Services<Self, Reducers>
568
- > =>
569
- make<Self, Id, D, Reducers, Worker.WorkerPlatform | Worker.Spawner, CR>({
604
+ > => {
605
+ const { Actor, Client: ClientM } = client.protocol
606
+ const encodeClient = encodeJsonString(ClientM)
607
+ const decodeActor = decodeJsonString(Actor)
608
+
609
+ return make<Self, Id, D, Reducers, Worker.WorkerPlatform | Worker.Spawner, CR>({
570
610
  client,
571
611
  reducers,
572
612
  onConnect,
573
613
  replay,
574
614
  build: Effect.gen(function* () {
575
615
  const platform = yield* Worker.WorkerPlatform
576
- const backing = yield* platform
577
- .spawn<T["Actor"]["Type"], T["Client"]["Type"]>(0)
578
- .pipe(Effect.catchTag("WorkerError", (cause) => new ConnectionError({ cause }).asEffect()))
616
+ const backing = yield* platform.spawn<string, string>(0).pipe(
617
+ Effect.catchTags({
618
+ WorkerError: (cause) => new ConnectionError({ cause }),
619
+ }),
620
+ )
579
621
 
580
622
  const send = (message: T["Client"]["Type"]) =>
581
- backing.send(message).pipe(
582
- Effect.catchTag("WorkerError", (cause) => new ConnectionError({ cause }).asEffect()),
583
- span("send"),
623
+ encodeClient(message).pipe(
624
+ Effect.flatMap((encoded) => backing.send(encoded)),
625
+ Effect.catchTags({
626
+ WorkerError: (cause) => new ConnectionError({ cause }),
627
+ }),
628
+ Boundary.span("send", import.meta.url),
584
629
  )
585
630
 
586
631
  return {
587
- listen: Effect.fnUntraced(function* (publish) {
588
- const stop = yield* Deferred.make<void>()
589
- yield* backing
590
- .run(
591
- Effect.fnUntraced(function* (message) {
592
- yield* publish(message)
593
- if (message._tag === "Disconnect" || message._tag === "Audition.Failure") {
594
- yield* Deferred.succeed(stop, void 0)
595
- }
596
- }),
597
- {
598
- onSpawn: backing
599
- .send({
600
- _tag: "Audition.Payload",
601
- client: client.key,
602
- })
603
- .pipe(Effect.orDie),
604
- },
605
- )
606
- .pipe(
607
- Effect.raceFirst(Deferred.await(stop)),
608
- Effect.catchTag("WorkerError", (cause) => new ConnectionError({ cause }).asEffect()),
609
- )
610
- }, span("listen")),
632
+ listen: Effect.fnUntraced(
633
+ function* (publish) {
634
+ const stop = yield* Deferred.make<void>()
635
+ const audition = yield* encodeClient({
636
+ _tag: "Audition.Payload",
637
+ client: client.key,
638
+ })
639
+ yield* backing
640
+ .run(
641
+ Effect.fnUntraced(function* (raw) {
642
+ const message = yield* decodeActor(raw)
643
+ yield* publish(message)
644
+ if (message._tag === "Disconnect" || message._tag === "Audition.Failure") {
645
+ yield* Deferred.succeed(stop, void 0)
646
+ }
647
+ }),
648
+ {
649
+ onSpawn: backing.send(audition).pipe(Effect.orDie),
650
+ },
651
+ )
652
+ .pipe(
653
+ Effect.raceFirst(Deferred.await(stop)),
654
+ Effect.catchTags({
655
+ WorkerError: (cause) => new ConnectionError({ cause }),
656
+ }),
657
+ )
658
+ },
659
+ Boundary.span("listen", import.meta.url),
660
+ ),
611
661
  send,
612
662
  }
613
663
  }),
614
664
  })
665
+ }