@livestore/utils 0.2.0-dev.2 → 0.3.0-dev.0
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/dist/.tsbuildinfo.json +1 -1
- package/dist/NoopTracer.js +1 -1
- package/dist/NoopTracer.js.map +1 -1
- package/dist/base64.d.ts +1 -1
- package/dist/base64.d.ts.map +1 -1
- package/dist/base64.js.map +1 -1
- package/dist/effect/BucketQueue.d.ts +7 -0
- package/dist/effect/BucketQueue.d.ts.map +1 -0
- package/dist/effect/BucketQueue.js +16 -0
- package/dist/effect/BucketQueue.js.map +1 -0
- package/dist/effect/Effect.d.ts +6 -1
- package/dist/effect/Effect.d.ts.map +1 -1
- package/dist/effect/Effect.js +24 -10
- package/dist/effect/Effect.js.map +1 -1
- package/dist/effect/Logger.d.ts +3 -0
- package/dist/effect/Logger.d.ts.map +1 -0
- package/dist/effect/Logger.js +10 -0
- package/dist/effect/Logger.js.map +1 -0
- package/dist/effect/Schema/index.d.ts.map +1 -1
- package/dist/effect/Schema/index.js +1 -1
- package/dist/effect/Schema/index.js.map +1 -1
- package/dist/effect/Schema/msgpack.d.ts +1 -1
- package/dist/effect/Schema/msgpack.d.ts.map +1 -1
- package/dist/effect/TaskTracing.d.ts +5 -0
- package/dist/effect/TaskTracing.d.ts.map +1 -0
- package/dist/effect/TaskTracing.js +38 -0
- package/dist/effect/TaskTracing.js.map +1 -0
- package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts +13 -0
- package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -0
- package/dist/effect/WebChannel/broadcastChannelWithAck.js +88 -0
- package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -0
- package/dist/effect/WebChannel/common.d.ts +16 -0
- package/dist/effect/WebChannel/common.d.ts.map +1 -0
- package/dist/effect/WebChannel/common.js +4 -0
- package/dist/effect/WebChannel/common.js.map +1 -0
- package/dist/effect/WebChannel.d.ts +55 -25
- package/dist/effect/WebChannel.d.ts.map +1 -1
- package/dist/effect/WebChannel.js +133 -15
- package/dist/effect/WebChannel.js.map +1 -1
- package/dist/effect/WebLock.d.ts +4 -0
- package/dist/effect/WebLock.d.ts.map +1 -1
- package/dist/effect/WebLock.js +61 -0
- package/dist/effect/WebLock.js.map +1 -1
- package/dist/effect/WebSocket.d.ts +19 -0
- package/dist/effect/WebSocket.d.ts.map +1 -0
- package/dist/effect/WebSocket.js +53 -0
- package/dist/effect/WebSocket.js.map +1 -0
- package/dist/effect/index.d.ts +6 -2
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +10 -2
- package/dist/effect/index.js.map +1 -1
- package/dist/env.d.ts +5 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +25 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -12
- package/dist/index.js.map +1 -1
- package/dist/nanoid/index.browser.d.ts +2 -0
- package/dist/nanoid/index.browser.d.ts.map +1 -0
- package/dist/nanoid/index.browser.js +3 -0
- package/dist/nanoid/index.browser.js.map +1 -0
- package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +5 -0
- package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -0
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js +55 -0
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -0
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +10 -0
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -0
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js +46 -0
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -0
- package/dist/node/mod.d.ts +22 -0
- package/dist/node/mod.d.ts.map +1 -0
- package/dist/node/mod.js +83 -0
- package/dist/node/mod.js.map +1 -0
- package/dist/node-vitest/mod.d.ts +2 -0
- package/dist/node-vitest/mod.d.ts.map +1 -0
- package/dist/node-vitest/mod.js +2 -0
- package/dist/node-vitest/mod.js.map +1 -0
- package/dist/node-vitest/polyfill.d.ts +2 -0
- package/dist/node-vitest/polyfill.d.ts.map +1 -0
- package/dist/node-vitest/polyfill.js +3 -0
- package/dist/node-vitest/polyfill.js.map +1 -0
- package/dist/object/index.d.ts.map +1 -1
- package/dist/object/pick.d.ts.map +1 -1
- package/package.json +50 -15
- package/src/NoopTracer.ts +1 -1
- package/src/base64.ts +1 -1
- package/src/effect/BucketQueue.ts +22 -0
- package/src/effect/Effect.ts +41 -17
- package/src/effect/Logger.ts +17 -0
- package/src/effect/Schema/index.ts +1 -1
- package/src/effect/TaskTracing.ts +43 -0
- package/src/effect/WebChannel/broadcastChannelWithAck.ts +126 -0
- package/src/effect/WebChannel/common.ts +17 -0
- package/src/effect/WebChannel.ts +223 -49
- package/src/effect/WebLock.ts +75 -0
- package/src/effect/WebSocket.ts +72 -0
- package/src/effect/index.ts +14 -1
- package/src/env.ts +31 -0
- package/src/index.ts +3 -11
- package/src/nanoid/index.browser.ts +2 -0
- package/src/node/ChildProcessRunner/ChildProcessRunner.ts +63 -0
- package/src/node/ChildProcessRunner/ChildProcessWorker.ts +66 -0
- package/src/node/mod.ts +115 -0
- package/src/node-vitest/mod.ts +1 -0
- package/src/node-vitest/polyfill.ts +1 -0
- package/tmp/effect-deferred-repro.ts +29 -0
- package/tmp/effect-semaphore-repro.ts +93 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Predicate } from 'effect'
|
|
2
|
+
import * as Context from 'effect/Context'
|
|
3
|
+
import * as Effect from 'effect/Effect'
|
|
4
|
+
import { pipe } from 'effect/Function'
|
|
5
|
+
import * as Layer from 'effect/Layer'
|
|
6
|
+
import * as Tracer from 'effect/Tracer'
|
|
7
|
+
|
|
8
|
+
export const withAsyncTaggingTracing =
|
|
9
|
+
(makeTrace: (name: string) => { run: (fn: any) => any }) =>
|
|
10
|
+
<A, E, R>(eff: Effect.Effect<A, E, R>) => {
|
|
11
|
+
if (Predicate.hasProperty(console, 'createTask') === false) {
|
|
12
|
+
return eff
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const makeTracer = Effect.gen(function* () {
|
|
16
|
+
const oldTracer = yield* Effect.tracer
|
|
17
|
+
return Tracer.make({
|
|
18
|
+
span: (name, ...args) => {
|
|
19
|
+
const span = oldTracer.span(name, ...args)
|
|
20
|
+
const trace = makeTrace(name)
|
|
21
|
+
;(span as any).runInTask = (f: any) => trace.run(f)
|
|
22
|
+
return span
|
|
23
|
+
},
|
|
24
|
+
context: (f, fiber) => {
|
|
25
|
+
const maybeParentSpan = Context.getOption(Tracer.ParentSpan)(fiber.currentContext)
|
|
26
|
+
|
|
27
|
+
if (maybeParentSpan._tag === 'None') return oldTracer.context(f, fiber)
|
|
28
|
+
const parentSpan = maybeParentSpan.value
|
|
29
|
+
if (parentSpan._tag === 'ExternalSpan') return oldTracer.context(f, fiber)
|
|
30
|
+
const span = parentSpan
|
|
31
|
+
if ('runInTask' in span && typeof span.runInTask === 'function') {
|
|
32
|
+
return span.runInTask(() => oldTracer.context(f, fiber))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return oldTracer.context(f, fiber)
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const withTracerLayer = pipe(makeTracer, Effect.map(Layer.setTracer), Layer.unwrapEffect)
|
|
41
|
+
|
|
42
|
+
return Effect.provide(eff, withTracerLayer)
|
|
43
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Scope } from 'effect'
|
|
2
|
+
import { Deferred, Effect, Predicate, Queue, Schema, Stream } from 'effect'
|
|
3
|
+
|
|
4
|
+
import type { WebChannel } from './common.js'
|
|
5
|
+
import { WebChannelSymbol } from './common.js'
|
|
6
|
+
|
|
7
|
+
const ConnectMessage = Schema.TaggedStruct('ConnectMessage', {
|
|
8
|
+
from: Schema.String,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const ConnectAckMessage = Schema.TaggedStruct('ConnectAckMessage', {
|
|
12
|
+
from: Schema.String,
|
|
13
|
+
to: Schema.String,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const DisconnectMessage = Schema.TaggedStruct('DisconnectMessage', {
|
|
17
|
+
from: Schema.String,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const PayloadMessage = Schema.TaggedStruct('PayloadMessage', {
|
|
21
|
+
from: Schema.String,
|
|
22
|
+
to: Schema.String,
|
|
23
|
+
payload: Schema.Any,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const Message = Schema.Union(ConnectMessage, ConnectAckMessage, DisconnectMessage, PayloadMessage)
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Same as `broadcastChannel`, but with a queue in between to guarantee message delivery and meant
|
|
30
|
+
* for 1:1 connections.
|
|
31
|
+
*/
|
|
32
|
+
export const broadcastChannelWithAck = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
|
|
33
|
+
channelName,
|
|
34
|
+
listenSchema,
|
|
35
|
+
sendSchema,
|
|
36
|
+
}: {
|
|
37
|
+
channelName: string
|
|
38
|
+
listenSchema: Schema.Schema<MsgListen, MsgListenEncoded>
|
|
39
|
+
sendSchema: Schema.Schema<MsgSend, MsgSendEncoded>
|
|
40
|
+
}): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
|
|
41
|
+
Effect.gen(function* () {
|
|
42
|
+
const channel = new BroadcastChannel(channelName)
|
|
43
|
+
const messageQueue = yield* Queue.unbounded<MsgSend>()
|
|
44
|
+
const connectionId = crypto.randomUUID()
|
|
45
|
+
|
|
46
|
+
const peerIdRef = { current: undefined as undefined | string }
|
|
47
|
+
const connectedLatch = yield* Effect.makeLatch(false)
|
|
48
|
+
const supportsTransferables = false
|
|
49
|
+
|
|
50
|
+
const postMessage = (msg: typeof Message.Type) => channel.postMessage(Schema.encodeSync(Message)(msg))
|
|
51
|
+
|
|
52
|
+
const send = (message: MsgSend) =>
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
yield* connectedLatch.await
|
|
55
|
+
|
|
56
|
+
const payload = yield* Schema.encode(sendSchema)(message)
|
|
57
|
+
postMessage(PayloadMessage.make({ from: connectionId, to: peerIdRef.current!, payload }))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
|
|
61
|
+
Stream.map(({ data }) => data),
|
|
62
|
+
Stream.map(Schema.decodeOption(Message)),
|
|
63
|
+
Stream.filterMap((_) => _),
|
|
64
|
+
Stream.mapEffect((data) =>
|
|
65
|
+
Effect.gen(function* () {
|
|
66
|
+
switch (data._tag) {
|
|
67
|
+
// Case: other side sends connect message (because otherside wasn't yet online when this side send their connect message)
|
|
68
|
+
case 'ConnectMessage': {
|
|
69
|
+
peerIdRef.current = data.from
|
|
70
|
+
postMessage(ConnectAckMessage.make({ from: connectionId, to: data.from }))
|
|
71
|
+
yield* connectedLatch.open
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
// Case: other side sends connect-ack message (because otherside was already online when this side connected)
|
|
75
|
+
case 'ConnectAckMessage': {
|
|
76
|
+
if (data.to === connectionId) {
|
|
77
|
+
peerIdRef.current = data.from
|
|
78
|
+
yield* connectedLatch.open
|
|
79
|
+
}
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
case 'DisconnectMessage': {
|
|
83
|
+
if (data.from === peerIdRef.current) {
|
|
84
|
+
peerIdRef.current = undefined
|
|
85
|
+
yield* connectedLatch.close
|
|
86
|
+
yield* establishConnection
|
|
87
|
+
}
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
case 'PayloadMessage': {
|
|
91
|
+
if (data.to === connectionId) {
|
|
92
|
+
return Schema.decodeEither(listenSchema)(data.payload)
|
|
93
|
+
}
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
|
+
Stream.filter(Predicate.isNotUndefined),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const establishConnection = Effect.gen(function* () {
|
|
103
|
+
postMessage(ConnectMessage.make({ from: connectionId }))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
yield* establishConnection
|
|
107
|
+
|
|
108
|
+
yield* Effect.addFinalizer(() =>
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
postMessage(DisconnectMessage.make({ from: connectionId }))
|
|
111
|
+
channel.close()
|
|
112
|
+
yield* Queue.shutdown(messageQueue)
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const closedDeferred = yield* Deferred.make<void>()
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
120
|
+
send,
|
|
121
|
+
listen,
|
|
122
|
+
closedDeferred,
|
|
123
|
+
schema: { listen: listenSchema, send: sendSchema },
|
|
124
|
+
supportsTransferables,
|
|
125
|
+
}
|
|
126
|
+
}).pipe(Effect.withSpan(`WebChannel:broadcastChannelWithAck(${channelName})`))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Deferred, Effect, Either, ParseResult, Schema, Stream } from 'effect'
|
|
2
|
+
import { Predicate } from 'effect'
|
|
3
|
+
|
|
4
|
+
export const WebChannelSymbol = Symbol('WebChannel')
|
|
5
|
+
export type WebChannelSymbol = typeof WebChannelSymbol
|
|
6
|
+
|
|
7
|
+
export const isWebChannel = <MsgListen, MsgSend>(value: unknown): value is WebChannel<MsgListen, MsgSend> =>
|
|
8
|
+
typeof value === 'object' && value !== null && Predicate.hasProperty(value, WebChannelSymbol)
|
|
9
|
+
|
|
10
|
+
export interface WebChannel<MsgListen, MsgSend, E = never> {
|
|
11
|
+
readonly [WebChannelSymbol]: unknown
|
|
12
|
+
send: (a: MsgSend) => Effect.Effect<void, ParseResult.ParseError | E>
|
|
13
|
+
listen: Stream.Stream<Either.Either<MsgListen, ParseResult.ParseError>, E>
|
|
14
|
+
supportsTransferables: boolean
|
|
15
|
+
closedDeferred: Deferred.Deferred<void>
|
|
16
|
+
schema: { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
|
|
17
|
+
}
|
package/src/effect/WebChannel.ts
CHANGED
|
@@ -1,116 +1,290 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { Deferred, Effect, Either,
|
|
1
|
+
import type { Scope } from 'effect'
|
|
2
|
+
import { Deferred, Effect, Either, Option, Predicate, Queue } from 'effect'
|
|
3
3
|
|
|
4
4
|
import * as Schema from './Schema/index.js'
|
|
5
|
+
import * as Stream from './Stream.js'
|
|
6
|
+
import { type WebChannel, WebChannelSymbol } from './WebChannel/common.js'
|
|
5
7
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
closedDeferred: Deferred.Deferred<void>
|
|
10
|
-
}
|
|
8
|
+
export * from './WebChannel/broadcastChannelWithAck.js'
|
|
9
|
+
|
|
10
|
+
export * from './WebChannel/common.js'
|
|
11
11
|
|
|
12
|
-
export const broadcastChannel = <
|
|
12
|
+
export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
|
|
13
13
|
channelName,
|
|
14
|
-
|
|
15
|
-
sendSchema,
|
|
14
|
+
schema: inputSchema,
|
|
16
15
|
}: {
|
|
17
16
|
channelName: string
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}): Effect.Effect<WebChannel<MsgIn, MsgOut>, never, Scope.Scope> =>
|
|
17
|
+
schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
|
|
18
|
+
}): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
|
|
21
19
|
Effect.gen(function* () {
|
|
20
|
+
const schema = mapSchema(inputSchema)
|
|
21
|
+
|
|
22
22
|
const channel = new BroadcastChannel(channelName)
|
|
23
23
|
|
|
24
24
|
yield* Effect.addFinalizer(() => Effect.try(() => channel.close()).pipe(Effect.ignoreLogged))
|
|
25
25
|
|
|
26
|
-
const send = (message:
|
|
26
|
+
const send = (message: MsgSend) =>
|
|
27
27
|
Effect.gen(function* () {
|
|
28
|
-
const messageEncoded = yield* Schema.encode(
|
|
28
|
+
const messageEncoded = yield* Schema.encode(schema.send)(message)
|
|
29
29
|
channel.postMessage(messageEncoded)
|
|
30
30
|
})
|
|
31
31
|
|
|
32
32
|
// TODO also listen to `messageerror` in parallel
|
|
33
33
|
const listen = Stream.fromEventListener<MessageEvent>(channel, 'message').pipe(
|
|
34
|
-
Stream.map((_) => Schema.decodeEither(
|
|
34
|
+
Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
const closedDeferred = yield* Deferred.make<void>()
|
|
38
|
+
const supportsTransferables = false
|
|
38
39
|
|
|
39
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
42
|
+
send,
|
|
43
|
+
listen,
|
|
44
|
+
closedDeferred,
|
|
45
|
+
schema,
|
|
46
|
+
supportsTransferables,
|
|
47
|
+
}
|
|
40
48
|
}).pipe(Effect.withSpan(`WebChannel:broadcastChannel(${channelName})`))
|
|
41
49
|
|
|
42
|
-
export const windowChannel = <
|
|
50
|
+
export const windowChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
|
|
43
51
|
window,
|
|
44
52
|
targetOrigin = '*',
|
|
45
|
-
|
|
46
|
-
sendSchema,
|
|
53
|
+
schema: inputSchema,
|
|
47
54
|
}: {
|
|
48
55
|
window: Window
|
|
49
56
|
targetOrigin?: string
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}): Effect.Effect<WebChannel<MsgIn, MsgOut>, never, Scope.Scope> =>
|
|
57
|
+
schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
|
|
58
|
+
}): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope> =>
|
|
53
59
|
Effect.gen(function* () {
|
|
54
|
-
const
|
|
60
|
+
const schema = mapSchema(inputSchema)
|
|
61
|
+
|
|
62
|
+
const send = (message: MsgSend) =>
|
|
55
63
|
Effect.gen(function* () {
|
|
56
|
-
const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(
|
|
64
|
+
const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
|
|
57
65
|
window.postMessage(messageEncoded, targetOrigin, transferables)
|
|
58
66
|
})
|
|
59
67
|
|
|
60
68
|
const listen = Stream.fromEventListener<MessageEvent>(window, 'message').pipe(
|
|
61
|
-
Stream.map((_) => Schema.decodeEither(
|
|
69
|
+
Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
|
|
62
70
|
)
|
|
63
71
|
|
|
64
72
|
const closedDeferred = yield* Deferred.make<void>()
|
|
73
|
+
const supportsTransferables = true
|
|
65
74
|
|
|
66
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
77
|
+
send,
|
|
78
|
+
listen,
|
|
79
|
+
closedDeferred,
|
|
80
|
+
schema,
|
|
81
|
+
supportsTransferables,
|
|
82
|
+
}
|
|
67
83
|
}).pipe(Effect.withSpan(`WebChannel:windowChannel`))
|
|
68
84
|
|
|
69
|
-
export const messagePortChannel
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}:
|
|
74
|
-
|
|
75
|
-
listenSchema: Schema.Schema<MsgIn, MsgInEncoded>
|
|
76
|
-
sendSchema: Schema.Schema<MsgOut, MsgOutEncoded>
|
|
77
|
-
}): Effect.Effect<WebChannel<MsgIn, MsgOut>, never, Scope.Scope> =>
|
|
85
|
+
export const messagePortChannel: {
|
|
86
|
+
<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
|
|
87
|
+
port: MessagePort
|
|
88
|
+
schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
|
|
89
|
+
}): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
|
|
90
|
+
} = ({ port, schema: inputSchema }) =>
|
|
78
91
|
Effect.gen(function* () {
|
|
79
|
-
const
|
|
92
|
+
const schema = mapSchema(inputSchema)
|
|
93
|
+
|
|
94
|
+
const send = (message: any) =>
|
|
80
95
|
Effect.gen(function* () {
|
|
81
|
-
const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(
|
|
96
|
+
const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(schema.send)(message)
|
|
82
97
|
port.postMessage(messageEncoded, transferables)
|
|
83
98
|
})
|
|
84
99
|
|
|
85
100
|
const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
|
|
86
|
-
Stream.map((_) => Schema.decodeEither(
|
|
101
|
+
Stream.map((_) => Schema.decodeEither(schema.listen)(_.data)),
|
|
87
102
|
)
|
|
88
103
|
|
|
89
104
|
port.start()
|
|
90
105
|
|
|
91
106
|
const closedDeferred = yield* Deferred.make<void>()
|
|
107
|
+
const supportsTransferables = true
|
|
92
108
|
|
|
93
109
|
yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
|
|
94
110
|
|
|
95
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
113
|
+
send,
|
|
114
|
+
listen,
|
|
115
|
+
closedDeferred,
|
|
116
|
+
schema,
|
|
117
|
+
supportsTransferables,
|
|
118
|
+
}
|
|
96
119
|
}).pipe(Effect.withSpan(`WebChannel:messagePortChannel`))
|
|
97
120
|
|
|
98
|
-
export const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
>
|
|
121
|
+
export const messagePortChannelWithAck: {
|
|
122
|
+
<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(args: {
|
|
123
|
+
port: MessagePort
|
|
124
|
+
schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>
|
|
125
|
+
}): Effect.Effect<WebChannel<MsgListen, MsgSend>, never, Scope.Scope>
|
|
126
|
+
} = ({ port, schema: inputSchema }) =>
|
|
103
127
|
Effect.gen(function* () {
|
|
104
|
-
const
|
|
105
|
-
|
|
128
|
+
const schema = mapSchema(inputSchema)
|
|
129
|
+
|
|
130
|
+
type RequestId = string
|
|
131
|
+
const requestAckMap = new Map<RequestId, Deferred.Deferred<void>>()
|
|
132
|
+
|
|
133
|
+
const ChannelRequest = Schema.TaggedStruct('ChannelRequest', {
|
|
134
|
+
id: Schema.String,
|
|
135
|
+
payload: Schema.Union(schema.listen, schema.send),
|
|
136
|
+
})
|
|
137
|
+
const ChannelRequestAck = Schema.TaggedStruct('ChannelRequestAck', {
|
|
138
|
+
reqId: Schema.String,
|
|
139
|
+
})
|
|
140
|
+
const ChannelMessage = Schema.Union(ChannelRequest, ChannelRequestAck)
|
|
141
|
+
type ChannelMessage = typeof ChannelMessage.Type
|
|
142
|
+
|
|
143
|
+
const send = (message: any) =>
|
|
144
|
+
Effect.gen(function* () {
|
|
145
|
+
const id = crypto.randomUUID()
|
|
146
|
+
const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(ChannelMessage)({
|
|
147
|
+
_tag: 'ChannelRequest',
|
|
148
|
+
id,
|
|
149
|
+
payload: message,
|
|
150
|
+
})
|
|
106
151
|
|
|
107
|
-
|
|
152
|
+
const ack = yield* Deferred.make<void>()
|
|
153
|
+
requestAckMap.set(id, ack)
|
|
154
|
+
|
|
155
|
+
port.postMessage(messageEncoded, transferables)
|
|
156
|
+
|
|
157
|
+
yield* ack
|
|
158
|
+
|
|
159
|
+
requestAckMap.delete(id)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const listen = Stream.fromEventListener<MessageEvent>(port, 'message').pipe(
|
|
163
|
+
Stream.map((_) => Schema.decodeEither(ChannelMessage)(_.data)),
|
|
164
|
+
Stream.tap((msg) =>
|
|
165
|
+
Effect.gen(function* () {
|
|
166
|
+
if (msg._tag === 'Right') {
|
|
167
|
+
if (msg.right._tag === 'ChannelRequestAck') {
|
|
168
|
+
yield* Deferred.succeed(requestAckMap.get(msg.right.reqId)!, void 0)
|
|
169
|
+
} else if (msg.right._tag === 'ChannelRequest') {
|
|
170
|
+
port.postMessage(Schema.encodeSync(ChannelMessage)({ _tag: 'ChannelRequestAck', reqId: msg.right.id }))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}),
|
|
174
|
+
),
|
|
175
|
+
Stream.filterMap((msg) =>
|
|
176
|
+
msg._tag === 'Left'
|
|
177
|
+
? Option.some(msg as any)
|
|
178
|
+
: msg.right._tag === 'ChannelRequest'
|
|
179
|
+
? Option.some(Either.right(msg.right.payload))
|
|
180
|
+
: Option.none(),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
port.start()
|
|
185
|
+
|
|
186
|
+
const closedDeferred = yield* Deferred.make<void>()
|
|
187
|
+
const supportsTransferables = true
|
|
188
|
+
|
|
189
|
+
yield* Effect.addFinalizer(() => Effect.try(() => port.close()).pipe(Effect.ignoreLogged))
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
193
|
+
send,
|
|
194
|
+
listen,
|
|
195
|
+
closedDeferred,
|
|
196
|
+
schema,
|
|
197
|
+
supportsTransferables,
|
|
198
|
+
}
|
|
199
|
+
}).pipe(Effect.withSpan(`WebChannel:messagePortChannelWithAck`))
|
|
200
|
+
|
|
201
|
+
export type InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded> =
|
|
202
|
+
| Schema.Schema<MsgListen | MsgSend, MsgListenEncoded | MsgSendEncoded>
|
|
203
|
+
| {
|
|
204
|
+
listen: Schema.Schema<MsgListen, MsgListenEncoded>
|
|
205
|
+
send: Schema.Schema<MsgSend, MsgSendEncoded>
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const mapSchema = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>(
|
|
209
|
+
schema: InputSchema<MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>,
|
|
210
|
+
): {
|
|
211
|
+
listen: Schema.Schema<MsgListen, MsgListenEncoded>
|
|
212
|
+
send: Schema.Schema<MsgSend, MsgSendEncoded>
|
|
213
|
+
} =>
|
|
214
|
+
Predicate.hasProperty(schema, 'send') && Predicate.hasProperty(schema, 'listen')
|
|
215
|
+
? { send: schema.send, listen: schema.listen }
|
|
216
|
+
: ({ send: schema, listen: schema } as any)
|
|
217
|
+
|
|
218
|
+
export type QueueChannelProxy<MsgListen, MsgSend> = {
|
|
219
|
+
/** Only meant to be used externally */
|
|
220
|
+
webChannel: WebChannel<MsgListen, MsgSend>
|
|
221
|
+
/**
|
|
222
|
+
* Meant to be listened to (e.g. via `Stream.fromQueue`) for messages that have been sent
|
|
223
|
+
* via `webChannel.send()`.
|
|
224
|
+
*/
|
|
225
|
+
sendQueue: Queue.Dequeue<MsgSend>
|
|
226
|
+
/**
|
|
227
|
+
* Meant to be pushed to (e.g. via `Queue.offer`) for messages that will be received
|
|
228
|
+
* via `webChannel.listen()`.
|
|
229
|
+
*/
|
|
230
|
+
listenQueue: Queue.Enqueue<MsgListen>
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* From the outside the `sendQueue` is only accessible read-only,
|
|
235
|
+
* and the `listenQueue` is only accessible write-only.
|
|
236
|
+
*/
|
|
237
|
+
export const queueChannelProxy = <MsgListen, MsgSend>({
|
|
238
|
+
schema: inputSchema,
|
|
239
|
+
}: {
|
|
240
|
+
schema:
|
|
241
|
+
| Schema.Schema<MsgListen | MsgSend, any>
|
|
242
|
+
| { listen: Schema.Schema<MsgListen, any>; send: Schema.Schema<MsgSend, any> }
|
|
243
|
+
}): Effect.Effect<QueueChannelProxy<MsgListen, MsgSend>, never, Scope.Scope> =>
|
|
244
|
+
Effect.gen(function* () {
|
|
245
|
+
const sendQueue = yield* Queue.unbounded<MsgSend>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
246
|
+
const listenQueue = yield* Queue.unbounded<MsgListen>().pipe(Effect.acquireRelease(Queue.shutdown))
|
|
247
|
+
|
|
248
|
+
const send = (message: MsgSend) => Queue.offer(sendQueue, message)
|
|
108
249
|
|
|
109
250
|
const listen = Stream.fromQueue(listenQueue).pipe(Stream.map(Either.right))
|
|
110
251
|
|
|
111
252
|
const closedDeferred = yield* Deferred.make<void>()
|
|
253
|
+
const supportsTransferables = true
|
|
112
254
|
|
|
113
|
-
const
|
|
255
|
+
const schema = mapSchema(inputSchema)
|
|
256
|
+
|
|
257
|
+
const webChannel = {
|
|
258
|
+
[WebChannelSymbol]: WebChannelSymbol,
|
|
259
|
+
send,
|
|
260
|
+
listen,
|
|
261
|
+
closedDeferred,
|
|
262
|
+
schema,
|
|
263
|
+
supportsTransferables,
|
|
264
|
+
}
|
|
114
265
|
|
|
115
266
|
return { webChannel, sendQueue, listenQueue }
|
|
116
267
|
})
|
|
268
|
+
|
|
269
|
+
// export const proxy = <MsgListen, MsgSend>({
|
|
270
|
+
// originWebChannel,
|
|
271
|
+
// proxyWebChannel,
|
|
272
|
+
// }: {
|
|
273
|
+
// originWebChannel: WebChannel<MsgListen, MsgSend>
|
|
274
|
+
// proxyWebChannel: QueueChannelProxy<MsgListen, MsgSend>
|
|
275
|
+
// }) =>
|
|
276
|
+
// Effect.gen(function* () {
|
|
277
|
+
// const proxyListen = originWebChannel.listen.pipe(
|
|
278
|
+
// Stream.flatten(),
|
|
279
|
+
// Stream.tap((_) => Queue.offer(proxyWebChannel.listenQueue, _)),
|
|
280
|
+
// Stream.runDrain,
|
|
281
|
+
// )
|
|
282
|
+
|
|
283
|
+
// const proxySend = proxyWebChannel.sendQueue.pipe(
|
|
284
|
+
// Stream.fromQueue,
|
|
285
|
+
// Stream.tap(originWebChannel.send),
|
|
286
|
+
// Stream.runDrain,
|
|
287
|
+
// )
|
|
288
|
+
|
|
289
|
+
// yield* Effect.all([proxyListen, proxySend], { concurrency: 2 })
|
|
290
|
+
// })
|
package/src/effect/WebLock.ts
CHANGED
|
@@ -93,3 +93,78 @@ export const tryGetDeferredLock = (deferred: Deferred.Deferred<void>, lockName:
|
|
|
93
93
|
])
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
|
+
|
|
97
|
+
export const stealDeferredLock = (deferred: Deferred.Deferred<void>, lockName: string) =>
|
|
98
|
+
Effect.async<boolean>((cb, signal) => {
|
|
99
|
+
navigator.locks.request(lockName, { mode: 'exclusive', steal: true }, (lock) => {
|
|
100
|
+
cb(Effect.succeed(lock !== null))
|
|
101
|
+
|
|
102
|
+
// the code below is still running
|
|
103
|
+
|
|
104
|
+
const abortPromise = new Promise<void>((resolve) => {
|
|
105
|
+
signal.addEventListener('abort', () => {
|
|
106
|
+
resolve()
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// holding lock until deferred is resolved
|
|
111
|
+
return Promise.race([Effect.runPromise(Deferred.await(deferred)), abortPromise])
|
|
112
|
+
// .finally(() =>
|
|
113
|
+
// console.log('[@livestore/utils:WebLock] tryGetDeferredLock. finally', lockName),
|
|
114
|
+
// )
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
export const waitForLock = (lockName: string) =>
|
|
119
|
+
Effect.async<void>((cb, signal) => {
|
|
120
|
+
if (signal.aborted) return
|
|
121
|
+
|
|
122
|
+
navigator.locks.request(lockName, { mode: 'shared', signal, ifAvailable: false }, (_lock) => {
|
|
123
|
+
cb(Effect.succeed(void 0))
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/** Attempts to get the lock if available and waits for it to be stolen */
|
|
128
|
+
export const getLockAndWaitForSteal = (lockName: string) =>
|
|
129
|
+
Effect.async<void>((cb, signal) => {
|
|
130
|
+
if (signal.aborted) return
|
|
131
|
+
|
|
132
|
+
navigator.locks
|
|
133
|
+
.request(lockName, { mode: 'exclusive', ifAvailable: true }, async (lock) => {
|
|
134
|
+
if (lock === null) {
|
|
135
|
+
// Lock wasn't available, resolve immediately
|
|
136
|
+
cb(Effect.succeed(void 0))
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// We got the lock, now wait for it to be stolen
|
|
141
|
+
// When the lock is stolen, the promise will resolve
|
|
142
|
+
await new Promise<void>((resolve) => {
|
|
143
|
+
// Create a never-resolving promise unless interrupted
|
|
144
|
+
const holdLock = new Promise(() => {})
|
|
145
|
+
|
|
146
|
+
// Listen for the abort signal
|
|
147
|
+
signal.addEventListener('abort', () => {
|
|
148
|
+
resolve()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return Promise.race([holdLock, signal.aborted ? Promise.resolve() : holdLock]).catch(() => {})
|
|
152
|
+
}).catch(() => {})
|
|
153
|
+
|
|
154
|
+
cb(Effect.succeed(void 0))
|
|
155
|
+
})
|
|
156
|
+
.catch((error) => {
|
|
157
|
+
if (
|
|
158
|
+
error.code === 20 &&
|
|
159
|
+
(error.message === 'signal is aborted without reason' ||
|
|
160
|
+
error.message === `Lock broken by another request with the 'steal' option.`)
|
|
161
|
+
) {
|
|
162
|
+
// Given signal interruption is handled via Effect, we can ignore this case
|
|
163
|
+
// or the case when the lock is stolen
|
|
164
|
+
cb(Effect.succeed(void 0))
|
|
165
|
+
} else {
|
|
166
|
+
console.error('[@livestore/utils:WebLock] getLockAndWaitForSteal. error', error)
|
|
167
|
+
throw error
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
})
|