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
@@ -0,0 +1,449 @@
1
+ import { DurableObject } from "cloudflare:workers"
2
+ import {
3
+ Layer,
4
+ Effect,
5
+ Scope,
6
+ Schema as S,
7
+ Context,
8
+ ManagedRuntime,
9
+ ConfigProvider,
10
+ Duration,
11
+ flow,
12
+ Option,
13
+ Tracer,
14
+ pipe,
15
+ Exit,
16
+ } from "effect"
17
+ import { DoState } from "effect-workerd"
18
+ import { Env } from "effect-workerd"
19
+ import { Clock } from "effect-workerd/platform"
20
+ import { SecWebSocketProtocol } from "effect-workerd/socket_util"
21
+ import { Headers, FetchHttpClient, HttpClient, HttpTraceContext } from "effect/unstable/http"
22
+ import * as Boundary from "liminal-util/Boundary"
23
+ import { type TopFromString, encodeJsonString, decodeJsonString } from "liminal-util/schema"
24
+
25
+ import type { ActorNamespace } from "./ActorNamespace.ts"
26
+ import type { ActorTransport } from "./ActorTransport.ts"
27
+ import * as ClientDirectory from "./ClientDirectory.ts"
28
+ import type { ClientHandle } from "./ClientHandle.ts"
29
+ import type { Handlers, Methods } from "./Method.ts"
30
+ import type { ProtocolDefinition } from "./Protocol.ts"
31
+ import * as Tracing from "./Tracing.ts"
32
+ import { sessionAttributes, SessionId, sessionLink } from "./Tracing.ts"
33
+
34
+ export interface ActorRuntimeDefinition<
35
+ NamespaceSelf,
36
+ NamespaceId extends string,
37
+ Internal extends Methods,
38
+ ActorSelf,
39
+ ActorId extends string,
40
+ Name extends TopFromString,
41
+ AttachmentFields extends S.Struct.Fields,
42
+ ClientSelf,
43
+ ClientId extends string,
44
+ D extends ProtocolDefinition,
45
+ PreludeROut,
46
+ PreludeE,
47
+ RunROut,
48
+ RunE,
49
+ > {
50
+ readonly ""?: this["namespace"]["definition"]["actor"]["definition"]["client"]["protocol"]
51
+
52
+ readonly namespace: ActorNamespace<
53
+ NamespaceSelf,
54
+ NamespaceId,
55
+ Internal,
56
+ ActorSelf,
57
+ ActorId,
58
+ Name,
59
+ AttachmentFields,
60
+ ClientSelf,
61
+ ClientId,
62
+ D
63
+ >
64
+
65
+ readonly prelude: Layer.Layer<
66
+ | PreludeROut
67
+ | NonNullable<this[""]>["F"]["Payload"]["DecodingServices"]
68
+ | NonNullable<this[""]>["F"]["Success"]["EncodingServices"]
69
+ | NonNullable<this[""]>["F"]["Failure"]["EncodingServices"]
70
+ | NonNullable<this[""]>["Event"]["EncodingServices"]
71
+ | S.Struct<AttachmentFields>["DecodingServices"]
72
+ | S.Struct<AttachmentFields>["EncodingServices"]
73
+ | Name["EncodingServices"]
74
+ | Name["DecodingServices"],
75
+ PreludeE,
76
+ HttpClient.HttpClient | Env
77
+ >
78
+
79
+ readonly layer: Layer.Layer<RunROut, RunE, ActorSelf | HttpClient.HttpClient | Env | PreludeROut>
80
+
81
+ readonly external: Handlers<
82
+ D["external"],
83
+ ActorSelf | HttpClient.HttpClient | Env | PreludeROut | RunROut | Scope.Scope
84
+ >
85
+
86
+ readonly internal: Handlers<Internal, ActorSelf | HttpClient.HttpClient | PreludeROut | RunROut | Scope.Scope>
87
+
88
+ readonly hydrate: Effect.Effect<
89
+ S.Struct<D["state"]>["Type"],
90
+ never,
91
+ ActorSelf | HttpClient.HttpClient | Env | PreludeROut | RunROut | Scope.Scope
92
+ >
93
+
94
+ readonly onDisconnect: Effect.Effect<
95
+ void,
96
+ never,
97
+ ActorSelf | HttpClient.HttpClient | Env | PreludeROut | RunROut | Scope.Scope
98
+ >
99
+
100
+ readonly hibernation?: Duration.Input | undefined
101
+ }
102
+
103
+ export const make = <
104
+ NamespaceSelf,
105
+ NamespaceId extends string,
106
+ Internal extends Methods,
107
+ ActorSelf,
108
+ ActorId extends string,
109
+ Name extends TopFromString,
110
+ AttachmentFields extends S.Struct.Fields,
111
+ ClientSelf,
112
+ ClientId extends string,
113
+ D extends ProtocolDefinition,
114
+ PreludeROut,
115
+ PreludeE,
116
+ RunROut,
117
+ RunE,
118
+ >(
119
+ definition: ActorRuntimeDefinition<
120
+ NamespaceSelf,
121
+ NamespaceId,
122
+ Internal,
123
+ ActorSelf,
124
+ ActorId,
125
+ Name,
126
+ AttachmentFields,
127
+ ClientSelf,
128
+ ClientId,
129
+ D,
130
+ PreludeROut,
131
+ PreludeE,
132
+ RunROut,
133
+ RunE
134
+ >,
135
+ ): new (state: DurableObjectState<{}>, env: Cloudflare.Env) => DurableObject => {
136
+ const {
137
+ hibernation,
138
+ prelude,
139
+ external,
140
+ layer,
141
+ hydrate,
142
+ onDisconnect,
143
+ internal,
144
+ namespace: {
145
+ definition: { actor },
146
+ },
147
+ } = definition
148
+ const {
149
+ definition: {
150
+ name: Name,
151
+ client: { protocol: P },
152
+ attachments: AttachmentFields,
153
+ },
154
+ } = actor
155
+
156
+ const Attachments = S.Struct(AttachmentFields)
157
+ const SocketAttachment = S.Struct({
158
+ attachments: S.toCodecJson(Attachments),
159
+ session: Tracing.Session,
160
+ })
161
+ const encodeSocketAttachment = S.encodeEffect(SocketAttachment)
162
+ const decodeSocketAttachment = S.decodeUnknownEffect(SocketAttachment)
163
+ const decodeAttachmentsString = decodeJsonString(Attachments)
164
+ const encodeAuditionSuccess = encodeJsonString(P.Audition.Success)
165
+ const decodeClient = decodeJsonString(P.Client)
166
+ const encodeFSuccess = encodeJsonString(P.F.Success)
167
+ const encodeFFailure = encodeJsonString(P.F.Failure)
168
+ const encodeEvent = encodeJsonString(P.Event)
169
+
170
+ const transport: ActorTransport<
171
+ WebSocket,
172
+ {
173
+ readonly socket: WebSocket
174
+ readonly session: typeof Tracing.Session.Type
175
+ },
176
+ AttachmentFields,
177
+ D
178
+ > = {
179
+ key: ({ socket }) => socket,
180
+ send: ({ socket, session }, event) => {
181
+ const { _tag } = event.event as never
182
+ return Effect.gen(function* () {
183
+ const trace = yield* Tracing.currentTrace
184
+ const encoded = yield* encodeEvent({ ...event, ...(trace && { trace }) }).pipe(
185
+ Effect.catchTags({
186
+ SchemaError: Effect.die,
187
+ }),
188
+ )
189
+ // @effect-diagnostics-next-line tryCatchInEffectGen:off
190
+ try {
191
+ socket.send(encoded)
192
+ // oxlint-disable-next-line no-unused-vars
193
+ } catch (_e) {}
194
+ }).pipe(
195
+ Boundary.span("send", import.meta.url, {
196
+ attributes: { _tag, ...sessionAttributes(session) },
197
+ kind: "producer",
198
+ links: [sessionLink(session)],
199
+ }),
200
+ )
201
+ },
202
+ close: ({ socket }) => Effect.sync(() => socket.close(1000)),
203
+ snapshot: ({ socket, session }, attachments) =>
204
+ encodeSocketAttachment({ attachments, session }).pipe(
205
+ Effect.catchTags({
206
+ SchemaError: Effect.die,
207
+ }),
208
+ Effect.andThen((v) => Effect.sync(() => socket.serializeAttachment(v))),
209
+ ),
210
+ }
211
+
212
+ class NameDecoded extends Context.Service<NameDecoded, Name["Type"]>()("liminal/ActorNamespace/NameDecoded") {}
213
+
214
+ return class extends DurableObject {
215
+ readonly run
216
+ readonly directory = ClientDirectory.make(actor, { transport })
217
+ readonly provideActor = (currentClient: ClientHandle<ActorSelf, AttachmentFields, D>) =>
218
+ flow(
219
+ Effect.provide(
220
+ Layer.provideMerge(
221
+ layer,
222
+ Effect.gen({ self: this }, function* () {
223
+ const name = yield* NameDecoded
224
+ return Layer.succeed(actor, {
225
+ name,
226
+ clients: this.directory.handles,
227
+ currentClient,
228
+ })
229
+ }).pipe(Layer.unwrap),
230
+ ),
231
+ ),
232
+ Effect.scoped,
233
+ )
234
+ constructor(state: DurableObjectState<{}>, env: Cloudflare.Env) {
235
+ super(state, env)
236
+ if (hibernation) {
237
+ Option.andThen(
238
+ Duration.fromInput(hibernation),
239
+ flow(Duration.toMillis, (timeout) => state.setHibernatableWebSocketEventTimeout(timeout)),
240
+ )
241
+ }
242
+
243
+ const Live = Layer.mergeAll(
244
+ FetchHttpClient.layer,
245
+ Layer.succeed(DoState.DoState, state),
246
+ Layer.succeed(Env, env as never),
247
+ Layer.effect(NameDecoded, S.decodeUnknownEffect(Name)(state.id.name)),
248
+ ).pipe(
249
+ Layer.provideMerge(
250
+ prelude.pipe(
251
+ Layer.provideMerge(
252
+ Layer.mergeAll(
253
+ FetchHttpClient.layer,
254
+ ConfigProvider.layer(ConfigProvider.fromUnknown(env)),
255
+ Layer.succeed(Env, env as never),
256
+ ),
257
+ ),
258
+ ),
259
+ ),
260
+ Layer.provideMerge(Clock.layer),
261
+ )
262
+
263
+ const HydrateClientsLive = Effect.gen({ self: this }, function* () {
264
+ for (const socket of state.getWebSockets()) {
265
+ const { attachments, session } = yield* decodeSocketAttachment(socket.deserializeAttachment())
266
+ yield* this.directory
267
+ .register({ socket, session }, attachments)
268
+ .pipe(Effect.linkSpans(Tracer.externalSpan(session.trace), sessionLink(session).attributes))
269
+ }
270
+ }).pipe(Boundary.span("hydrate", import.meta.url), Layer.effectDiscard)
271
+
272
+ const runtime = ManagedRuntime.make(
273
+ HydrateClientsLive.pipe(Layer.provideMerge(Live), Boundary.layer("actor", import.meta.url)),
274
+ )
275
+ this.run = <A, E, R extends ManagedRuntime.ManagedRuntime.Services<typeof runtime>>(
276
+ effect: Effect.Effect<A, E, R>,
277
+ ) => Effect.onError(effect, Effect.logError).pipe(runtime.runPromise)
278
+ }
279
+
280
+ override fetch(request: Request): Promise<Response> {
281
+ return Effect.gen({ self: this }, function* () {
282
+ const url = new URL(request.url)
283
+ const attachments = yield* decodeAttachmentsString(url.searchParams.get("__liminal_attachments"))
284
+ const clientId = yield* Effect.fromNullishOr(url.searchParams.get("__liminal_client_id"))
285
+ const { 0: webSocket, 1: server } = new WebSocketPair()
286
+ const state = yield* DoState.DoState
287
+ const session = {
288
+ id: SessionId.make(clientId),
289
+ trace: yield* Effect.currentSpan.pipe(Effect.map(Tracing.toTraceEnvelope)),
290
+ }
291
+ const currentClient = yield* this.directory.register({ socket: server, session }, attachments)
292
+ state.acceptWebSocket(server)
293
+ const initial = yield* hydrate.pipe(
294
+ this.provideActor(currentClient),
295
+ Boundary.span("hydrate", import.meta.url, {
296
+ attributes: sessionAttributes(session),
297
+ links: [sessionLink(session)],
298
+ }),
299
+ )
300
+ server.send(yield* encodeAuditionSuccess({ _tag: "Audition.Success", initial }))
301
+ return new Response(null, {
302
+ status: 101,
303
+ webSocket,
304
+ headers: { [SecWebSocketProtocol]: "liminal" },
305
+ })
306
+ }).pipe(
307
+ Boundary.span("fetch", import.meta.url, {
308
+ kind: "server",
309
+ parent: pipe(request.headers, Headers.fromInput, HttpTraceContext.fromHeaders, Option.getOrUndefined),
310
+ }),
311
+ this.run,
312
+ )
313
+ }
314
+
315
+ override webSocketMessage(socket: WebSocket, raw: string | ArrayBuffer) {
316
+ Effect.gen({ self: this }, function* () {
317
+ const { client, handle: currentClient } = yield* this.directory.entry(socket)
318
+ const { session } = client
319
+ yield* Effect.annotateCurrentSpan(sessionAttributes(session))
320
+ const message = yield* decodeClient(raw instanceof ArrayBuffer ? new TextDecoder().decode(raw) : raw)
321
+ if (message._tag === "Audition.Payload") {
322
+ return yield* Effect.die(undefined)
323
+ }
324
+ if (message._tag === "Disconnect") {
325
+ yield* currentClient.disconnect
326
+ return yield* onDisconnect.pipe(
327
+ this.provideActor(currentClient),
328
+ Boundary.span("disconnect", import.meta.url, {
329
+ attributes: sessionAttributes(session),
330
+ links: [sessionLink(session)],
331
+ }),
332
+ )
333
+ }
334
+ const { id, payload } = message
335
+ const { _tag, value } = payload as never
336
+ const parent = message.trace ? Tracer.externalSpan(message.trace) : undefined
337
+ const transportSpan = yield* Tracing.parent
338
+ const links = [
339
+ sessionLink(session),
340
+ ...(parent && transportSpan
341
+ ? [
342
+ {
343
+ span: transportSpan,
344
+ attributes: {
345
+ "liminal.link": "transport",
346
+ "liminal.transport": "websocket",
347
+ },
348
+ },
349
+ ]
350
+ : []),
351
+ ]
352
+ yield* external[_tag]!(value).pipe(
353
+ Effect.matchEffect({
354
+ onSuccess: (value) =>
355
+ encodeFSuccess({
356
+ _tag: "F.Success",
357
+ id,
358
+ success: { _tag, value } as never,
359
+ }),
360
+ onFailure: (value) =>
361
+ encodeFFailure({
362
+ _tag: "F.Failure",
363
+ id,
364
+ failure: { _tag, value } as never,
365
+ }),
366
+ }),
367
+ Effect.andThen((v) =>
368
+ Effect.try({
369
+ try: () => socket.send(v),
370
+ catch: () => {},
371
+ }),
372
+ ),
373
+ this.provideActor(currentClient),
374
+ Boundary.span("handler", import.meta.url, {
375
+ attributes: { _tag, ...sessionAttributes(session) },
376
+ kind: "server",
377
+ parent,
378
+ links,
379
+ }),
380
+ )
381
+ }).pipe(Boundary.span("socket-message", import.meta.url), this.run)
382
+ }
383
+
384
+ override webSocketClose(socket: WebSocket, _code: number, _reason: string, _wasClean: boolean) {
385
+ Effect.gen({ self: this }, function* () {
386
+ const entry = yield* this.directory.entry(socket).pipe(
387
+ Effect.catchTags({
388
+ NoSuchElementError: () => Effect.undefined,
389
+ }),
390
+ )
391
+ if (!entry) {
392
+ return
393
+ }
394
+ const {
395
+ client: { session },
396
+ handle: currentClient,
397
+ } = entry
398
+ yield* Effect.annotateCurrentSpan(sessionAttributes(session))
399
+ yield* this.directory.unregister(socket)
400
+ yield* onDisconnect.pipe(
401
+ this.provideActor(currentClient),
402
+ Boundary.span("disconnect", import.meta.url, {
403
+ attributes: sessionAttributes(session),
404
+ links: [sessionLink(session)],
405
+ }),
406
+ )
407
+ }).pipe(Boundary.span("socket-close", import.meta.url), this.run)
408
+ }
409
+
410
+ override webSocketError(socket: WebSocket, cause: unknown) {
411
+ Effect.gen({ self: this }, function* () {
412
+ const {
413
+ client: { session },
414
+ handle: currentClient,
415
+ } = yield* this.directory.entry(socket)
416
+ yield* Effect.annotateCurrentSpan(sessionAttributes(session))
417
+ yield* this.directory.unregister(socket)
418
+ yield* onDisconnect.pipe(
419
+ this.provideActor(currentClient),
420
+ Boundary.span("disconnect", import.meta.url, {
421
+ attributes: sessionAttributes(session),
422
+ links: [sessionLink(session)],
423
+ }),
424
+ )
425
+ yield* Effect.annotateLogs(Effect.logDebug("SocketErrored"), { cause })
426
+ }).pipe(Boundary.span("socket-error", import.meta.url), this.run)
427
+ }
428
+
429
+ async rpc<K extends keyof Internal>(
430
+ method: K,
431
+ payload: Internal[K]["payload"]["Type"],
432
+ ): Promise<Exit.Exit<Internal[K]["success"]["Type"], Internal[K]["failure"]["Type"]>> {
433
+ const handler = internal[method]
434
+ return await handler(payload).pipe(
435
+ this.provideActor(null!),
436
+ Boundary.span("fn-internal", import.meta.url),
437
+ Effect.exit,
438
+ this.run,
439
+ )
440
+ }
441
+
442
+ async proxySendAll<K extends keyof D["events"]>(event: K, payload: S.Struct<D["events"][K]>["Type"]) {
443
+ await Effect.gen(function* () {
444
+ const { clients } = yield* actor
445
+ yield* Effect.forEach(clients, ({ send }) => send(event, payload), { concurrency: "unbounded" })
446
+ }).pipe(this.provideActor(null!), Boundary.span("fn-internal", import.meta.url), this.run)
447
+ }
448
+ }
449
+ }
package/ActorTransport.ts CHANGED
@@ -2,16 +2,18 @@ import { Effect, Schema as S } from "effect"
2
2
 
3
3
  import type { Protocol, ProtocolDefinition } from "./Protocol.ts"
4
4
 
5
- export interface ActorTransport<Raw, AttachmentFields extends S.Struct.Fields, D extends ProtocolDefinition> {
5
+ export interface ActorTransport<Key, Client, AttachmentFields extends S.Struct.Fields, D extends ProtocolDefinition> {
6
+ readonly key: (client: Client) => Key
7
+
6
8
  readonly send: (
7
- transport: Raw,
9
+ client: Client,
8
10
  event: Protocol<D>["Event"]["Type"],
9
- ) => Effect.Effect<void, S.SchemaError, Protocol<D>["Event"]["EncodingServices"]>
11
+ ) => Effect.Effect<void, never, Protocol<D>["Event"]["EncodingServices"]>
10
12
 
11
- readonly close: (transport: Raw) => Effect.Effect<void>
13
+ readonly close: (client: Client) => Effect.Effect<void>
12
14
 
13
15
  readonly snapshot: (
14
- transport: Raw,
16
+ client: Client,
15
17
  attachments: S.Struct<AttachmentFields>["Type"],
16
- ) => Effect.Effect<void, S.SchemaError, S.Struct<AttachmentFields>["EncodingServices"]>
18
+ ) => Effect.Effect<void, never, S.Struct<AttachmentFields>["EncodingServices"]>
17
19
  }
package/Audition.ts CHANGED
@@ -1,80 +1,126 @@
1
- import { Schema as S, Pipeable, Stream, Effect, Function } from "effect"
1
+ import { Schema as S, Pipeable, Stream, Effect, Function, Types } from "effect"
2
2
 
3
- import type { F } from "./F.ts"
4
- import type { ProtocolDefinition } from "./Protocol.ts"
5
-
6
- import { diagnostic } from "./_diagnostic.ts"
7
3
  import * as Client from "./Client.ts"
8
4
  import { type ClientError, AuditionError } from "./errors.ts"
9
-
10
- const { debug, span } = diagnostic("Audition")
5
+ import type { Fn } from "./Fn.ts"
6
+ import type { Methods } from "./Method.ts"
7
+ import type { ProtocolDefinition } from "./Protocol.ts"
11
8
 
12
9
  const TypeId = "~liminal/Audition" as const
13
10
 
14
- export interface Audition<ClientSelf, D extends ProtocolDefinition> extends Pipeable.Pipeable {
11
+ export interface Audition<AuditionSelf, State extends S.Union<ReadonlyArray<S.Top>>, External extends Methods, Event>
12
+ extends Pipeable.Pipeable {
15
13
  readonly [TypeId]: typeof TypeId
16
14
 
17
- readonly events: Stream.Stream<
18
- ReturnType<typeof S.TaggedUnion<D["events"]>>["Type"],
19
- ClientError | S.SchemaError,
20
- ClientSelf
21
- >
15
+ readonly state: Stream.Stream<State["Type"], ClientError | S.SchemaError, AuditionSelf | State["DecodingServices"]>
16
+
17
+ readonly fn: Fn<AuditionSelf, External>
22
18
 
23
- readonly f: F<ClientSelf, D>
19
+ readonly events: Stream.Stream<Event, ClientError | S.SchemaError, AuditionSelf>
24
20
  }
25
21
 
26
- export const empty: Audition<never, never> = {
22
+ type MergeMethods<T extends Methods, U extends Methods> = [keyof T] extends [never]
23
+ ? U
24
+ : { [K in keyof T & keyof U]: Types.Equals<T[K], U[K]> extends true ? T[K] : never }
25
+
26
+ type MergeState<State, D extends ProtocolDefinition> = [State] extends [never]
27
+ ? S.Union<[S.Struct<D["state"]>]>
28
+ : State extends S.Union<ReadonlyArray<S.Top>>
29
+ ? S.Union<[...State["members"], S.Struct<D["state"]>]>
30
+ : never
31
+
32
+ export const empty: Audition<never, never, {}, never> = {
27
33
  [TypeId]: TypeId,
28
34
  pipe() {
29
35
  return Pipeable.pipeArguments(this, arguments)
30
36
  },
37
+ state: Stream.fail(new AuditionError()),
38
+ fn: () => () => new AuditionError(),
31
39
  events: Stream.fail(new AuditionError()),
32
- f: () => () => new AuditionError().asEffect(),
33
40
  }
34
41
 
42
+ export const cycleOn =
43
+ <Event>(predicate: (event: Event) => boolean) =>
44
+ <AuditionSelf, State extends S.Union<ReadonlyArray<S.Top>>, External extends Methods>(
45
+ audition: Audition<AuditionSelf, State, External, Event>,
46
+ ): Audition<AuditionSelf, State, External, Event> => {
47
+ const events = audition.events.pipe(Stream.takeUntil(predicate), Stream.forever)
48
+ const state = audition.state.pipe(Stream.forever)
49
+
50
+ return {
51
+ [TypeId]: TypeId,
52
+ pipe() {
53
+ return Pipeable.pipeArguments(this, arguments)
54
+ },
55
+ events,
56
+ fn: audition.fn,
57
+ state,
58
+ }
59
+ }
60
+
35
61
  export const add: {
36
62
  <ClientSelf, ClientId extends string, ClientD extends ProtocolDefinition>(
37
63
  client: Client.Client<ClientSelf, ClientId, ClientD>,
38
- ): <AuditionSelf, AuditionD extends ProtocolDefinition>(
39
- audition: Audition<AuditionSelf, AuditionD>,
40
- ) => Audition<AuditionSelf | ClientSelf, ProtocolDefinition.Merge<AuditionD, ClientD>>
64
+ ): <AuditionSelf, State extends S.Union<ReadonlyArray<S.Top>> | never, External extends Methods, Event>(
65
+ audition: Audition<AuditionSelf, State, External, Event>,
66
+ ) => Audition<
67
+ AuditionSelf | ClientSelf,
68
+ MergeState<State, ClientD>,
69
+ MergeMethods<External, ClientD["external"]>,
70
+ Event | ReturnType<typeof S.TaggedUnion<ClientD["events"]>>["Type"]
71
+ >
41
72
  <
42
- AuditionClientSelf,
43
- AuditionD extends ProtocolDefinition,
73
+ AuditionSelf,
74
+ State extends S.Union<ReadonlyArray<S.Top>> | never,
75
+ External extends Methods,
76
+ Event,
44
77
  ClientSelf,
45
78
  ClientId extends string,
46
79
  ClientD extends ProtocolDefinition,
47
80
  >(
48
- audition: Audition<AuditionClientSelf, AuditionD>,
81
+ audition: Audition<AuditionSelf, State, External, Event>,
49
82
  client: Client.Client<ClientSelf, ClientId, ClientD>,
50
- ): Audition<AuditionClientSelf | ClientSelf, ProtocolDefinition.Merge<AuditionD, ClientD>>
83
+ ): Audition<
84
+ AuditionSelf | ClientSelf,
85
+ MergeState<State, ClientD>,
86
+ MergeMethods<External, ClientD["external"]>,
87
+ Event | ReturnType<typeof S.TaggedUnion<ClientD["events"]>>["Type"]
88
+ >
51
89
  } = Function.dual(
52
90
  2,
53
91
  <
54
92
  AuditionSelf,
55
- AuditionD extends ProtocolDefinition,
93
+ State extends S.Union<ReadonlyArray<S.Top>>,
94
+ External extends Methods,
95
+ Event,
56
96
  ClientSelf,
57
97
  ClientId extends string,
58
98
  ClientD extends ProtocolDefinition,
59
99
  >(
60
- audition: Audition<AuditionSelf, AuditionD>,
100
+ audition: Audition<AuditionSelf, State, External, Event>,
61
101
  client: Client.Client<ClientSelf, ClientId, ClientD>,
62
- ): Audition<AuditionSelf | ClientSelf, ProtocolDefinition.Merge<AuditionD, ClientD>> => {
63
- const f: F<AuditionSelf | ClientSelf, ProtocolDefinition.Merge<AuditionD, ClientD>> = (method) => (payload) =>
64
- audition
65
- .f(method)(payload)
66
- .pipe(
67
- Effect.catchTag("AuditionError", () => client.f(method)(payload)),
68
- span("f"),
69
- )
102
+ ): Audition<
103
+ AuditionSelf | ClientSelf,
104
+ MergeState<State, ClientD>,
105
+ MergeMethods<External, ClientD["external"]>,
106
+ Event | ReturnType<typeof S.TaggedUnion<ClientD["events"]>>["Type"]
107
+ > => {
108
+ const fn = ((method: string, ...f: [any]) =>
109
+ Effect.fnUntraced(
110
+ function* (payload: any) {
111
+ return yield* audition
112
+ .fn(method)(payload)
113
+ .pipe(Effect.catchTag("AuditionError", () => client.fn(method)(payload)))
114
+ },
115
+ ...f,
116
+ )) as Fn<AuditionSelf | ClientSelf, MergeMethods<External, ClientD["external"]>>
70
117
 
71
118
  const events = audition.events.pipe(
72
- Stream.catchTag("AuditionError", () =>
73
- Effect.succeed(client.events).pipe(
74
- Effect.tap(() => debug("AuditionStaged", { client: client.key })),
75
- Stream.unwrap,
76
- ),
77
- ),
119
+ Stream.catchTag("AuditionError", () => Effect.succeed(client.events).pipe(Stream.unwrap)),
120
+ )
121
+
122
+ const state = audition.state.pipe(
123
+ Stream.catchTag("AuditionError", () => Effect.succeed(client.state).pipe(Stream.unwrap)),
78
124
  )
79
125
 
80
126
  return {
@@ -83,7 +129,8 @@ export const add: {
83
129
  return Pipeable.pipeArguments(this, arguments)
84
130
  },
85
131
  events,
86
- f,
132
+ fn,
133
+ state,
87
134
  }
88
135
  },
89
136
  )