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.
- package/Actor.ts +22 -34
- package/ActorHandle.ts +34 -0
- package/ActorNamespace.ts +188 -0
- package/ActorRuntime.ts +449 -0
- package/ActorTransport.ts +8 -6
- package/Audition.ts +87 -40
- package/BrowserActorNamespace.ts +257 -0
- package/CHANGELOG.md +17 -0
- package/Client.ts +374 -197
- package/ClientDirectory.ts +71 -49
- package/ClientHandle.ts +9 -7
- package/ClientHandleEncoders.ts +15 -0
- package/Fn.ts +94 -0
- package/Method.ts +11 -21
- package/Protocol.ts +44 -36
- package/Reducer.ts +22 -0
- package/Tracing.ts +45 -0
- package/dist/Actor.d.ts +3 -5
- package/dist/Actor.js +5 -9
- package/dist/Actor.js.map +1 -1
- package/dist/ActorHandle.d.ts +12 -0
- package/dist/ActorHandle.js +4 -0
- package/dist/ActorHandle.js.map +1 -0
- package/dist/ActorNamespace.d.ts +25 -0
- package/dist/ActorNamespace.js +60 -0
- package/dist/ActorNamespace.js.map +1 -0
- package/dist/ActorRuntime.d.ts +20 -0
- package/dist/ActorRuntime.js +210 -0
- package/dist/ActorRuntime.js.map +1 -0
- package/dist/ActorTransport.d.ts +5 -4
- package/dist/Audition.d.ts +16 -9
- package/dist/Audition.js +25 -9
- package/dist/Audition.js.map +1 -1
- package/dist/BrowserActorNamespace.d.ts +39 -0
- package/dist/BrowserActorNamespace.js +134 -0
- package/dist/BrowserActorNamespace.js.map +1 -0
- package/dist/Client.d.ts +26 -16
- package/dist/Client.js +186 -109
- package/dist/Client.js.map +1 -1
- package/dist/ClientDirectory.d.ts +15 -7
- package/dist/ClientDirectory.js +32 -23
- package/dist/ClientDirectory.js.map +1 -1
- package/dist/ClientHandle.d.ts +5 -4
- package/dist/ClientHandleEncoders.d.ts +7 -0
- package/dist/ClientHandleEncoders.js +2 -0
- package/dist/ClientHandleEncoders.js.map +1 -0
- package/dist/Fn.d.ts +24 -0
- package/dist/Fn.js +2 -0
- package/dist/Fn.js.map +1 -0
- package/dist/Method.d.ts +9 -14
- package/dist/Method.js +0 -1
- package/dist/Method.js.map +1 -1
- package/dist/Protocol.d.ts +19 -22
- package/dist/Protocol.js +20 -15
- package/dist/Protocol.js.map +1 -1
- package/dist/Reducer.d.ts +11 -0
- package/dist/Reducer.js +2 -0
- package/dist/Reducer.js.map +1 -0
- package/dist/Tracing.d.ts +37 -0
- package/dist/Tracing.js +33 -0
- package/dist/Tracing.js.map +1 -0
- package/dist/errors.d.ts +0 -4
- package/dist/errors.js.map +1 -1
- package/dist/experimental/L/append.js +1 -1
- package/dist/experimental/L/append.js.map +1 -1
- package/dist/experimental/L/history.js +1 -1
- package/dist/experimental/L/history.js.map +1 -1
- package/dist/experimental/TaggedTemplateFunction.js +1 -1
- package/dist/experimental/TaggedTemplateFunction.js.map +1 -1
- package/dist/index.common.d.ts +12 -0
- package/dist/index.common.js +13 -0
- package/dist/index.common.js.map +1 -0
- package/dist/index.d.ts +4 -11
- package/dist/index.js +4 -11
- package/dist/index.js.map +1 -1
- package/dist/index.non-workerd.d.ts +1 -0
- package/dist/index.non-workerd.js +2 -0
- package/dist/index.non-workerd.js.map +1 -0
- package/dist/package.json +20 -19
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/errors.ts +0 -6
- package/experimental/L/append.ts +1 -1
- package/experimental/L/history.ts +1 -1
- package/experimental/TaggedTemplateFunction.ts +1 -1
- package/index.common.ts +12 -0
- package/index.non-workerd.ts +1 -0
- package/index.ts +4 -11
- package/package.json +11 -23
- package/tsconfig.json +1 -1
- package/vitest.config.ts +7 -0
- package/Accumulator.ts +0 -103
- package/F.ts +0 -10
- package/_diagnostic.ts +0 -3
- package/_util/Mutex.ts +0 -13
- package/_util/schema.ts +0 -7
- package/browser/BrowserActorNamespace.ts +0 -213
- package/browser/index.ts +0 -1
- package/dist/Accumulator.d.ts +0 -22
- package/dist/Accumulator.js +0 -37
- package/dist/Accumulator.js.map +0 -1
- package/dist/F.d.ts +0 -4
- package/dist/F.js +0 -2
- package/dist/F.js.map +0 -1
- package/dist/_diagnostic.d.ts +0 -4
- package/dist/_diagnostic.js +0 -3
- package/dist/_diagnostic.js.map +0 -1
- package/dist/_util/Mutex.d.ts +0 -7
- package/dist/_util/Mutex.js +0 -9
- package/dist/_util/Mutex.js.map +0 -1
- package/dist/_util/schema.d.ts +0 -4
- package/dist/_util/schema.js +0 -5
- package/dist/_util/schema.js.map +0 -1
- package/dist/browser/BrowserActorNamespace.d.ts +0 -16
- package/dist/browser/BrowserActorNamespace.js +0 -112
- package/dist/browser/BrowserActorNamespace.js.map +0 -1
- package/dist/browser/index.d.ts +0 -1
- package/dist/browser/index.js +0 -2
- package/dist/browser/index.js.map +0 -1
- package/dist/workerd/WorkerdActorNamespace.d.ts +0 -25
- package/dist/workerd/WorkerdActorNamespace.js +0 -146
- package/dist/workerd/WorkerdActorNamespace.js.map +0 -1
- package/dist/workerd/index.d.ts +0 -1
- package/dist/workerd/index.js +0 -2
- package/dist/workerd/index.js.map +0 -1
- package/workerd/WorkerdActorNamespace.ts +0 -362
- package/workerd/index.ts +0 -1
package/ActorRuntime.ts
ADDED
|
@@ -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<
|
|
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
|
-
|
|
9
|
+
client: Client,
|
|
8
10
|
event: Protocol<D>["Event"]["Type"],
|
|
9
|
-
) => Effect.Effect<void,
|
|
11
|
+
) => Effect.Effect<void, never, Protocol<D>["Event"]["EncodingServices"]>
|
|
10
12
|
|
|
11
|
-
readonly close: (
|
|
13
|
+
readonly close: (client: Client) => Effect.Effect<void>
|
|
12
14
|
|
|
13
15
|
readonly snapshot: (
|
|
14
|
-
|
|
16
|
+
client: Client,
|
|
15
17
|
attachments: S.Struct<AttachmentFields>["Type"],
|
|
16
|
-
) => Effect.Effect<void,
|
|
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
|
-
|
|
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<
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
19
|
+
readonly events: Stream.Stream<Event, ClientError | S.SchemaError, AuditionSelf>
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
|
|
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,
|
|
39
|
-
audition: Audition<AuditionSelf,
|
|
40
|
-
) => Audition<
|
|
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
|
-
|
|
43
|
-
|
|
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<
|
|
81
|
+
audition: Audition<AuditionSelf, State, External, Event>,
|
|
49
82
|
client: Client.Client<ClientSelf, ClientId, ClientD>,
|
|
50
|
-
): Audition<
|
|
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
|
-
|
|
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,
|
|
100
|
+
audition: Audition<AuditionSelf, State, External, Event>,
|
|
61
101
|
client: Client.Client<ClientSelf, ClientId, ClientD>,
|
|
62
|
-
): Audition<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
132
|
+
fn,
|
|
133
|
+
state,
|
|
87
134
|
}
|
|
88
135
|
},
|
|
89
136
|
)
|