@livestore/utils 0.2.0 → 0.3.0-dev.1

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 (104) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.js +1 -1
  3. package/dist/NoopTracer.js.map +1 -1
  4. package/dist/base64.d.ts +1 -1
  5. package/dist/base64.d.ts.map +1 -1
  6. package/dist/base64.js.map +1 -1
  7. package/dist/effect/BucketQueue.d.ts +7 -0
  8. package/dist/effect/BucketQueue.d.ts.map +1 -0
  9. package/dist/effect/BucketQueue.js +16 -0
  10. package/dist/effect/BucketQueue.js.map +1 -0
  11. package/dist/effect/Effect.d.ts +6 -1
  12. package/dist/effect/Effect.d.ts.map +1 -1
  13. package/dist/effect/Effect.js +24 -10
  14. package/dist/effect/Effect.js.map +1 -1
  15. package/dist/effect/Logger.d.ts +3 -0
  16. package/dist/effect/Logger.d.ts.map +1 -0
  17. package/dist/effect/Logger.js +10 -0
  18. package/dist/effect/Logger.js.map +1 -0
  19. package/dist/effect/Schema/index.d.ts.map +1 -1
  20. package/dist/effect/Schema/index.js +1 -1
  21. package/dist/effect/Schema/index.js.map +1 -1
  22. package/dist/effect/Schema/msgpack.d.ts +1 -1
  23. package/dist/effect/Schema/msgpack.d.ts.map +1 -1
  24. package/dist/effect/TaskTracing.d.ts +5 -0
  25. package/dist/effect/TaskTracing.d.ts.map +1 -0
  26. package/dist/effect/TaskTracing.js +38 -0
  27. package/dist/effect/TaskTracing.js.map +1 -0
  28. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts +13 -0
  29. package/dist/effect/WebChannel/broadcastChannelWithAck.d.ts.map +1 -0
  30. package/dist/effect/WebChannel/broadcastChannelWithAck.js +88 -0
  31. package/dist/effect/WebChannel/broadcastChannelWithAck.js.map +1 -0
  32. package/dist/effect/WebChannel/common.d.ts +16 -0
  33. package/dist/effect/WebChannel/common.d.ts.map +1 -0
  34. package/dist/effect/WebChannel/common.js +4 -0
  35. package/dist/effect/WebChannel/common.js.map +1 -0
  36. package/dist/effect/WebChannel.d.ts +55 -25
  37. package/dist/effect/WebChannel.d.ts.map +1 -1
  38. package/dist/effect/WebChannel.js +133 -15
  39. package/dist/effect/WebChannel.js.map +1 -1
  40. package/dist/effect/WebLock.d.ts +4 -0
  41. package/dist/effect/WebLock.d.ts.map +1 -1
  42. package/dist/effect/WebLock.js +61 -0
  43. package/dist/effect/WebLock.js.map +1 -1
  44. package/dist/effect/WebSocket.d.ts +19 -0
  45. package/dist/effect/WebSocket.d.ts.map +1 -0
  46. package/dist/effect/WebSocket.js +53 -0
  47. package/dist/effect/WebSocket.js.map +1 -0
  48. package/dist/effect/index.d.ts +6 -2
  49. package/dist/effect/index.d.ts.map +1 -1
  50. package/dist/effect/index.js +10 -2
  51. package/dist/effect/index.js.map +1 -1
  52. package/dist/env.d.ts +5 -0
  53. package/dist/env.d.ts.map +1 -0
  54. package/dist/env.js +25 -0
  55. package/dist/env.js.map +1 -0
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +3 -12
  59. package/dist/index.js.map +1 -1
  60. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +5 -0
  61. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -0
  62. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +55 -0
  63. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -0
  64. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +10 -0
  65. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -0
  66. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +46 -0
  67. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -0
  68. package/dist/node/mod.d.ts +22 -0
  69. package/dist/node/mod.d.ts.map +1 -0
  70. package/dist/node/mod.js +83 -0
  71. package/dist/node/mod.js.map +1 -0
  72. package/dist/node-vitest/mod.d.ts +2 -0
  73. package/dist/node-vitest/mod.d.ts.map +1 -0
  74. package/dist/node-vitest/mod.js +2 -0
  75. package/dist/node-vitest/mod.js.map +1 -0
  76. package/dist/node-vitest/polyfill.d.ts +2 -0
  77. package/dist/node-vitest/polyfill.d.ts.map +1 -0
  78. package/dist/node-vitest/polyfill.js +3 -0
  79. package/dist/node-vitest/polyfill.js.map +1 -0
  80. package/dist/object/index.d.ts.map +1 -1
  81. package/dist/object/pick.d.ts.map +1 -1
  82. package/package.json +49 -15
  83. package/src/NoopTracer.ts +1 -1
  84. package/src/base64.ts +1 -1
  85. package/src/effect/BucketQueue.ts +22 -0
  86. package/src/effect/Effect.ts +41 -17
  87. package/src/effect/Logger.ts +17 -0
  88. package/src/effect/Schema/index.ts +1 -1
  89. package/src/effect/TaskTracing.ts +43 -0
  90. package/src/effect/WebChannel/broadcastChannelWithAck.ts +126 -0
  91. package/src/effect/WebChannel/common.ts +17 -0
  92. package/src/effect/WebChannel.ts +223 -49
  93. package/src/effect/WebLock.ts +75 -0
  94. package/src/effect/WebSocket.ts +72 -0
  95. package/src/effect/index.ts +14 -1
  96. package/src/env.ts +31 -0
  97. package/src/index.ts +3 -11
  98. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +63 -0
  99. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +66 -0
  100. package/src/node/mod.ts +115 -0
  101. package/src/node-vitest/mod.ts +1 -0
  102. package/src/node-vitest/polyfill.ts +1 -0
  103. package/tmp/effect-deferred-repro.ts +29 -0
  104. 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
+ }
@@ -1,116 +1,290 @@
1
- import type { ParseResult, Scope } from 'effect'
2
- import { Deferred, Effect, Either, Queue, Stream } from 'effect'
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 type WebChannel<MsgIn, MsgOut, E = never> = {
7
- send: (a: MsgOut) => Effect.Effect<void, ParseResult.ParseError | E>
8
- listen: Stream.Stream<Either.Either<MsgIn, ParseResult.ParseError>, E>
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 = <MsgIn, MsgOut, MsgInEncoded, MsgOutEncoded>({
12
+ export const broadcastChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
13
13
  channelName,
14
- listenSchema,
15
- sendSchema,
14
+ schema: inputSchema,
16
15
  }: {
17
16
  channelName: string
18
- listenSchema: Schema.Schema<MsgIn, MsgInEncoded>
19
- sendSchema: Schema.Schema<MsgOut, MsgOutEncoded>
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: MsgOut) =>
26
+ const send = (message: MsgSend) =>
27
27
  Effect.gen(function* () {
28
- const messageEncoded = yield* Schema.encode(sendSchema)(message)
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(listenSchema)(_.data)),
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 { send, listen, closedDeferred }
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 = <MsgIn, MsgOut, MsgInEncoded, MsgOutEncoded>({
50
+ export const windowChannel = <MsgListen, MsgSend, MsgListenEncoded, MsgSendEncoded>({
43
51
  window,
44
52
  targetOrigin = '*',
45
- listenSchema,
46
- sendSchema,
53
+ schema: inputSchema,
47
54
  }: {
48
55
  window: Window
49
56
  targetOrigin?: string
50
- listenSchema: Schema.Schema<MsgIn, MsgInEncoded>
51
- sendSchema: Schema.Schema<MsgOut, MsgOutEncoded>
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 send = (message: MsgOut) =>
60
+ const schema = mapSchema(inputSchema)
61
+
62
+ const send = (message: MsgSend) =>
55
63
  Effect.gen(function* () {
56
- const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(sendSchema)(message)
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(listenSchema)(_.data)),
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 { send, listen, closedDeferred }
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 = <MsgIn, MsgOut, MsgInEncoded, MsgOutEncoded>({
70
- port,
71
- listenSchema,
72
- sendSchema,
73
- }: {
74
- port: MessagePort
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 send = (message: MsgOut) =>
92
+ const schema = mapSchema(inputSchema)
93
+
94
+ const send = (message: any) =>
80
95
  Effect.gen(function* () {
81
- const [messageEncoded, transferables] = yield* Schema.encodeWithTransferables(sendSchema)(message)
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(listenSchema)(_.data)),
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 { send, listen, closedDeferred }
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 queueChannelProxy = <MsgIn, MsgOut>(): Effect.Effect<
99
- { webChannel: WebChannel<MsgIn, MsgOut>; sendQueue: Queue.Queue<MsgOut>; listenQueue: Queue.Queue<MsgIn> },
100
- never,
101
- Scope.Scope
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 sendQueue = yield* Queue.unbounded<MsgOut>().pipe(Effect.acquireRelease(Queue.shutdown))
105
- const listenQueue = yield* Queue.unbounded<MsgIn>().pipe(Effect.acquireRelease(Queue.shutdown))
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
- const send = (message: MsgOut) => Queue.offer(sendQueue, message)
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 webChannel = { send, listen, closedDeferred }
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
+ // })
@@ -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
+ })
@@ -0,0 +1,72 @@
1
+ import type { Schedule, Scope } from 'effect'
2
+ import { Effect, Exit, identity, Schema } from 'effect'
3
+
4
+ export class WebSocketError extends Schema.TaggedError<WebSocketError>()('WebSocketError', {
5
+ cause: Schema.Defect,
6
+ }) {}
7
+
8
+ // TODO refactor using Effect socket implementation
9
+ // https://github.com/Effect-TS/effect/blob/main/packages%2Fexperimental%2Fsrc%2FDevTools%2FClient.ts#L113
10
+ // "In a Stream pipeline everything above the pipeThrough is the outgoing (send) messages. Everything below is the incoming (message event) messages."
11
+ // https://github.com/Effect-TS/effect/blob/main/packages%2Fplatform%2Fsrc%2FSocket.ts#L451
12
+
13
+ /**
14
+ * Creates a WebSocket connection and waits for the connection to be established.
15
+ * Automatically closes the connection when the scope is closed.
16
+ */
17
+ export const makeWebSocket = ({
18
+ url,
19
+ reconnect,
20
+ }: {
21
+ url: string
22
+ reconnect?: Schedule.Schedule<unknown> | false
23
+ }): Effect.Effect<globalThis.WebSocket, WebSocketError, Scope.Scope> =>
24
+ Effect.gen(function* () {
25
+ const socket = yield* Effect.tryPromise({
26
+ try: async () => {
27
+ // console.debug('[WebSocket] connecting to', url)
28
+ const socket = new globalThis.WebSocket(url)
29
+
30
+ if (socket.readyState === globalThis.WebSocket.OPEN) {
31
+ return socket
32
+ }
33
+
34
+ return await new Promise<globalThis.WebSocket>((resolve, reject) => {
35
+ socket.addEventListener('open', () => resolve(socket), { once: true })
36
+ // eslint-disable-next-line unicorn/prefer-add-event-listener
37
+ socket.onerror = (event) => reject(event)
38
+ })
39
+ },
40
+ catch: (errorEvent: any) => {
41
+ if (errorEvent.currentTarget != null && errorEvent.currentTarget instanceof globalThis.WebSocket) {
42
+ errorEvent.currentTarget.close(3000, `closing websocket connection due to error: ${errorEvent.toString()}`)
43
+ }
44
+
45
+ return new WebSocketError({ cause: errorEvent })
46
+ },
47
+ }).pipe(
48
+ /**
49
+ * Common WebSocket close codes: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close
50
+ * 1000: Normal closure
51
+ * 1001: Endpoint is going away, a server is terminating the connection because it has received a request that indicates the client is ending the connection.
52
+ * 1002: Protocol error, a server is terminating the connection because it has received data on the connection that was not consistent with the type of the connection.
53
+ * 1011: Internal server error, a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
54
+ *
55
+ * For reference, here are the valid WebSocket close code ranges:
56
+ * 1000-1999: Reserved for protocol usage
57
+ * 2000-2999: Reserved for WebSocket extensions
58
+ * 3000-3999: Available for libraries and frameworks
59
+ * 4000-4999: Available for applications
60
+ */
61
+ Effect.acquireRelease((socket, exit) =>
62
+ Effect.sync(() =>
63
+ Exit.isFailure(exit)
64
+ ? socket.close(3000, `closing webmesh websocket connection due to error: ${exit.cause.toString()}`)
65
+ : socket.close(1000, 'closing webmesh websocket connection gracefully'),
66
+ ),
67
+ ),
68
+ reconnect ? Effect.retry(reconnect) : identity,
69
+ )
70
+
71
+ return socket
72
+ })