liminal 0.17.14 → 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 (126) hide show
  1. package/Actor.ts +22 -34
  2. package/ActorHandle.ts +34 -0
  3. package/ActorNamespace.ts +188 -0
  4. package/ActorRuntime.ts +449 -0
  5. package/ActorTransport.ts +8 -6
  6. package/Audition.ts +87 -40
  7. package/BrowserActorNamespace.ts +257 -0
  8. package/CHANGELOG.md +17 -0
  9. package/Client.ts +374 -197
  10. package/ClientDirectory.ts +71 -49
  11. package/ClientHandle.ts +9 -7
  12. package/ClientHandleEncoders.ts +15 -0
  13. package/Fn.ts +94 -0
  14. package/Method.ts +11 -21
  15. package/Protocol.ts +44 -36
  16. package/Reducer.ts +22 -0
  17. package/Tracing.ts +45 -0
  18. package/dist/Actor.d.ts +3 -5
  19. package/dist/Actor.js +5 -9
  20. package/dist/Actor.js.map +1 -1
  21. package/dist/ActorHandle.d.ts +12 -0
  22. package/dist/ActorHandle.js +4 -0
  23. package/dist/ActorHandle.js.map +1 -0
  24. package/dist/ActorNamespace.d.ts +25 -0
  25. package/dist/ActorNamespace.js +60 -0
  26. package/dist/ActorNamespace.js.map +1 -0
  27. package/dist/ActorRuntime.d.ts +20 -0
  28. package/dist/ActorRuntime.js +210 -0
  29. package/dist/ActorRuntime.js.map +1 -0
  30. package/dist/ActorTransport.d.ts +5 -4
  31. package/dist/Audition.d.ts +16 -9
  32. package/dist/Audition.js +25 -9
  33. package/dist/Audition.js.map +1 -1
  34. package/dist/BrowserActorNamespace.d.ts +39 -0
  35. package/dist/BrowserActorNamespace.js +134 -0
  36. package/dist/BrowserActorNamespace.js.map +1 -0
  37. package/dist/Client.d.ts +26 -16
  38. package/dist/Client.js +186 -109
  39. package/dist/Client.js.map +1 -1
  40. package/dist/ClientDirectory.d.ts +15 -7
  41. package/dist/ClientDirectory.js +32 -23
  42. package/dist/ClientDirectory.js.map +1 -1
  43. package/dist/ClientHandle.d.ts +5 -4
  44. package/dist/ClientHandleEncoders.d.ts +7 -0
  45. package/dist/ClientHandleEncoders.js +2 -0
  46. package/dist/ClientHandleEncoders.js.map +1 -0
  47. package/dist/Fn.d.ts +24 -0
  48. package/dist/Fn.js +2 -0
  49. package/dist/Fn.js.map +1 -0
  50. package/dist/Method.d.ts +9 -14
  51. package/dist/Method.js +0 -1
  52. package/dist/Method.js.map +1 -1
  53. package/dist/Protocol.d.ts +19 -22
  54. package/dist/Protocol.js +20 -15
  55. package/dist/Protocol.js.map +1 -1
  56. package/dist/Reducer.d.ts +11 -0
  57. package/dist/Reducer.js +2 -0
  58. package/dist/Reducer.js.map +1 -0
  59. package/dist/Tracing.d.ts +37 -0
  60. package/dist/Tracing.js +33 -0
  61. package/dist/Tracing.js.map +1 -0
  62. package/dist/errors.d.ts +0 -4
  63. package/dist/errors.js.map +1 -1
  64. package/dist/experimental/L/append.js +1 -1
  65. package/dist/experimental/L/append.js.map +1 -1
  66. package/dist/experimental/L/history.js +1 -1
  67. package/dist/experimental/L/history.js.map +1 -1
  68. package/dist/experimental/TaggedTemplateFunction.js +1 -1
  69. package/dist/experimental/TaggedTemplateFunction.js.map +1 -1
  70. package/dist/index.common.d.ts +12 -0
  71. package/dist/index.common.js +13 -0
  72. package/dist/index.common.js.map +1 -0
  73. package/dist/index.d.ts +4 -11
  74. package/dist/index.js +4 -11
  75. package/dist/index.js.map +1 -1
  76. package/dist/index.non-workerd.d.ts +1 -0
  77. package/dist/index.non-workerd.js +2 -0
  78. package/dist/index.non-workerd.js.map +1 -0
  79. package/dist/package.json +20 -19
  80. package/dist/tsconfig.tsbuildinfo +1 -1
  81. package/errors.ts +0 -6
  82. package/experimental/L/append.ts +1 -1
  83. package/experimental/L/history.ts +1 -1
  84. package/experimental/TaggedTemplateFunction.ts +1 -1
  85. package/index.common.ts +12 -0
  86. package/index.non-workerd.ts +1 -0
  87. package/index.ts +4 -11
  88. package/package.json +11 -23
  89. package/tsconfig.json +1 -1
  90. package/vitest.config.ts +7 -0
  91. package/Accumulator.ts +0 -103
  92. package/F.ts +0 -10
  93. package/_diagnostic.ts +0 -3
  94. package/_util/Mutex.ts +0 -13
  95. package/_util/schema.ts +0 -7
  96. package/browser/BrowserActorNamespace.ts +0 -213
  97. package/browser/index.ts +0 -1
  98. package/dist/Accumulator.d.ts +0 -22
  99. package/dist/Accumulator.js +0 -37
  100. package/dist/Accumulator.js.map +0 -1
  101. package/dist/F.d.ts +0 -4
  102. package/dist/F.js +0 -2
  103. package/dist/F.js.map +0 -1
  104. package/dist/_diagnostic.d.ts +0 -4
  105. package/dist/_diagnostic.js +0 -3
  106. package/dist/_diagnostic.js.map +0 -1
  107. package/dist/_util/Mutex.d.ts +0 -7
  108. package/dist/_util/Mutex.js +0 -9
  109. package/dist/_util/Mutex.js.map +0 -1
  110. package/dist/_util/schema.d.ts +0 -4
  111. package/dist/_util/schema.js +0 -5
  112. package/dist/_util/schema.js.map +0 -1
  113. package/dist/browser/BrowserActorNamespace.d.ts +0 -16
  114. package/dist/browser/BrowserActorNamespace.js +0 -112
  115. package/dist/browser/BrowserActorNamespace.js.map +0 -1
  116. package/dist/browser/index.d.ts +0 -1
  117. package/dist/browser/index.js +0 -2
  118. package/dist/browser/index.js.map +0 -1
  119. package/dist/workerd/WorkerdActorNamespace.d.ts +0 -25
  120. package/dist/workerd/WorkerdActorNamespace.js +0 -146
  121. package/dist/workerd/WorkerdActorNamespace.js.map +0 -1
  122. package/dist/workerd/index.d.ts +0 -1
  123. package/dist/workerd/index.js +0 -2
  124. package/dist/workerd/index.js.map +0 -1
  125. package/workerd/WorkerdActorNamespace.ts +0 -362
  126. package/workerd/index.ts +0 -1
package/Client.ts CHANGED
@@ -15,23 +15,25 @@ import {
15
15
  Take,
16
16
  Schema as S,
17
17
  Array,
18
- Struct,
19
18
  Fiber,
20
19
  Exit,
21
20
  Cause,
22
21
  Result,
23
22
  flow,
23
+ Tracer,
24
+ identity,
25
+ Semaphore,
24
26
  } from "effect"
25
27
  import { Socket } from "effect/unstable/socket"
26
28
  import { Worker } from "effect/unstable/workers"
29
+ import * as Boundary from "liminal-util/Boundary"
30
+ import { decodeJsonString, encodeJsonString } from "liminal-util/schema"
27
31
 
28
- import { diagnostic } from "./_diagnostic.ts"
29
- import { decodeJsonString, encodeJsonString } from "./_util/schema.ts"
30
- import { type ClientError, AuditionError, ConnectionError, type FError, UnresolvedError } from "./errors.ts"
31
- import { type F } from "./F.ts"
32
+ import { type ClientError, AuditionError, ConnectionError, UnresolvedError } from "./errors.ts"
33
+ import type { Fn, FnError, FnNoSelf } from "./Fn.ts"
32
34
  import { Protocol, type ProtocolDefinition } from "./Protocol.ts"
33
-
34
- const { debug, span } = diagnostic("Client")
35
+ import * as Reducer from "./Reducer.ts"
36
+ import * as Tracing from "./Tracing.ts"
35
37
 
36
38
  export const TypeId = "~liminal/Client" as const
37
39
 
@@ -47,21 +49,29 @@ interface EventTake<A, E> {
47
49
  readonly take: Take.Take<A, E>
48
50
  }
49
51
 
50
- export interface Session<Self, D extends ProtocolDefinition> {
51
- readonly events: Stream.Stream<ReturnType<typeof S.TaggedUnion<D["events"]>>["Type"], ClientError | S.SchemaError>
52
+ export type Service<ClientSelf, D extends ProtocolDefinition> = RcRef.RcRef<
53
+ {
54
+ readonly state: Stream.Stream<S.Struct<D["state"]>["Type"], ClientError | S.SchemaError>
52
55
 
53
- readonly f: F<Self, D>
56
+ readonly events: Stream.Stream<ReturnType<typeof S.TaggedUnion<D["events"]>>["Type"], ClientError | S.SchemaError>
54
57
 
55
- readonly end: Effect.Effect<void>
56
- }
58
+ readonly fnRaw: <K extends keyof D["external"], M extends D["external"][K]>(
59
+ tag: K,
60
+ payload: M["payload"]["Type"],
61
+ ) => Effect.Effect<M["success"]["Type"], M["failure"]["Type"], ClientSelf>
57
62
 
58
- export type Service<ClientSelf, D extends ProtocolDefinition> = RcRef.RcRef<Session<ClientSelf, D>, ClientError>
63
+ readonly end: Effect.Effect<void>
64
+ },
65
+ ClientError
66
+ >
59
67
 
60
- 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<
61
69
  Self,
62
70
  Service<Self, D>
63
71
  > {
64
- 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
+ }
65
75
 
66
76
  readonly [TypeId]: typeof TypeId
67
77
 
@@ -69,17 +79,36 @@ export interface Client<Self, ClientId extends string, D extends ProtocolDefinit
69
79
 
70
80
  readonly protocol: Protocol<D>
71
81
 
82
+ readonly state: Stream.Stream<
83
+ S.Struct<D["state"]>["Type"],
84
+ ClientError | S.SchemaError,
85
+ Self | S.Struct<D["state"]>["DecodingServices"]
86
+ >
87
+
72
88
  readonly events: Stream.Stream<
73
89
  ReturnType<typeof S.TaggedUnion<D["events"]>>["Type"],
74
90
  ClientError | S.SchemaError,
75
91
  Self
76
92
  >
77
93
 
78
- readonly f: F<Self, D>
94
+ readonly fn: Fn<Self, D["external"]>
79
95
 
80
96
  readonly invalidate: Effect.Effect<void, never, Self>
97
+
98
+ readonly reducer: <K extends keyof D["events"], R extends Reducer.Reducer<D, K>>(_tag: K, f: R) => R
81
99
  }
82
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
+
83
112
  export const Service =
84
113
  <Self>() =>
85
114
  <Id extends string, D extends ProtocolDefinition>(id: Id, definition: D): Client<Self, Id, D> => {
@@ -87,15 +116,29 @@ export const Service =
87
116
 
88
117
  const protocol = Protocol(definition)
89
118
 
90
- const events = tag.asEffect().pipe(Effect.flatMap(RcRef.get), Effect.map(Struct.get("events")), Stream.unwrap)
119
+ const state = tag.pipe(
120
+ Effect.flatMap(RcRef.get),
121
+ Effect.map(({ state }) => state),
122
+ Stream.unwrap,
123
+ )
91
124
 
92
- const f: F<Self, D> = (_tag) =>
93
- Effect.fnUntraced(function* (value) {
94
- const { f } = yield* tag.asEffect().pipe(Effect.flatMap(RcRef.get))
95
- return yield* f(_tag)(value)
96
- }, Effect.scoped)
125
+ const events = tag.pipe(
126
+ Effect.flatMap(RcRef.get),
127
+ Effect.map(({ events }) => events),
128
+ Stream.unwrap,
129
+ )
97
130
 
98
- const invalidate = tag.asEffect().pipe(
131
+ const fn = ((_tag: keyof D["external"], ...f: Array<any>) =>
132
+ Effect.fnUntraced(
133
+ function* (payload: any) {
134
+ const { fnRaw: fn } = yield* tag.pipe(Effect.flatMap(RcRef.get))
135
+ return yield* fn(_tag, payload)
136
+ },
137
+ Effect.scoped,
138
+ ...(f as [any]),
139
+ )) as Fn<Self, D["external"]>
140
+
141
+ const invalidate = tag.pipe(
99
142
  Effect.flatMap((rc) =>
100
143
  RcRef.get(rc).pipe(
101
144
  Effect.flatMap(({ end }) => end),
@@ -106,47 +149,69 @@ export const Service =
106
149
  Effect.ignore,
107
150
  )
108
151
 
109
- return Object.assign(tag, {
152
+ const reducer = <K extends keyof D["events"], R extends Reducer.Reducer<D, K>>(_event: K, f: R) => f
153
+
154
+ return Object.assign(tag satisfies Context.ServiceClass.Shape<Id, Service<Self, D>> as never, {
110
155
  [TypeId]: TypeId,
111
156
  definition,
112
157
  protocol,
158
+ state,
113
159
  events,
114
- f,
160
+ fn,
115
161
  invalidate,
162
+ reducer,
116
163
  })
117
164
  }
118
165
 
119
- export interface ClientTransport<D extends ProtocolDefinition> {
166
+ export interface ClientTransport<D extends ProtocolDefinition, R> {
120
167
  readonly listen: (
121
- publish: (message: Protocol<D>["Actor"]["Type"]) => Effect.Effect<void, ClientError>,
122
- ) => Effect.Effect<void, ClientError | S.SchemaError, Scope.Scope | Protocol<D>["Actor"]["DecodingServices"]>
168
+ publish: (message: Protocol<D>["Actor"]["Type"]) => Effect.Effect<void, ClientError, R>,
169
+ ) => Effect.Effect<void, ClientError | S.SchemaError, Scope.Scope | Protocol<D>["Actor"]["DecodingServices"] | R>
123
170
 
124
171
  readonly send: (
125
172
  message: Protocol<D>["F"]["Payload"]["Type"],
126
173
  ) => Effect.Effect<void, ClientError | S.SchemaError, Protocol<D>["F"]["Payload"]["EncodingServices"]>
127
174
  }
128
175
 
129
- const make = <Self, Id extends string, D extends ProtocolDefinition, R>(
130
- client: Client<Self, Id, D>,
131
- build: Effect.Effect<ClientTransport<D>, ClientError, R | Scope.Scope>,
132
- replay?: ReplayConfig | undefined,
133
- ) =>
176
+ const make = <Self, Id extends string, D extends ProtocolDefinition, Reducers extends Reducer.Reducers<D>, R, CR>({
177
+ build,
178
+ client,
179
+ reducers,
180
+ onConnect,
181
+ replay,
182
+ }: {
183
+ readonly client: Client<Self, Id, D>
184
+ readonly onConnect?: undefined | ((state: S.Struct<D["state"]>["Type"]) => Effect.Effect<void, never, CR>)
185
+ readonly reducers: Reducers
186
+ readonly build: Effect.Effect<
187
+ ClientTransport<D, Reducer.Reducers.Services<Self, Reducers> | CR>,
188
+ ClientError,
189
+ R | Scope.Scope
190
+ >
191
+ readonly replay?: ReplayConfig | undefined
192
+ }) =>
134
193
  Effect.gen(function* () {
135
- const rcr: RcRef.RcRef<Session<Self, D>, ClientError> = yield* RcRef.make({
194
+ const rcr: Service<Self, D> = yield* RcRef.make({
136
195
  acquire: Effect.gen(function* () {
137
196
  type _ = typeof client.protocol
138
197
  type Event = ReturnType<typeof S.TaggedUnion<D["events"]>>["Type"]
139
198
 
140
- yield* debug("AcquisitionStarted")
141
-
142
199
  const { listen, send } = yield* build
143
200
 
144
201
  const audition = yield* Deferred.make<void>()
202
+ const stateDeferred = yield* Deferred.make<Ref.Ref<S.Struct<D["state"]>["Type"]>>()
145
203
 
146
- const inflights: Record<string, Deferred.Deferred<_["F"]["Success"]["Type"], FError<D>>> = {}
204
+ const inflights: Record<
205
+ string,
206
+ {
207
+ readonly deferred: Deferred.Deferred<_["F"]["Success"]["Type"], FnError<D["external"], keyof D["external"]>>
208
+ readonly span?: Tracer.AnySpan | undefined
209
+ }
210
+ > = {}
147
211
  let callId = 0
148
212
  let takeCount = 0
149
- const pubsub = yield* PubSub.unbounded<EventTake<Event, ClientError>>()
213
+ const eventsPubsub = yield* PubSub.unbounded<EventTake<Event, ClientError>>()
214
+ const statePubsub = yield* PubSub.unbounded<S.Struct<D["state"]>["Type"]>({ replay: 1 })
150
215
 
151
216
  const replayState = yield* Ref.make<{
152
217
  readonly startupOpen: boolean
@@ -175,57 +240,87 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, R>(
175
240
  return { startupOpen, buffer }
176
241
  })
177
242
  }
178
- yield* PubSub.publish(pubsub, eventTake)
243
+ yield* PubSub.publish(eventsPubsub, eventTake)
179
244
  })
180
245
 
181
246
  const outer = yield* Scope.Scope
182
247
  const scope = yield* Scope.fork(outer, "sequential")
183
248
  const end = Scope.close(scope, Exit.void)
249
+ const reduceMutex = yield* Semaphore.make(1)
250
+ const reduceTask = Semaphore.withPermits(reduceMutex, 1)
184
251
 
185
252
  const fiber = yield* listen(
186
253
  Effect.fnUntraced(function* (message) {
187
254
  switch (message._tag) {
188
255
  case "Audition.Success": {
189
- yield* debug("Audition.Succeeded")
256
+ const { initial } = message
257
+ yield* PubSub.publish(statePubsub, initial)
258
+ const state = yield* Ref.make(initial)
259
+ yield* Deferred.succeed(stateDeferred, state)
190
260
  yield* Deferred.succeed(audition, void 0)
261
+ yield* onConnect?.(initial) ?? Effect.void
191
262
  return
192
263
  }
193
264
  case "Audition.Failure": {
194
265
  const { expected, actual } = message
195
- yield* debug("Audition.Failed", { expected, actual })
196
266
  return yield* new AuditionError({ value: { expected, actual } })
197
267
  }
198
268
  case "Event": {
199
269
  const { event } = message
200
- yield* debug("Event.Emitted", { event })
201
- yield* publishTake([event as never], true)
270
+ const { _tag } = event as never
271
+ const reducer = reducers[_tag]!
272
+ const state = yield* Deferred.await(stateDeferred)
273
+ yield* Effect.gen(function* () {
274
+ const current = yield* Ref.get(state)
275
+ const reduced = yield* reducer(event as never)(current).pipe(
276
+ Effect.provideService(client, rcr),
277
+ // TODO: rework error-handling
278
+ Effect.catchDefect(() => Effect.succeed(undefined)),
279
+ ) as Effect.Effect<
280
+ S.Struct<D["state"]>["Type"] | undefined,
281
+ never,
282
+ Reducer.Reducers.Services<Self, Reducers>
283
+ >
284
+ if (reduced) {
285
+ yield* PubSub.publish(statePubsub, reduced)
286
+ yield* Ref.set(state, reduced)
287
+ }
288
+ }).pipe(reduceTask)
289
+ const parent = message.trace ? Tracer.externalSpan(message.trace) : undefined
290
+ yield* publishTake([event], true).pipe(
291
+ Boundary.span("enqueue-event", import.meta.url, {
292
+ attributes: { _tag },
293
+ kind: "consumer",
294
+ parent,
295
+ }),
296
+ )
202
297
  return
203
298
  }
204
299
  case "F.Success":
205
300
  case "F.Failure": {
206
301
  const { id } = message
207
- const deferred = inflights[id]
208
- if (deferred) {
302
+ const inflight = inflights[id]
303
+ if (inflight) {
209
304
  delete inflights[id]
210
- switch (message._tag) {
211
- case "F.Success": {
212
- const { _tag, value } = message.success as never
213
- yield* debug("Call.Succeeded", { id, _tag, value })
214
- yield* Deferred.succeed(deferred, value)
215
- return
305
+ return yield* Effect.gen(function* () {
306
+ switch (message._tag) {
307
+ case "F.Success": {
308
+ const { value } = message.success as never
309
+ yield* Deferred.succeed(inflight.deferred, value)
310
+ return
311
+ }
312
+ case "F.Failure": {
313
+ const { _tag, value } = message.failure as never
314
+ yield* Effect.annotateLogs(Effect.logDebug("Call.Failed"), { id, _tag })
315
+ yield* Deferred.fail(inflight.deferred, value)
316
+ return
317
+ }
216
318
  }
217
- case "F.Failure": {
218
- const { _tag, value } = message.failure as never
219
- yield* debug("Call.Failed", { id, _tag, value })
220
- yield* Deferred.fail(deferred, value)
221
- return
222
- }
223
- }
319
+ }).pipe(inflight.span ? Effect.withParentSpan(inflight.span, { captureStackTrace: false }) : identity)
224
320
  }
225
321
  return
226
322
  }
227
323
  case "Disconnect": {
228
- yield* debug("Disconnected")
229
324
  return
230
325
  }
231
326
  }
@@ -234,7 +329,13 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, R>(
234
329
  Effect.ensuring(
235
330
  Effect.all(
236
331
  [
237
- debug("Client.Closed", { unresolved: Record.keys(inflights).length }),
332
+ Effect.sync(() => Record.keys(inflights).length).pipe(
333
+ Effect.flatMap((unresolved) =>
334
+ unresolved === 0
335
+ ? Effect.void
336
+ : Effect.annotateLogs(Effect.logDebug("Client.Closed"), { unresolved }),
337
+ ),
338
+ ),
238
339
  Deferred.succeed(audition, void 0),
239
340
  RcRef.invalidate(rcr),
240
341
  ],
@@ -245,8 +346,25 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, R>(
245
346
  Effect.provideService(Scope.Scope, scope),
246
347
  )
247
348
 
349
+ const interrupt = Stream.interruptWhen(
350
+ Fiber.await(fiber).pipe(
351
+ Effect.flatMap(
352
+ Exit.match({
353
+ onSuccess: () => Effect.void,
354
+ onFailure: flow(
355
+ Cause.findError,
356
+ Result.match({
357
+ onSuccess: Effect.fail,
358
+ onFailure: () => Effect.void,
359
+ }),
360
+ ),
361
+ }),
362
+ ),
363
+ ),
364
+ )
365
+
248
366
  const events = Effect.gen(function* () {
249
- const queue = yield* PubSub.subscribe(pubsub)
367
+ const queue = yield* PubSub.subscribe(eventsPubsub)
250
368
  const live = (replayCount: number) =>
251
369
  Stream.fromSubscription(queue).pipe(
252
370
  Stream.filter((entry) => entry.seq > replayCount),
@@ -283,206 +401,265 @@ const make = <Self, Id extends string, D extends ProtocolDefinition, R>(
283
401
  ),
284
402
  live(replayCount),
285
403
  )
286
- }).pipe(
287
- Stream.unwrap,
288
- Stream.interruptWhen(
289
- Fiber.await(fiber).pipe(
290
- Effect.flatMap(
291
- Exit.match({
292
- onSuccess: () => Effect.void,
293
- onFailure: flow(
294
- Cause.findError,
295
- Result.match({
296
- onSuccess: Effect.fail,
297
- onFailure: () => Effect.void,
298
- }),
299
- ),
300
- }),
301
- ),
302
- ),
303
- ),
304
- )
404
+ }).pipe(Stream.unwrap, interrupt)
305
405
 
306
- yield* Deferred.await(audition)
406
+ const state = Stream.fromPubSub(statePubsub).pipe(interrupt)
307
407
 
308
408
  const encodingServices = yield* Effect.context<_["F"]["Payload"]["EncodingServices"]>()
309
409
 
310
- const f: F<Self, D> = (_tag) =>
311
- Effect.fnUntraced(
312
- function* (value) {
313
- const exit = fiber.pollUnsafe()
314
- if (exit) {
315
- return yield* Exit.match(exit, {
316
- onSuccess: () => new UnresolvedError(),
317
- onFailure: flow(
318
- Cause.findError,
319
- Result.match({
320
- onSuccess: Effect.fail,
321
- onFailure: () => new UnresolvedError(),
322
- }),
323
- ),
324
- })
325
- }
326
- const id = callId++
327
- const inflight = yield* Deferred.make<_["F"]["Success"]["Type"], FError<D>>()
328
- inflights[id] = inflight
329
- yield* send({
330
- _tag: "F.Payload",
331
- id,
332
- payload: { _tag, value } as never,
410
+ yield* Deferred.await(audition)
411
+
412
+ const fnRaw = <K extends keyof D["external"]>(_tag: K, value: D["external"][K]["payload"]["Type"]) =>
413
+ Effect.gen(function* () {
414
+ const exit = fiber.pollUnsafe()
415
+ if (exit) {
416
+ return yield* Exit.match(exit, {
417
+ onSuccess: () => new UnresolvedError(),
418
+ onFailure: flow(
419
+ Cause.findError,
420
+ Result.match({
421
+ onSuccess: Effect.fail,
422
+ onFailure: () => new UnresolvedError(),
423
+ }),
424
+ ),
333
425
  })
334
- return yield* Effect.raceFirst(
335
- Deferred.await(inflight),
336
- Fiber.await(fiber).pipe(
337
- Effect.flatMap(
338
- (exit): Effect.Effect<never, ClientError | UnresolvedError | S.SchemaError> =>
339
- Exit.match(exit, {
340
- onSuccess: () => new UnresolvedError().asEffect(),
341
- onFailure: flow(
342
- Cause.findError,
343
- Result.match({
344
- onSuccess: Effect.fail,
345
- onFailure: () => new UnresolvedError().asEffect(),
346
- }),
347
- ),
348
- }),
349
- ),
426
+ }
427
+ const id = callId++
428
+ const deferred = yield* Deferred.make<
429
+ _["F"]["Success"]["Type"],
430
+ FnError<D["external"], keyof D["external"]>
431
+ >()
432
+ const span = yield* Tracing.current
433
+ const trace = span ? Tracing.toTraceEnvelope(span) : undefined
434
+ inflights[id] = { deferred, span }
435
+ yield* send({
436
+ _tag: "F.Payload",
437
+ id,
438
+ payload: { _tag, value } as never,
439
+ ...(trace && { trace }),
440
+ })
441
+ return yield* Effect.raceFirst(
442
+ Deferred.await(deferred),
443
+ Fiber.await(fiber).pipe(
444
+ Effect.flatMap(
445
+ (exit): Effect.Effect<never, ClientError | UnresolvedError | S.SchemaError> =>
446
+ Exit.match(exit, {
447
+ onSuccess: () => new UnresolvedError(),
448
+ onFailure: flow(
449
+ Cause.findError,
450
+ Result.match({
451
+ onSuccess: Effect.fail,
452
+ onFailure: () => new UnresolvedError(),
453
+ }),
454
+ ),
455
+ }),
350
456
  ),
351
- )
352
- },
353
- span("f"),
354
- Effect.scoped,
457
+ ),
458
+ )
459
+ }).pipe(
460
+ Boundary.span("fn", import.meta.url, {
461
+ kind: "client",
462
+ attributes: { _tag },
463
+ }),
355
464
  Effect.provide(encodingServices),
356
465
  )
357
466
 
358
- return { events, f, end }
359
- }).pipe(span("acquire", { attributes: { client: client.key } }), Effect.annotateLogs("client", client.key)),
467
+ return { state, events, fnRaw, end }
468
+ }).pipe(
469
+ Boundary.span("acquire", import.meta.url, {
470
+ attributes: { client: client.key },
471
+ }),
472
+ Effect.annotateLogs("client", client.key),
473
+ ),
360
474
  })
361
475
 
362
476
  return rcr
363
477
  }).pipe(Layer.effect(client))
364
478
 
365
- export const layerSocket = <Self, Id extends string, D extends ProtocolDefinition>({
479
+ let clientId_: string | undefined
480
+ const clientId = () => {
481
+ if (!clientId_) {
482
+ clientId_ = crypto.randomUUID()
483
+ }
484
+ return clientId_
485
+ }
486
+
487
+ export const layerSocket = <
488
+ Self,
489
+ Id extends string,
490
+ D extends ProtocolDefinition,
491
+ Reducers extends Reducer.Reducers<D>,
492
+ CR = never,
493
+ >({
366
494
  client,
495
+ reducers,
367
496
  url,
368
497
  protocols,
369
498
  replay,
499
+ onConnect,
370
500
  }: {
371
501
  readonly client: Client<Self, Id, D>
372
- readonly url?: string | undefined
373
- readonly protocols?: string | Array<string> | undefined
502
+ readonly reducers: Reducers
374
503
  readonly replay?: ReplayConfig | undefined
504
+ readonly onConnect?: undefined | ((state: S.Struct<D["state"]>["Type"]) => Effect.Effect<void, never, CR>)
505
+ readonly protocols?: string | Array<string> | undefined
506
+ readonly url?: string | undefined
375
507
  }): Layer.Layer<
376
508
  Self,
377
509
  never,
378
510
  | Socket.WebSocketConstructor
379
511
  | Protocol<D>["Actor"]["DecodingServices"]
380
512
  | Protocol<D>["F"]["Payload"]["EncodingServices"]
513
+ | Reducer.Reducers.Services<Self, Reducers>
514
+ | CR
381
515
  > => {
382
516
  const { F, Actor } = client.protocol
383
517
  const encodeFPayload = encodeJsonString(F.Payload)
384
518
  const decodeActor = decodeJsonString(Actor)
385
519
 
386
- return make<Self, Id, D, Socket.WebSocketConstructor>(
520
+ return make<Self, Id, D, Reducers, Socket.WebSocketConstructor, CR>({
387
521
  client,
388
- Effect.gen(function* () {
522
+ reducers,
523
+ onConnect,
524
+ replay,
525
+ build: Effect.gen(function* () {
389
526
  const socket = yield* Socket.makeWebSocket(url ?? "/", {
390
- 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
+ ],
391
533
  })
392
534
  return {
393
- listen: Effect.fnUntraced(function* (publish) {
394
- yield* socket
395
- .runRaw((raw) =>
396
- pipe(
397
- raw instanceof Uint8Array ? new TextDecoder().decode(raw) : raw,
398
- decodeActor,
399
- Effect.andThen(publish),
400
- ),
401
- )
402
- .pipe(
403
- Effect.catchIf(
404
- Socket.isSocketError,
405
- Effect.fnUntraced(function* (cause) {
406
- const { reason } = cause
407
- if (reason._tag === "SocketCloseError" && reason.code === 1000) {
408
- yield* debug("Socket.Disconnected")
409
- return yield* publish({ _tag: "Disconnect" })
410
- }
411
- yield* debug(`SocketErrored.${reason._tag}`, { cause })
412
- return yield* new ConnectionError({ cause })
413
- }),
414
- ),
415
- )
416
- }, 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
+ ),
417
561
  send: Effect.fnUntraced(
418
562
  function* (v) {
419
563
  const write = yield* socket.writer
420
564
  const message = yield* encodeFPayload(v)
421
565
  yield* write(message).pipe(
422
- Effect.catchTag("SocketError", (cause) => new ConnectionError({ cause }).asEffect()),
566
+ Effect.catchTags({
567
+ SocketError: (cause) => new ConnectionError({ cause }),
568
+ }),
423
569
  )
424
570
  },
425
- span("send"),
571
+ Boundary.span("send", import.meta.url),
426
572
  Effect.scoped,
427
573
  ),
428
574
  }
429
575
  }),
430
- replay,
431
- )
576
+ })
432
577
  }
433
578
 
434
- export const layerWorker = <Self, Id extends string, D extends ProtocolDefinition, T extends Protocol<D>>({
579
+ export const layerWorker = <
580
+ Self,
581
+ Id extends string,
582
+ D extends ProtocolDefinition,
583
+ Reducers extends Reducer.Reducers<D>,
584
+ T extends Protocol<D>,
585
+ CR = never,
586
+ >({
435
587
  client,
588
+ reducers,
436
589
  replay,
590
+ onConnect,
437
591
  }: {
438
592
  readonly client: Client<Self, Id, D>
593
+ readonly reducers: Reducers
439
594
  readonly replay?: ReplayConfig | undefined
595
+ readonly onConnect?: undefined | ((state: S.Struct<D["state"]>["Type"]) => Effect.Effect<void, never, CR>)
440
596
  }): Layer.Layer<
441
597
  Self,
442
598
  never,
443
- Worker.WorkerPlatform | Worker.Spawner | T["Actor"]["DecodingServices"] | T["F"]["Payload"]["EncodingServices"]
444
- > =>
445
- make<Self, Id, D, Worker.WorkerPlatform | Worker.Spawner>(
599
+ | Worker.WorkerPlatform
600
+ | Worker.Spawner
601
+ | T["Actor"]["DecodingServices"]
602
+ | T["Client"]["EncodingServices"]
603
+ | Reducer.Reducers.Services<Self, Reducers>
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>({
446
610
  client,
447
- Effect.gen(function* () {
611
+ reducers,
612
+ onConnect,
613
+ replay,
614
+ build: Effect.gen(function* () {
448
615
  const platform = yield* Worker.WorkerPlatform
449
- const backing = yield* platform
450
- .spawn<T["Actor"]["Type"], T["Client"]["Type"]>(0)
451
- .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
+ )
452
621
 
453
622
  const send = (message: T["Client"]["Type"]) =>
454
- backing.send(message).pipe(
455
- Effect.catchTag("WorkerError", (cause) => new ConnectionError({ cause }).asEffect()),
456
- 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),
457
629
  )
458
630
 
459
631
  return {
460
- listen: Effect.fnUntraced(function* (publish) {
461
- const stop = yield* Deferred.make<void>()
462
- yield* backing
463
- .run(
464
- Effect.fnUntraced(function* (message) {
465
- yield* publish(message)
466
- if (message._tag === "Disconnect" || message._tag === "Audition.Failure") {
467
- yield* Deferred.succeed(stop, void 0)
468
- }
469
- }),
470
- {
471
- onSpawn: backing
472
- .send({
473
- _tag: "Audition.Payload",
474
- client: client.key,
475
- })
476
- .pipe(Effect.orDie),
477
- },
478
- )
479
- .pipe(
480
- Effect.raceFirst(Deferred.await(stop)),
481
- Effect.catchTag("WorkerError", (cause) => new ConnectionError({ cause }).asEffect()),
482
- )
483
- }, 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
+ ),
484
661
  send,
485
662
  }
486
663
  }),
487
- replay,
488
- )
664
+ })
665
+ }