liminal 0.17.15 → 0.17.17
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 -33
- package/{workerd/ActorHandle.ts → ActorHandle.ts} +7 -2
- package/{workerd/WorkerdActorNamespace.ts → ActorNamespace.ts} +46 -32
- package/{workerd/WorkerdActorRuntime.ts → ActorRuntime.ts} +71 -44
- package/ActorTransport.ts +2 -2
- package/Audition.ts +1 -1
- package/BrowserActorNamespace.ts +257 -0
- package/CHANGELOG.md +28 -0
- package/Client.ts +127 -76
- package/ClientDirectory.ts +40 -32
- package/ClientHandle.ts +9 -7
- package/Fn.ts +39 -0
- package/Tracing.ts +11 -3
- package/dist/Actor.d.ts +3 -5
- package/dist/Actor.js +5 -9
- package/dist/Actor.js.map +1 -1
- package/dist/{workerd/ActorHandle.d.ts → ActorHandle.d.ts} +6 -3
- package/dist/ActorHandle.js.map +1 -0
- package/dist/{workerd/WorkerdActorNamespace.d.ts → ActorNamespace.d.ts} +12 -10
- package/dist/{workerd/WorkerdActorNamespace.js → ActorNamespace.js} +16 -10
- package/dist/ActorNamespace.js.map +1 -0
- package/dist/{workerd/WorkerdActorRuntime.d.ts → ActorRuntime.d.ts} +10 -9
- package/dist/{workerd/WorkerdActorRuntime.js → ActorRuntime.js} +40 -34
- package/dist/ActorRuntime.js.map +1 -0
- package/dist/ActorTransport.d.ts +2 -2
- package/dist/Audition.js +1 -1
- 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 +7 -4
- package/dist/Client.js +78 -48
- package/dist/Client.js.map +1 -1
- package/dist/ClientDirectory.d.ts +1 -1
- package/dist/ClientDirectory.js +11 -5
- package/dist/ClientDirectory.js.map +1 -1
- package/dist/ClientHandle.d.ts +5 -4
- package/dist/Fn.d.ts +8 -0
- package/dist/Tracing.js +6 -2
- package/dist/Tracing.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/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 +13 -7
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/experimental/L/append.ts +1 -1
- package/experimental/L/history.ts +1 -1
- package/index.common.ts +12 -0
- package/index.non-workerd.ts +1 -0
- package/index.ts +4 -11
- package/package.json +12 -9
- package/_util/schema.ts +0 -7
- package/browser/BrowserActorNamespace.ts +0 -248
- package/browser/index.ts +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 -133
- 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/ActorHandle.js.map +0 -1
- package/dist/workerd/WorkerdActorNamespace.js.map +0 -1
- package/dist/workerd/WorkerdActorRuntime.js.map +0 -1
- package/dist/workerd/index.d.ts +0 -3
- package/dist/workerd/index.js +0 -4
- package/dist/workerd/index.js.map +0 -1
- package/workerd/index.ts +0 -3
- /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,33 @@
|
|
|
1
1
|
# liminal
|
|
2
2
|
|
|
3
|
+
## 0.17.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#465](https://github.com/crosshatch/liminal/pull/465)
|
|
8
|
+
[`6920795`](https://github.com/crosshatch/liminal/commit/692079548446a9d4274d5a9a4ae4039b3ab3dc91) Thanks
|
|
9
|
+
@harrysolovay! - Continue debugging trusted / CI publishing.
|
|
10
|
+
|
|
11
|
+
- [#465](https://github.com/crosshatch/liminal/pull/465)
|
|
12
|
+
[`6920795`](https://github.com/crosshatch/liminal/commit/692079548446a9d4274d5a9a4ae4039b3ab3dc91) Thanks
|
|
13
|
+
@harrysolovay! - Continue testing changesets configuration tweaks.
|
|
14
|
+
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
[[`b64dcce`](https://github.com/crosshatch/liminal/commit/b64dcce3009984771205d5c31f7f7e14508b2757),
|
|
17
|
+
[`6920795`](https://github.com/crosshatch/liminal/commit/692079548446a9d4274d5a9a4ae4039b3ab3dc91),
|
|
18
|
+
[`6920795`](https://github.com/crosshatch/liminal/commit/692079548446a9d4274d5a9a4ae4039b3ab3dc91)]:
|
|
19
|
+
- liminal-util@0.0.11
|
|
20
|
+
- effect-workerd@0.0.7
|
|
21
|
+
|
|
22
|
+
## 0.17.16
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- b08b6d9: Begin trusted publishing configuration.
|
|
27
|
+
- Updated dependencies [b08b6d9]
|
|
28
|
+
- effect-workerd@0.0.6
|
|
29
|
+
- liminal-util@0.0.10
|
|
30
|
+
|
|
3
31
|
## 0.17.15
|
|
4
32
|
|
|
5
33
|
### 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
|
|
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,
|
|
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<
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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()
|
|
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()
|
|
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(
|
|
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: [
|
|
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(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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.
|
|
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["
|
|
602
|
+
| T["Client"]["EncodingServices"]
|
|
567
603
|
| Reducer.Reducers.Services<Self, Reducers>
|
|
568
|
-
> =>
|
|
569
|
-
|
|
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
|
-
.
|
|
578
|
-
|
|
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
|
-
|
|
582
|
-
Effect.
|
|
583
|
-
|
|
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(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|