@livestore/utils 0.4.0-dev.3 → 0.4.0-dev.6
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.d.ts.map +1 -1
- package/dist/NoopTracer.js +1 -0
- package/dist/NoopTracer.js.map +1 -1
- package/dist/effect/Effect.d.ts +1 -2
- package/dist/effect/Effect.d.ts.map +1 -1
- package/dist/effect/Effect.js +2 -2
- package/dist/effect/Effect.js.map +1 -1
- package/dist/effect/Error.d.ts +1 -1
- package/dist/effect/Error.js.map +1 -1
- package/dist/effect/Logger.d.ts +4 -1
- package/dist/effect/Logger.d.ts.map +1 -1
- package/dist/effect/Logger.js +12 -3
- package/dist/effect/Logger.js.map +1 -1
- package/dist/effect/OtelTracer.d.ts +5 -0
- package/dist/effect/OtelTracer.d.ts.map +1 -0
- package/dist/effect/OtelTracer.js +8 -0
- package/dist/effect/OtelTracer.js.map +1 -0
- package/dist/effect/RpcClient.d.ts +32 -0
- package/dist/effect/RpcClient.d.ts.map +1 -0
- package/dist/effect/RpcClient.js +142 -0
- package/dist/effect/RpcClient.js.map +1 -0
- package/dist/effect/Schema/index.d.ts +0 -1
- package/dist/effect/Schema/index.d.ts.map +1 -1
- package/dist/effect/Schema/index.js +0 -1
- package/dist/effect/Schema/index.js.map +1 -1
- package/dist/effect/Stream.d.ts +73 -2
- package/dist/effect/Stream.d.ts.map +1 -1
- package/dist/effect/Stream.js +68 -1
- package/dist/effect/Stream.js.map +1 -1
- package/dist/effect/Stream.test.d.ts +2 -0
- package/dist/effect/Stream.test.d.ts.map +1 -0
- package/dist/effect/Stream.test.js +84 -0
- package/dist/effect/Stream.test.js.map +1 -0
- package/dist/effect/SubscriptionRef.d.ts +2 -2
- package/dist/effect/SubscriptionRef.d.ts.map +1 -1
- package/dist/effect/SubscriptionRef.js +6 -1
- package/dist/effect/SubscriptionRef.js.map +1 -1
- package/dist/effect/WebSocket.js +1 -1
- package/dist/effect/WebSocket.js.map +1 -1
- package/dist/effect/index.d.ts +9 -5
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +10 -7
- package/dist/effect/index.js.map +1 -1
- package/dist/global.d.ts +1 -0
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js.map +1 -1
- package/dist/mod.d.ts +2 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +4 -0
- package/dist/mod.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js +54 -10
- package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +182 -3
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +12 -3
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +7 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +11 -1
- package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +16 -0
- package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js +98 -2
- package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
- package/dist/node/mod.d.ts +1 -1
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +2 -2
- package/dist/node/mod.js.map +1 -1
- package/package.json +31 -32
- package/src/NoopTracer.ts +1 -0
- package/src/effect/Effect.ts +15 -3
- package/src/effect/Error.ts +1 -1
- package/src/effect/Logger.ts +14 -4
- package/src/effect/OtelTracer.ts +11 -0
- package/src/effect/RpcClient.ts +205 -0
- package/src/effect/Schema/index.ts +0 -1
- package/src/effect/Stream.test.ts +127 -0
- package/src/effect/Stream.ts +111 -2
- package/src/effect/SubscriptionRef.ts +14 -2
- package/src/effect/WebSocket.ts +1 -1
- package/src/effect/index.ts +16 -5
- package/src/global.ts +1 -0
- package/src/mod.ts +9 -0
- package/src/node/ChildProcessRunner/ChildProcessRunner.ts +59 -10
- package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +253 -3
- package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +14 -1
- package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +14 -1
- package/src/node/ChildProcessRunner/ChildProcessWorker.ts +111 -3
- package/src/node/mod.ts +3 -3
- package/src/effect/Schema/msgpack.ts +0 -8
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/utils",
|
|
3
|
-
"version": "0.4.0-dev.
|
|
3
|
+
"version": "0.4.0-dev.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
|
-
"./src/global.ts"
|
|
6
|
+
"./src/global.ts",
|
|
7
|
+
"./dist/global.js"
|
|
7
8
|
],
|
|
8
9
|
"exports": {
|
|
9
10
|
".": {
|
|
@@ -35,32 +36,32 @@
|
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@standard-schema/spec": "1.0.0",
|
|
38
|
-
"msgpackr": "1.11.5",
|
|
39
39
|
"nanoid": "5.1.5",
|
|
40
|
-
"pretty-bytes": "7.0.
|
|
40
|
+
"pretty-bytes": "7.0.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@effect/
|
|
44
|
-
"@effect/
|
|
45
|
-
"@effect/
|
|
46
|
-
"@effect/
|
|
47
|
-
"@effect/
|
|
43
|
+
"@effect/ai": "^0.26.0",
|
|
44
|
+
"@effect/cli": "^0.69.2",
|
|
45
|
+
"@effect/cluster": "^0.48.2",
|
|
46
|
+
"@effect/experimental": "^0.54.6",
|
|
47
|
+
"@effect/opentelemetry": "0.56.4",
|
|
48
|
+
"@effect/platform": "^0.90.6",
|
|
48
49
|
"@effect/platform-browser": "^0.70.0",
|
|
49
|
-
"@effect/platform-bun": "^0.
|
|
50
|
-
"@effect/platform-node": "^0.
|
|
50
|
+
"@effect/platform-bun": "^0.79.0",
|
|
51
|
+
"@effect/platform-node": "^0.96.0",
|
|
51
52
|
"@effect/printer": "^0.45.0",
|
|
52
53
|
"@effect/printer-ansi": "^0.45.0",
|
|
53
|
-
"@effect/rpc": "^0.
|
|
54
|
-
"@effect/sql": "^0.44.
|
|
54
|
+
"@effect/rpc": "^0.69.1",
|
|
55
|
+
"@effect/sql": "^0.44.2",
|
|
55
56
|
"@effect/typeclass": "^0.36.0",
|
|
56
|
-
"@effect/vitest": "^0.25.
|
|
57
|
-
"@opentelemetry/api": "
|
|
57
|
+
"@effect/vitest": "^0.25.1",
|
|
58
|
+
"@opentelemetry/api": "1.9.0",
|
|
58
59
|
"@opentelemetry/resources": "^2.0.1",
|
|
59
|
-
"@types/bun": "^1.2.
|
|
60
|
+
"@types/bun": "^1.2.21",
|
|
60
61
|
"@types/jsdom": "^21.1.7",
|
|
61
62
|
"@types/node": "24.2.0",
|
|
62
|
-
"@types/web": "^0.0.
|
|
63
|
-
"effect": "
|
|
63
|
+
"@types/web": "^0.0.264",
|
|
64
|
+
"effect": "3.17.9",
|
|
64
65
|
"jsdom": "^26.1.0",
|
|
65
66
|
"vitest": "3.2.4"
|
|
66
67
|
},
|
|
@@ -71,28 +72,26 @@
|
|
|
71
72
|
],
|
|
72
73
|
"license": "Apache-2.0",
|
|
73
74
|
"peerDependencies": {
|
|
74
|
-
"@effect/
|
|
75
|
-
"@effect/
|
|
76
|
-
"@effect/
|
|
77
|
-
"@effect/
|
|
78
|
-
"@effect/
|
|
75
|
+
"@effect/ai": "^0.26.0",
|
|
76
|
+
"@effect/cli": "^0.69.2",
|
|
77
|
+
"@effect/cluster": "^0.48.2",
|
|
78
|
+
"@effect/experimental": "^0.54.6",
|
|
79
|
+
"@effect/opentelemetry": "^0.56.4",
|
|
80
|
+
"@effect/platform": "^0.90.6",
|
|
79
81
|
"@effect/platform-browser": "^0.70.0",
|
|
80
|
-
"@effect/platform-bun": "^0.
|
|
81
|
-
"@effect/platform-node": "^0.
|
|
82
|
+
"@effect/platform-bun": "^0.79.0",
|
|
83
|
+
"@effect/platform-node": "^0.96.0",
|
|
82
84
|
"@effect/printer": "^0.45.0",
|
|
83
85
|
"@effect/printer-ansi": "^0.45.0",
|
|
84
|
-
"@effect/rpc": "^0.
|
|
85
|
-
"@effect/sql": "^0.44.
|
|
86
|
+
"@effect/rpc": "^0.69.1",
|
|
87
|
+
"@effect/sql": "^0.44.2",
|
|
86
88
|
"@effect/typeclass": "^0.36.0",
|
|
87
89
|
"@opentelemetry/api": "^1.9.0",
|
|
88
90
|
"@opentelemetry/resources": "^2.0.1",
|
|
89
|
-
"effect": "^3.17.
|
|
91
|
+
"effect": "^3.17.9"
|
|
90
92
|
},
|
|
91
93
|
"publishConfig": {
|
|
92
|
-
"access": "public"
|
|
93
|
-
"sideEffects": [
|
|
94
|
-
"./dist/global.js"
|
|
95
|
-
]
|
|
94
|
+
"access": "public"
|
|
96
95
|
},
|
|
97
96
|
"react-native": "./dist/index.js",
|
|
98
97
|
"scripts": {
|
package/src/NoopTracer.ts
CHANGED
package/src/effect/Effect.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import * as OtelTracer from '@effect/opentelemetry/Tracer'
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
Cause,
|
|
4
|
+
type Context,
|
|
5
|
+
Deferred,
|
|
6
|
+
Duration,
|
|
7
|
+
Effect,
|
|
8
|
+
Fiber,
|
|
9
|
+
FiberRef,
|
|
10
|
+
HashSet,
|
|
11
|
+
Logger,
|
|
12
|
+
pipe,
|
|
13
|
+
Scope,
|
|
14
|
+
type Stream,
|
|
15
|
+
} from 'effect'
|
|
4
16
|
import type { UnknownException } from 'effect/Cause'
|
|
5
17
|
import { log } from 'effect/Console'
|
|
6
18
|
import type { LazyArg } from 'effect/Function'
|
|
@@ -166,7 +178,7 @@ export const logDuration =
|
|
|
166
178
|
const start = Date.now()
|
|
167
179
|
const res = yield* eff
|
|
168
180
|
const end = Date.now()
|
|
169
|
-
yield* Effect.log(`${label}: ${end - start}
|
|
181
|
+
yield* Effect.log(`${label}: ${Duration.format(end - start)}`)
|
|
170
182
|
return res
|
|
171
183
|
})
|
|
172
184
|
|
package/src/effect/Error.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from 'effect'
|
|
2
2
|
|
|
3
|
-
export class UnknownError extends Schema.TaggedError<
|
|
3
|
+
export class UnknownError extends Schema.TaggedError<UnknownError>()('UnknownError', {
|
|
4
4
|
cause: Schema.Any,
|
|
5
5
|
payload: Schema.optional(Schema.Any),
|
|
6
6
|
}) {}
|
package/src/effect/Logger.ts
CHANGED
|
@@ -8,20 +8,24 @@ const defaultDateFormat = (date: Date): string =>
|
|
|
8
8
|
.toString()
|
|
9
9
|
.padStart(2, '0')}.${date.getMilliseconds().toString().padStart(3, '0')}`
|
|
10
10
|
|
|
11
|
-
export const prettyWithThread = (threadName: string) =>
|
|
11
|
+
export const prettyWithThread = (threadName: string, options: { mode?: 'tty' | 'browser' } = {}) =>
|
|
12
12
|
Logger.replace(
|
|
13
13
|
Logger.defaultLogger,
|
|
14
14
|
Logger.prettyLogger({
|
|
15
15
|
formatDate: (date) => `${defaultDateFormat(date)} ${threadName}`,
|
|
16
|
+
mode: options.mode,
|
|
16
17
|
}),
|
|
17
|
-
// consoleLogger(threadName),
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
export const consoleLogger = (threadName: string) =>
|
|
21
21
|
Logger.make(({ message, annotations, date, logLevel, cause }) => {
|
|
22
|
+
const isCloudflareWorker = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
|
|
22
23
|
const consoleFn =
|
|
23
24
|
logLevel === LogLevel.Debug
|
|
24
|
-
? console.debug
|
|
25
|
+
? // Cloudflare Workers doesn't support console.debug 🤷
|
|
26
|
+
isCloudflareWorker
|
|
27
|
+
? console.log
|
|
28
|
+
: console.debug
|
|
25
29
|
: logLevel === LogLevel.Info
|
|
26
30
|
? console.info
|
|
27
31
|
: logLevel === LogLevel.Warning
|
|
@@ -35,5 +39,11 @@ export const consoleLogger = (threadName: string) =>
|
|
|
35
39
|
messages.push(Cause.pretty(cause, { renderErrorCause: true }))
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
if (Object.keys(annotationsObj).length > 0) {
|
|
43
|
+
messages.push(annotationsObj)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
consoleFn(`[${defaultDateFormat(date)} ${threadName}]`, ...messages)
|
|
39
47
|
})
|
|
48
|
+
|
|
49
|
+
export const consoleWithThread = (threadName: string) => Logger.replace(Logger.defaultLogger, consoleLogger(threadName))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { makeExternalSpan } from '@effect/opentelemetry/Tracer'
|
|
2
|
+
import type { Link as OtelSpanLink } from '@opentelemetry/api'
|
|
3
|
+
import type { SpanLink as EffectSpanLink } from 'effect/Tracer'
|
|
4
|
+
|
|
5
|
+
export * from '@effect/opentelemetry/Tracer'
|
|
6
|
+
|
|
7
|
+
export const makeSpanLink = (otelSpanLink: OtelSpanLink): EffectSpanLink => ({
|
|
8
|
+
_tag: 'SpanLink',
|
|
9
|
+
span: makeExternalSpan(otelSpanLink.context),
|
|
10
|
+
attributes: otelSpanLink.attributes ?? {},
|
|
11
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export * from '@effect/rpc/RpcClient'
|
|
2
|
+
|
|
3
|
+
import { Socket } from '@effect/platform'
|
|
4
|
+
import { RpcClient, RpcClientError, RpcSerialization } from '@effect/rpc'
|
|
5
|
+
import { Protocol } from '@effect/rpc/RpcClient'
|
|
6
|
+
import { constPing, type FromServerEncoded } from '@effect/rpc/RpcMessage'
|
|
7
|
+
import { Cause, Deferred, Effect, Layer, Option, Schedule, type Scope } from 'effect'
|
|
8
|
+
import { constVoid, identity } from 'effect/Function'
|
|
9
|
+
import * as SubscriptionRef from './SubscriptionRef.ts'
|
|
10
|
+
|
|
11
|
+
// This is based on `makeProtocolSocket` / `layerProtocolSocket` from `@effect/rpc` in order to:
|
|
12
|
+
// - Add a `isConnected` subscription ref to track the connection state
|
|
13
|
+
// - Add a ping schedule to the socket
|
|
14
|
+
// - Add a retry schedule to the socket
|
|
15
|
+
|
|
16
|
+
export const layerProtocolSocketWithIsConnected = (options: {
|
|
17
|
+
readonly url: string
|
|
18
|
+
readonly retryTransientErrors?: Schedule.Schedule<unknown> | undefined
|
|
19
|
+
readonly isConnected: SubscriptionRef.SubscriptionRef<boolean>
|
|
20
|
+
readonly pingSchedule?: Schedule.Schedule<unknown> | undefined
|
|
21
|
+
}): Layer.Layer<Protocol, never, RpcSerialization.RpcSerialization | Socket.Socket> =>
|
|
22
|
+
Layer.scoped(Protocol, makeProtocolSocketWithIsConnected(options))
|
|
23
|
+
|
|
24
|
+
export const makeProtocolSocketWithIsConnected = (options: {
|
|
25
|
+
readonly url: string
|
|
26
|
+
readonly retryTransientErrors?: Schedule.Schedule<unknown> | undefined
|
|
27
|
+
// CHANGED: add isConnected subscription ref
|
|
28
|
+
readonly isConnected: SubscriptionRef.SubscriptionRef<boolean>
|
|
29
|
+
// CHANGED: add ping schedule
|
|
30
|
+
readonly pingSchedule?: Schedule.Schedule<unknown> | undefined
|
|
31
|
+
}): Effect.Effect<Protocol['Type'], never, Scope.Scope | RpcSerialization.RpcSerialization | Socket.Socket> =>
|
|
32
|
+
Protocol.make(
|
|
33
|
+
Effect.fnUntraced(function* (writeResponse) {
|
|
34
|
+
const socket = yield* Socket.Socket
|
|
35
|
+
const serialization = yield* RpcSerialization.RpcSerialization
|
|
36
|
+
|
|
37
|
+
const write = yield* socket.writer
|
|
38
|
+
const parser = serialization.unsafeMake()
|
|
39
|
+
|
|
40
|
+
const pinger = yield* makePinger(write(parser.encode(constPing)!), options?.pingSchedule)
|
|
41
|
+
|
|
42
|
+
yield* Effect.suspend(() => {
|
|
43
|
+
// CHANGED: don't reset parser on every message
|
|
44
|
+
// parser = serialization.unsafeMake()
|
|
45
|
+
pinger.reset()
|
|
46
|
+
return socket
|
|
47
|
+
.runRaw((message) => {
|
|
48
|
+
try {
|
|
49
|
+
const responses = parser.decode(message) as Array<FromServerEncoded>
|
|
50
|
+
if (responses.length === 0) return
|
|
51
|
+
let i = 0
|
|
52
|
+
return Effect.whileLoop({
|
|
53
|
+
while: () => i < responses.length,
|
|
54
|
+
body: () => {
|
|
55
|
+
const response = responses[i++]!
|
|
56
|
+
if (response._tag === 'Pong') {
|
|
57
|
+
pinger.onPong()
|
|
58
|
+
}
|
|
59
|
+
return writeResponse(response).pipe(
|
|
60
|
+
// CHANGED: set isConnected to true on pong
|
|
61
|
+
Effect.tap(
|
|
62
|
+
Effect.fn(function* () {
|
|
63
|
+
if (options?.isConnected !== undefined) {
|
|
64
|
+
yield* SubscriptionRef.set(options.isConnected, true)
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
step: constVoid,
|
|
71
|
+
})
|
|
72
|
+
} catch (defect) {
|
|
73
|
+
return writeResponse({
|
|
74
|
+
_tag: 'ClientProtocolError',
|
|
75
|
+
error: new RpcClientError.RpcClientError({
|
|
76
|
+
reason: 'Protocol',
|
|
77
|
+
message: 'Error decoding message',
|
|
78
|
+
cause: Cause.fail(defect),
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.pipe(
|
|
84
|
+
Effect.raceFirst(
|
|
85
|
+
Effect.zipRight(
|
|
86
|
+
pinger.timeout,
|
|
87
|
+
Effect.fail(
|
|
88
|
+
new Socket.SocketGenericError({
|
|
89
|
+
reason: 'OpenTimeout',
|
|
90
|
+
cause: new Error('ping timeout'),
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}).pipe(
|
|
97
|
+
Effect.zipRight(
|
|
98
|
+
Effect.fail(
|
|
99
|
+
new Socket.SocketCloseError({
|
|
100
|
+
reason: 'Close',
|
|
101
|
+
code: 1000,
|
|
102
|
+
closeReason: 'Closing connection',
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
Effect.tapErrorCause(
|
|
107
|
+
Effect.fn(function* (cause) {
|
|
108
|
+
// CHANGED: set isConnected to false on error
|
|
109
|
+
if (options?.isConnected !== undefined) {
|
|
110
|
+
yield* SubscriptionRef.set(options.isConnected, false)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const error = Cause.failureOption(cause)
|
|
114
|
+
if (
|
|
115
|
+
options?.retryTransientErrors &&
|
|
116
|
+
Option.isSome(error) &&
|
|
117
|
+
(error.value.reason === 'Open' || error.value.reason === 'OpenTimeout')
|
|
118
|
+
) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
// yield* Effect.logError('Error in socket', cause)
|
|
122
|
+
return yield* writeResponse({
|
|
123
|
+
_tag: 'ClientProtocolError',
|
|
124
|
+
error: new RpcClientError.RpcClientError({
|
|
125
|
+
reason: 'Protocol',
|
|
126
|
+
message: 'Error in socket',
|
|
127
|
+
cause: Cause.squash(cause),
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
// CHANGED: make configurable via schedule
|
|
133
|
+
options?.retryTransientErrors ? Effect.retry(options.retryTransientErrors) : identity,
|
|
134
|
+
Effect.annotateLogs({
|
|
135
|
+
module: 'RpcClient',
|
|
136
|
+
method: 'makeProtocolSocket',
|
|
137
|
+
}),
|
|
138
|
+
Effect.interruptible,
|
|
139
|
+
Effect.ignore, // Errors are already handled
|
|
140
|
+
Effect.provide(Layer.setUnhandledErrorLogLevel(Option.none())),
|
|
141
|
+
Effect.forkScoped,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
send: (request) => {
|
|
146
|
+
const encoded = parser.encode(request)
|
|
147
|
+
if (encoded === undefined) return Effect.void
|
|
148
|
+
|
|
149
|
+
return Effect.orDie(write(encoded))
|
|
150
|
+
},
|
|
151
|
+
supportsAck: true,
|
|
152
|
+
supportsTransferables: false,
|
|
153
|
+
pinger,
|
|
154
|
+
}
|
|
155
|
+
}),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
export const SocketPinger = Effect.map(RpcClient.Protocol, (protocol) => (protocol as any).pinger as SocketPinger)
|
|
159
|
+
|
|
160
|
+
export type SocketPinger = Effect.Effect.Success<ReturnType<typeof makePinger>>
|
|
161
|
+
|
|
162
|
+
const makePinger = Effect.fnUntraced(function* <A, E, R>(
|
|
163
|
+
writePing: Effect.Effect<A, E, R>,
|
|
164
|
+
pingSchedule: Schedule.Schedule<unknown> = Schedule.spaced(10000).pipe(Schedule.addDelay(() => 5000)),
|
|
165
|
+
) {
|
|
166
|
+
// CHANGED: add manual ping deferreds
|
|
167
|
+
const manualPingDeferreds = new Set<Deferred.Deferred<void, never>>()
|
|
168
|
+
|
|
169
|
+
let recievedPong = true
|
|
170
|
+
const latch = Effect.unsafeMakeLatch()
|
|
171
|
+
const reset = () => {
|
|
172
|
+
recievedPong = true
|
|
173
|
+
latch.unsafeClose()
|
|
174
|
+
}
|
|
175
|
+
const onPong = () => {
|
|
176
|
+
recievedPong = true
|
|
177
|
+
// CHANGED: mark all manual ping deferreds as done
|
|
178
|
+
for (const deferred of manualPingDeferreds) {
|
|
179
|
+
Deferred.unsafeDone(deferred, Effect.void)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
yield* Effect.suspend(() => {
|
|
183
|
+
// Starting new ping
|
|
184
|
+
if (!recievedPong) return latch.open
|
|
185
|
+
recievedPong = false
|
|
186
|
+
return writePing
|
|
187
|
+
}).pipe(
|
|
188
|
+
// CHANGED: make configurable via schedule
|
|
189
|
+
Effect.schedule(pingSchedule),
|
|
190
|
+
Effect.ignore,
|
|
191
|
+
Effect.forever,
|
|
192
|
+
Effect.interruptible,
|
|
193
|
+
Effect.forkScoped,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
// CHANGED: add manual ping
|
|
197
|
+
const ping = Effect.gen(function* () {
|
|
198
|
+
const deferred = yield* Deferred.make<void, never>()
|
|
199
|
+
manualPingDeferreds.add(deferred)
|
|
200
|
+
yield* deferred
|
|
201
|
+
manualPingDeferreds.delete(deferred)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
return { timeout: latch.await, reset, onPong, ping } as const
|
|
205
|
+
})
|
|
@@ -8,7 +8,6 @@ import { shouldNeverHappen } from '../../mod.ts'
|
|
|
8
8
|
|
|
9
9
|
export * from 'effect/Schema'
|
|
10
10
|
export * from './debug-diff.ts'
|
|
11
|
-
export * from './msgpack.ts'
|
|
12
11
|
|
|
13
12
|
// NOTE this is a temporary workaround until Effect schema has a better way to hash schemas
|
|
14
13
|
// https://github.com/Effect-TS/effect/issues/2719
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Effect, Option, Stream } from 'effect'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { concatWithLastElement, runCollectReadonlyArray } from './Stream.ts'
|
|
4
|
+
|
|
5
|
+
describe('concatWithLastElement', () => {
|
|
6
|
+
it('should concatenate streams with access to last element of first stream', async () => {
|
|
7
|
+
const stream1 = Stream.make(1, 2, 3)
|
|
8
|
+
const result = concatWithLastElement(stream1, (lastElement) =>
|
|
9
|
+
lastElement.pipe(
|
|
10
|
+
Option.match({
|
|
11
|
+
onNone: () => Stream.make('no-previous'),
|
|
12
|
+
onSome: (last) => Stream.make(`last-was-${last}`, 'continuing'),
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
18
|
+
expect(collected).toEqual([1, 2, 3, 'last-was-3', 'continuing'])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should handle empty first stream', async () => {
|
|
22
|
+
const stream1 = Stream.empty
|
|
23
|
+
const result = concatWithLastElement(stream1, (lastElement) =>
|
|
24
|
+
lastElement.pipe(
|
|
25
|
+
Option.match({
|
|
26
|
+
onNone: () => Stream.make('no-previous-element'),
|
|
27
|
+
onSome: (last) => Stream.make(`last-was-${last}`),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
33
|
+
expect(collected).toEqual(['no-previous-element'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should handle single element first stream', async () => {
|
|
37
|
+
const stream1 = Stream.make('single')
|
|
38
|
+
const result = concatWithLastElement(stream1, (lastElement) =>
|
|
39
|
+
lastElement.pipe(
|
|
40
|
+
Option.match({
|
|
41
|
+
onNone: () => Stream.make('unexpected'),
|
|
42
|
+
onSome: (last) => Stream.make(`after-${last}`),
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
48
|
+
expect(collected).toEqual(['single', 'after-single'])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should handle empty second stream', async () => {
|
|
52
|
+
const stream1 = Stream.make(1, 2, 3)
|
|
53
|
+
const result = concatWithLastElement(stream1, () => Stream.empty)
|
|
54
|
+
|
|
55
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
56
|
+
expect(collected).toEqual([1, 2, 3])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should preserve error handling from first stream', async () => {
|
|
60
|
+
const stream1 = Stream.fail('first-error')
|
|
61
|
+
const result = concatWithLastElement(stream1, () => Stream.make('should-not-reach'))
|
|
62
|
+
|
|
63
|
+
const outcome = await Effect.runPromise(Effect.either(runCollectReadonlyArray(result)))
|
|
64
|
+
expect(outcome._tag).toBe('Left')
|
|
65
|
+
if (outcome._tag === 'Left') {
|
|
66
|
+
expect(outcome.left).toBe('first-error')
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should preserve error handling from second stream', async () => {
|
|
71
|
+
const stream1 = Stream.make(1, 2)
|
|
72
|
+
const result = concatWithLastElement(stream1, () => Stream.fail('second-error'))
|
|
73
|
+
|
|
74
|
+
const outcome = await Effect.runPromise(Effect.either(runCollectReadonlyArray(result)))
|
|
75
|
+
expect(outcome._tag).toBe('Left')
|
|
76
|
+
if (outcome._tag === 'Left') {
|
|
77
|
+
expect(outcome.left).toBe('second-error')
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should work with different types in streams', async () => {
|
|
82
|
+
const stream1 = Stream.make(1, 2, 3)
|
|
83
|
+
const result = concatWithLastElement(stream1, (lastElement) =>
|
|
84
|
+
lastElement.pipe(
|
|
85
|
+
Option.match({
|
|
86
|
+
onNone: () => Stream.make('no-number') as Stream.Stream<number | string, never, never>,
|
|
87
|
+
onSome: (last) => Stream.make(last * 10, last * 100),
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
93
|
+
expect(collected).toEqual([1, 2, 3, 30, 300])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should handle async effects in streams', async () => {
|
|
97
|
+
const stream1 = Stream.fromEffect(Effect.succeed('async-value'))
|
|
98
|
+
const result = concatWithLastElement(stream1, (lastElement) =>
|
|
99
|
+
lastElement.pipe(
|
|
100
|
+
Option.match({
|
|
101
|
+
onNone: () => Stream.fromEffect(Effect.succeed('no-async')),
|
|
102
|
+
onSome: (last) => Stream.fromEffect(Effect.succeed(`processed-${last}`)),
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
108
|
+
expect(collected).toEqual(['async-value', 'processed-async-value'])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should work with dual function - piped style', async () => {
|
|
112
|
+
const stream1 = Stream.make('a', 'b', 'c')
|
|
113
|
+
const result = stream1.pipe(
|
|
114
|
+
concatWithLastElement((lastElement) =>
|
|
115
|
+
lastElement.pipe(
|
|
116
|
+
Option.match({
|
|
117
|
+
onNone: () => Stream.make('no-last'),
|
|
118
|
+
onSome: (last) => Stream.make(`last-${last}`, 'done'),
|
|
119
|
+
}),
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const collected = await Effect.runPromise(runCollectReadonlyArray(result))
|
|
125
|
+
expect(collected).toEqual(['a', 'b', 'c', 'last-c', 'done'])
|
|
126
|
+
})
|
|
127
|
+
})
|
package/src/effect/Stream.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/useIterableCallbackReturn: Biome bug */
|
|
1
2
|
export * from 'effect/Stream'
|
|
2
3
|
|
|
3
|
-
import type
|
|
4
|
-
import {
|
|
4
|
+
import { type Cause, Chunk, Effect, Option, pipe, Ref, Stream } from 'effect'
|
|
5
|
+
import { dual } from 'effect/Function'
|
|
5
6
|
|
|
6
7
|
export const tapLog = <R, E, A>(stream: Stream.Stream<A, E, R>): Stream.Stream<A, E, R> =>
|
|
7
8
|
tapChunk<never, never, A, void>(Effect.forEach((_) => Effect.succeed(console.log(_))))(stream)
|
|
@@ -61,3 +62,111 @@ export const skipRepeated_ = <R, E, A>(
|
|
|
61
62
|
),
|
|
62
63
|
),
|
|
63
64
|
)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the first element of the stream or `None` if the stream is empty.
|
|
68
|
+
* It's different than `Stream.runHead` which runs the stream to completion.
|
|
69
|
+
* */
|
|
70
|
+
export const runFirst = <A, E, R>(stream: Stream.Stream<A, E, R>): Effect.Effect<Option.Option<A>, E, R> =>
|
|
71
|
+
stream.pipe(Stream.take(1), Stream.runCollect, Effect.map(Chunk.head))
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the first element of the stream or throws a `NoSuchElementException` if the stream is empty.
|
|
75
|
+
* It's different than `Stream.runHead` which runs the stream to completion.
|
|
76
|
+
* */
|
|
77
|
+
export const runFirstUnsafe = <A, E, R>(
|
|
78
|
+
stream: Stream.Stream<A, E, R>,
|
|
79
|
+
): Effect.Effect<A, Cause.NoSuchElementException | E, R> => runFirst(stream).pipe(Effect.flatten)
|
|
80
|
+
|
|
81
|
+
export const runCollectReadonlyArray = <A, E, R>(stream: Stream.Stream<A, E, R>): Effect.Effect<readonly A[], E, R> =>
|
|
82
|
+
stream.pipe(Stream.runCollect, Effect.map(Chunk.toReadonlyArray))
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Concatenates two streams where the second stream has access to the last element
|
|
86
|
+
* of the first stream as an `Option`. If the first stream is empty, the callback
|
|
87
|
+
* receives `Option.none()`.
|
|
88
|
+
*
|
|
89
|
+
* @param stream - The first stream to consume
|
|
90
|
+
* @param getStream2 - Function that receives the last element from the first stream
|
|
91
|
+
* and returns the second stream to concatenate
|
|
92
|
+
* @returns A new stream containing all elements from both streams
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* // Direct usage
|
|
97
|
+
* const result = concatWithLastElement(
|
|
98
|
+
* Stream.make(1, 2, 3),
|
|
99
|
+
* lastElement => lastElement.pipe(
|
|
100
|
+
* Option.match({
|
|
101
|
+
* onNone: () => Stream.make('empty'),
|
|
102
|
+
* onSome: last => Stream.make(`last-was-${last}`)
|
|
103
|
+
* })
|
|
104
|
+
* )
|
|
105
|
+
* )
|
|
106
|
+
*
|
|
107
|
+
* // Piped usage
|
|
108
|
+
* const result = Stream.make(1, 2, 3).pipe(
|
|
109
|
+
* concatWithLastElement(lastElement =>
|
|
110
|
+
* Stream.make(lastElement.pipe(Option.getOrElse(() => 0)) * 10)
|
|
111
|
+
* )
|
|
112
|
+
* )
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export const concatWithLastElement: {
|
|
116
|
+
<A1, A2, E2, R2>(
|
|
117
|
+
getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
|
|
118
|
+
): <E1, R1>(stream: Stream.Stream<A1, E1, R1>) => Stream.Stream<A1 | A2, E1 | E2, R1 | R2>
|
|
119
|
+
<A1, E1, R1, A2, E2, R2>(
|
|
120
|
+
stream: Stream.Stream<A1, E1, R1>,
|
|
121
|
+
getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
|
|
122
|
+
): Stream.Stream<A1 | A2, E1 | E2, R1 | R2>
|
|
123
|
+
} = dual(
|
|
124
|
+
2,
|
|
125
|
+
<A1, E1, R1, A2, E2, R2>(
|
|
126
|
+
stream1: Stream.Stream<A1, E1, R1>,
|
|
127
|
+
getStream2: (lastElement: Option.Option<A1>) => Stream.Stream<A2, E2, R2>,
|
|
128
|
+
): Stream.Stream<A1 | A2, E1 | E2, R1 | R2> =>
|
|
129
|
+
pipe(
|
|
130
|
+
Ref.make<Option.Option<A1>>(Option.none()),
|
|
131
|
+
Stream.fromEffect,
|
|
132
|
+
Stream.flatMap((lastRef) =>
|
|
133
|
+
pipe(
|
|
134
|
+
stream1,
|
|
135
|
+
Stream.tap((value) => Ref.set(lastRef, Option.some(value))),
|
|
136
|
+
Stream.concat(pipe(Ref.get(lastRef), Effect.map(getStream2), Stream.unwrap)),
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Emits a default value if the stream is empty, otherwise passes through all elements.
|
|
144
|
+
* Uses `concatWithLastElement` internally to detect if the stream was empty.
|
|
145
|
+
*
|
|
146
|
+
* @param fallbackValue - The value to emit if the stream is empty
|
|
147
|
+
* @returns A dual function that can be used in pipe or direct call
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* // Direct usage
|
|
152
|
+
* const result = emitIfEmpty(Stream.empty, 'default')
|
|
153
|
+
* // Emits: 'default'
|
|
154
|
+
*
|
|
155
|
+
* // Piped usage
|
|
156
|
+
* const result = Stream.make(1, 2, 3).pipe(emitIfEmpty('fallback'))
|
|
157
|
+
* // Emits: 1, 2, 3
|
|
158
|
+
*
|
|
159
|
+
* const empty = Stream.empty.pipe(emitIfEmpty('fallback'))
|
|
160
|
+
* // Emits: 'fallback'
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export const emitIfEmpty: {
|
|
164
|
+
<A>(fallbackValue: A): <E, R>(stream: Stream.Stream<A, E, R>) => Stream.Stream<A, E, R>
|
|
165
|
+
<A, E, R>(stream: Stream.Stream<A, E, R>, fallbackValue: A): Stream.Stream<A, E, R>
|
|
166
|
+
} = dual(
|
|
167
|
+
2,
|
|
168
|
+
<A, E, R>(stream: Stream.Stream<A, E, R>, fallbackValue: A): Stream.Stream<A, E, R> =>
|
|
169
|
+
concatWithLastElement(stream, (lastElement) =>
|
|
170
|
+
lastElement._tag === 'None' ? Stream.make(fallbackValue) : Stream.empty,
|
|
171
|
+
),
|
|
172
|
+
)
|