@livestore/utils 0.0.54-dev.22 → 0.0.54-dev.24
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/effect/Effect.d.ts +6 -5
- package/dist/effect/Effect.d.ts.map +1 -1
- package/dist/effect/Effect.js +37 -13
- package/dist/effect/Effect.js.map +1 -1
- package/dist/effect/Schema.d.ts +5 -1
- package/dist/effect/Schema.d.ts.map +1 -1
- package/dist/effect/Schema.js +8 -2
- package/dist/effect/Schema.js.map +1 -1
- package/dist/effect/SubscriptionRef.d.ts +8 -3
- package/dist/effect/SubscriptionRef.d.ts.map +1 -1
- package/dist/effect/SubscriptionRef.js +2 -2
- package/dist/effect/SubscriptionRef.js.map +1 -1
- package/dist/effect/WebLock.d.ts +5 -1
- package/dist/effect/WebLock.d.ts.map +1 -1
- package/dist/effect/WebLock.js +26 -6
- package/dist/effect/WebLock.js.map +1 -1
- package/dist/effect/index.d.ts +3 -2
- package/dist/effect/index.d.ts.map +1 -1
- package/dist/effect/index.js +3 -2
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/port-platform-runner.d.ts +5 -0
- package/dist/effect/port-platform-runner.d.ts.map +1 -0
- package/dist/effect/port-platform-runner.js +54 -0
- package/dist/effect/port-platform-runner.js.map +1 -0
- package/package.json +14 -14
- package/src/effect/Effect.ts +54 -18
- package/src/effect/Schema.ts +18 -2
- package/src/effect/SubscriptionRef.ts +22 -7
- package/src/effect/WebLock.ts +41 -13
- package/src/effect/index.ts +4 -0
- package/src/effect/port-platform-runner.ts +73 -0
package/src/effect/Effect.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Context, Duration } from 'effect'
|
|
2
|
-
import { Cause, Deferred, Effect, pipe } from 'effect'
|
|
1
|
+
import type { Context, Duration, Scope } from 'effect'
|
|
2
|
+
import { Cause, Deferred, Effect, Fiber, pipe } from 'effect'
|
|
3
|
+
import { log } from 'effect/Console'
|
|
3
4
|
import type { LazyArg } from 'effect/Function'
|
|
4
5
|
|
|
5
6
|
import { isNonEmptyString } from '../index.js'
|
|
@@ -7,20 +8,20 @@ import { UnknownError } from './Error.js'
|
|
|
7
8
|
|
|
8
9
|
export * from 'effect/Effect'
|
|
9
10
|
|
|
10
|
-
export const log = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// export const log = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
12
|
+
// Effect.sync(() => {
|
|
13
|
+
// console.log(message, ...rest)
|
|
14
|
+
// })
|
|
14
15
|
|
|
15
|
-
export const logWarn = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// export const logWarn = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
17
|
+
// Effect.sync(() => {
|
|
18
|
+
// console.warn(message, ...rest)
|
|
19
|
+
// })
|
|
19
20
|
|
|
20
|
-
export const logError = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// export const logError = <A>(message: A, ...rest: any[]): Effect.Effect<void> =>
|
|
22
|
+
// Effect.sync(() => {
|
|
23
|
+
// console.error(message, ...rest)
|
|
24
|
+
// })
|
|
24
25
|
|
|
25
26
|
const getThreadName = () =>
|
|
26
27
|
isNonEmptyString(self.name) ? self.name : typeof window === 'object' ? 'Browser Main Thread' : 'unknown-thread'
|
|
@@ -29,17 +30,51 @@ const getThreadName = () =>
|
|
|
29
30
|
export const tapCauseLogPretty = <R, E, A>(eff: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
30
31
|
Effect.tapErrorCause(eff, (err) => {
|
|
31
32
|
if (Cause.isInterruptedOnly(err)) {
|
|
33
|
+
// console.log('interrupted', Cause.pretty(err), err)
|
|
32
34
|
return Effect.void
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const threadName = getThreadName()
|
|
36
38
|
|
|
37
39
|
// const prettyError = (err as any).error ? (err as any).error.toString() : Cause.pretty(err)
|
|
38
|
-
const prettyError = Cause.pretty(err)
|
|
40
|
+
// const prettyError = Cause.pretty(err)
|
|
39
41
|
|
|
40
|
-
return logError(`Error on ${threadName}:`, prettyError)
|
|
42
|
+
// return Effect.logError(`Error on ${threadName}:`, prettyError)
|
|
43
|
+
const firstErrLine = err.toString().split('\n')[0]
|
|
44
|
+
return Effect.logError(`Error on ${threadName}: ${firstErrLine}`, err)
|
|
41
45
|
})
|
|
42
46
|
|
|
47
|
+
export const logWarnIfTakesLongerThan =
|
|
48
|
+
({ label, duration }: { label: string; duration: Duration.DurationInput }) =>
|
|
49
|
+
<R, E, A>(eff: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
50
|
+
Effect.gen(function* () {
|
|
51
|
+
const runtime = yield* Effect.runtime<never>()
|
|
52
|
+
|
|
53
|
+
let timedOut = false
|
|
54
|
+
|
|
55
|
+
const timeoutFiber = Effect.sleep(duration).pipe(
|
|
56
|
+
Effect.tap(() => {
|
|
57
|
+
timedOut = true
|
|
58
|
+
// TODO include span info
|
|
59
|
+
return Effect.logWarning(`${label}: Took longer than ${duration}ms`)
|
|
60
|
+
}),
|
|
61
|
+
Effect.provide(runtime),
|
|
62
|
+
Effect.runFork,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const start = Date.now()
|
|
66
|
+
const res = yield* eff
|
|
67
|
+
|
|
68
|
+
if (timedOut) {
|
|
69
|
+
const end = Date.now()
|
|
70
|
+
yield* Effect.logWarning(`${label}: Actual duration: ${end - start}ms`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
yield* Fiber.interrupt(timeoutFiber)
|
|
74
|
+
|
|
75
|
+
return res
|
|
76
|
+
})
|
|
77
|
+
|
|
43
78
|
export const tapSync =
|
|
44
79
|
<A>(tapFn: (a: A) => unknown) =>
|
|
45
80
|
<R, E>(eff: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
|
@@ -67,14 +102,15 @@ export const timeoutDieMsg =
|
|
|
67
102
|
|
|
68
103
|
export const toForkedDeferred = <R, E, A>(
|
|
69
104
|
eff: Effect.Effect<A, E, R>,
|
|
70
|
-
): Effect.Effect<Deferred.Deferred<A, E>, never, R> =>
|
|
105
|
+
): Effect.Effect<Deferred.Deferred<A, E>, never, R | Scope.Scope> =>
|
|
71
106
|
pipe(
|
|
72
107
|
Deferred.make<A, E>(),
|
|
73
108
|
Effect.tap((deferred) =>
|
|
74
109
|
pipe(
|
|
75
110
|
Effect.exit(eff),
|
|
76
111
|
Effect.flatMap((ex) => Deferred.done(deferred, ex)),
|
|
77
|
-
|
|
112
|
+
tapCauseLogPretty,
|
|
113
|
+
Effect.forkScoped,
|
|
78
114
|
),
|
|
79
115
|
),
|
|
80
116
|
)
|
package/src/effect/Schema.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { Transferable } from '@effect/platform'
|
|
1
2
|
import { Schema } from '@effect/schema'
|
|
2
|
-
import {
|
|
3
|
+
import type { ParseOptions } from '@effect/schema/AST'
|
|
4
|
+
import type { ParseError } from '@effect/schema/ParseResult'
|
|
5
|
+
import { Effect, Hash } from 'effect'
|
|
3
6
|
|
|
4
7
|
import { objectToString } from '../misc.js'
|
|
5
8
|
|
|
@@ -21,7 +24,7 @@ export const hash = (schema: Schema.Schema<any>) => {
|
|
|
21
24
|
|
|
22
25
|
const errorStructSchema = Schema.Struct({
|
|
23
26
|
message: Schema.String,
|
|
24
|
-
stack: Schema.String,
|
|
27
|
+
stack: Schema.optional(Schema.String),
|
|
25
28
|
})
|
|
26
29
|
|
|
27
30
|
export class AnyError extends Schema.transform(errorStructSchema, Schema.Any, {
|
|
@@ -41,3 +44,16 @@ export class AnyError extends Schema.transform(errorStructSchema, Schema.Any, {
|
|
|
41
44
|
stack: anyError.stack,
|
|
42
45
|
}),
|
|
43
46
|
}) {}
|
|
47
|
+
|
|
48
|
+
export const encodeWithTransferables =
|
|
49
|
+
<A, I, R>(schema: Schema.Schema<A, I, R>, options?: ParseOptions | undefined) =>
|
|
50
|
+
(a: A, overrideOptions?: ParseOptions | undefined): Effect.Effect<[I, Transferable[]], ParseError, R> =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const collector = yield* Transferable.makeCollector
|
|
53
|
+
|
|
54
|
+
const encoded: I = yield* Schema.encode(schema, options)(a, overrideOptions).pipe(
|
|
55
|
+
Effect.provideService(Transferable.Collector, collector),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return [encoded, collector.unsafeRead() as Transferable[]]
|
|
59
|
+
})
|
|
@@ -1,15 +1,30 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { pipe, Stream, SubscriptionRef } from 'effect'
|
|
1
|
+
import { Chunk, Effect, pipe, Stream, SubscriptionRef } from 'effect'
|
|
3
2
|
import { dual } from 'effect/Function'
|
|
3
|
+
import type { Predicate, Refinement } from 'effect/Predicate'
|
|
4
4
|
|
|
5
5
|
export * from 'effect/SubscriptionRef'
|
|
6
6
|
|
|
7
7
|
export const changeStreamIncludingCurrent = <A>(sref: SubscriptionRef.SubscriptionRef<A>) =>
|
|
8
8
|
pipe(Stream.fromEffect(SubscriptionRef.get(sref)), Stream.concat(sref.changes))
|
|
9
9
|
|
|
10
|
-
export const waitUntil
|
|
11
|
-
<A
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
export const waitUntil: {
|
|
11
|
+
<A, B extends A>(
|
|
12
|
+
refinement: Refinement<NoInfer<A>, B>,
|
|
13
|
+
): (sref: SubscriptionRef.SubscriptionRef<A>) => Effect.Effect<B, never, never>
|
|
14
|
+
<A, B extends A>(
|
|
15
|
+
predicate: Predicate<B>,
|
|
16
|
+
): (sref: SubscriptionRef.SubscriptionRef<A>) => Effect.Effect<A, never, never>
|
|
17
|
+
<A, B extends A>(
|
|
18
|
+
sref: SubscriptionRef.SubscriptionRef<A>,
|
|
19
|
+
refinement: Refinement<NoInfer<A>, B>,
|
|
20
|
+
): Effect.Effect<B, never, never>
|
|
21
|
+
<A, B extends A>(sref: SubscriptionRef.SubscriptionRef<A>, predicate: Predicate<B>): Effect.Effect<A, never, never>
|
|
22
|
+
} = dual(2, <A>(sref: SubscriptionRef.SubscriptionRef<A>, predicate: (a: A) => boolean) =>
|
|
23
|
+
pipe(
|
|
24
|
+
changeStreamIncludingCurrent(sref),
|
|
25
|
+
Stream.filter(predicate),
|
|
26
|
+
Stream.take(1),
|
|
27
|
+
Stream.runCollect,
|
|
28
|
+
Effect.map(Chunk.unsafeHead),
|
|
29
|
+
),
|
|
15
30
|
)
|
package/src/effect/WebLock.ts
CHANGED
|
@@ -3,18 +3,36 @@ import { Deferred, Effect, Runtime } from 'effect'
|
|
|
3
3
|
|
|
4
4
|
// See https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API
|
|
5
5
|
export const withLock =
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
<E2>({
|
|
7
|
+
lockName,
|
|
8
|
+
onTaken,
|
|
9
|
+
options,
|
|
10
|
+
}: {
|
|
11
|
+
lockName: string
|
|
12
|
+
onTaken?: Effect.Effect<void, E2>
|
|
13
|
+
options?: Omit<LockOptions, 'signal'>
|
|
14
|
+
}) =>
|
|
15
|
+
<Ctx, E, A>(eff: Effect.Effect<A, E, Ctx>): Effect.Effect<A | void, E | E2, Ctx> =>
|
|
8
16
|
Effect.gen(function* ($) {
|
|
9
17
|
const runtime = yield* $(Effect.runtime<Ctx>())
|
|
10
18
|
|
|
11
19
|
const exit = yield* $(
|
|
12
|
-
Effect.tryPromise<Exit.Exit<A, E>, E>({
|
|
20
|
+
Effect.tryPromise<Exit.Exit<A, E>, E | E2>({
|
|
13
21
|
try: (signal) => {
|
|
22
|
+
if (signal.aborted) return 'aborted' as never
|
|
23
|
+
|
|
14
24
|
// NOTE The 'signal' and 'ifAvailable' options cannot be used together.
|
|
15
25
|
const requestOptions = options?.ifAvailable === true ? options : { ...options, signal }
|
|
16
26
|
return navigator.locks.request(lockName, requestOptions, async (lock) => {
|
|
17
|
-
if (lock === null)
|
|
27
|
+
if (lock === null) {
|
|
28
|
+
if (onTaken) {
|
|
29
|
+
const exit = await Runtime.runPromiseExit(runtime)(onTaken)
|
|
30
|
+
if (exit._tag === 'Failure') {
|
|
31
|
+
return exit
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return
|
|
35
|
+
}
|
|
18
36
|
|
|
19
37
|
// TODO also propagate Effect interruption to the execution
|
|
20
38
|
return Runtime.runPromiseExit(runtime)(eff)
|
|
@@ -33,25 +51,35 @@ export const withLock =
|
|
|
33
51
|
|
|
34
52
|
export const waitForDeferredLock = (deferred: Deferred.Deferred<void>, lockName: string) =>
|
|
35
53
|
Effect.async<void>((cb, signal) => {
|
|
36
|
-
|
|
37
|
-
// immediately continuing calling Effect since we have the lock
|
|
38
|
-
cb(Effect.void)
|
|
54
|
+
if (signal.aborted) return
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
navigator.locks
|
|
57
|
+
.request(lockName, { signal, mode: 'exclusive', ifAvailable: false }, (_lock) => {
|
|
58
|
+
// immediately continuing calling Effect since we have the lock
|
|
59
|
+
cb(Effect.void)
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
61
|
+
// the code below is still running
|
|
62
|
+
|
|
63
|
+
// holding lock until deferred is resolved
|
|
64
|
+
return Effect.runPromise(Deferred.await(deferred))
|
|
65
|
+
})
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
if (error.code === 20 && error.message === 'signal is aborted without reason') {
|
|
68
|
+
// Given signal interruption is handled via Effect, we can ignore this case
|
|
69
|
+
} else {
|
|
70
|
+
throw error
|
|
71
|
+
}
|
|
72
|
+
})
|
|
45
73
|
})
|
|
46
74
|
|
|
47
75
|
export const tryGetDeferredLock = (deferred: Deferred.Deferred<void>, lockName: string) =>
|
|
48
76
|
Effect.async<boolean>((cb) => {
|
|
49
|
-
navigator.locks.request(lockName, { mode: 'exclusive', ifAvailable: true },
|
|
77
|
+
navigator.locks.request(lockName, { mode: 'exclusive', ifAvailable: true }, (lock) => {
|
|
50
78
|
cb(Effect.succeed(lock !== null))
|
|
51
79
|
|
|
52
80
|
// the code below is still running
|
|
53
81
|
|
|
54
82
|
// holding lock until deferred is resolved
|
|
55
|
-
|
|
83
|
+
return Effect.runPromise(Deferred.await(deferred))
|
|
56
84
|
})
|
|
57
85
|
})
|
package/src/effect/index.ts
CHANGED
|
@@ -36,6 +36,8 @@ export {
|
|
|
36
36
|
HashSet,
|
|
37
37
|
MutableHashSet,
|
|
38
38
|
Option,
|
|
39
|
+
LogLevel,
|
|
40
|
+
Logger,
|
|
39
41
|
Layer,
|
|
40
42
|
STM,
|
|
41
43
|
TRef,
|
|
@@ -55,6 +57,7 @@ export {
|
|
|
55
57
|
TreeFormatter,
|
|
56
58
|
AST as SchemaAST,
|
|
57
59
|
Pretty as SchemaPretty,
|
|
60
|
+
Equivalence as SchemaEquivalence,
|
|
58
61
|
Serializable,
|
|
59
62
|
JSONSchema,
|
|
60
63
|
ParseResult,
|
|
@@ -64,6 +67,7 @@ export * as OtelTracer from '@effect/opentelemetry/Tracer'
|
|
|
64
67
|
|
|
65
68
|
export { Transferable, FileSystem, Worker, WorkerError, WorkerRunner, Terminal, HttpServer } from '@effect/platform'
|
|
66
69
|
export { BrowserWorker, BrowserWorkerRunner } from '@effect/platform-browser'
|
|
70
|
+
export * as PortPlatformRunner from './port-platform-runner.js'
|
|
67
71
|
|
|
68
72
|
export * as Effect from './Effect.js'
|
|
69
73
|
export * as Schedule from './Schedule.js'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { WorkerError } from '@effect/platform/WorkerError'
|
|
2
|
+
import * as Runner from '@effect/platform/WorkerRunner'
|
|
3
|
+
import { Deferred } from 'effect'
|
|
4
|
+
import * as Cause from 'effect/Cause'
|
|
5
|
+
import * as Effect from 'effect/Effect'
|
|
6
|
+
import * as Layer from 'effect/Layer'
|
|
7
|
+
import * as Queue from 'effect/Queue'
|
|
8
|
+
import * as Schedule from 'effect/Schedule'
|
|
9
|
+
|
|
10
|
+
const platformRunnerImpl = (port: MessagePort) =>
|
|
11
|
+
Runner.PlatformRunner.of({
|
|
12
|
+
[Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId,
|
|
13
|
+
start: <I, O>(shutdown: Effect.Effect<void>) => {
|
|
14
|
+
return Effect.gen(function* () {
|
|
15
|
+
const queue = yield* Queue.unbounded<readonly [portId: number, message: I]>()
|
|
16
|
+
|
|
17
|
+
const latch = yield* Deferred.make<void>()
|
|
18
|
+
|
|
19
|
+
yield* Effect.async<never, WorkerError>((resume) => {
|
|
20
|
+
const onMessage = (msg: MessageEvent<Runner.BackingRunner.Message<I>>) => {
|
|
21
|
+
const message = msg.data
|
|
22
|
+
if (message[0] === 0) {
|
|
23
|
+
queue.unsafeOffer([0, message[1]])
|
|
24
|
+
} else {
|
|
25
|
+
Effect.runFork(shutdown)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const onError = (error: any) => {
|
|
30
|
+
resume(new WorkerError({ reason: 'decode', error }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
port.addEventListener('message', onMessage)
|
|
34
|
+
port.addEventListener('messageerror', onError)
|
|
35
|
+
port.addEventListener('error', onError)
|
|
36
|
+
|
|
37
|
+
Deferred.unsafeDone(latch, Effect.void)
|
|
38
|
+
|
|
39
|
+
return Effect.sync(() => {
|
|
40
|
+
port.removeEventListener('message', onMessage as any)
|
|
41
|
+
port.removeEventListener('error', onError as any)
|
|
42
|
+
})
|
|
43
|
+
}).pipe(
|
|
44
|
+
Effect.tapErrorCause((cause) => (Cause.isInterruptedOnly(cause) ? Effect.void : Effect.logDebug(cause))),
|
|
45
|
+
Effect.retry(Schedule.forever),
|
|
46
|
+
Effect.annotateLogs({
|
|
47
|
+
package: '@livestore/utils/effect',
|
|
48
|
+
module: 'PortPlatformRunner',
|
|
49
|
+
}),
|
|
50
|
+
Effect.interruptible,
|
|
51
|
+
Effect.forkScoped,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
yield* Deferred.await(latch)
|
|
55
|
+
|
|
56
|
+
port.start()
|
|
57
|
+
|
|
58
|
+
const send = (_portId: number, message: O, transfers?: ReadonlyArray<unknown>) =>
|
|
59
|
+
Effect.try({
|
|
60
|
+
try: () => port.postMessage([1, message], transfers as any),
|
|
61
|
+
catch: (error) => new WorkerError({ reason: 'send', error }),
|
|
62
|
+
}).pipe(Effect.catchTag('WorkerError', Effect.orDie))
|
|
63
|
+
|
|
64
|
+
// ready
|
|
65
|
+
port.postMessage([0])
|
|
66
|
+
|
|
67
|
+
return { queue, send }
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/** @internal */
|
|
73
|
+
export const layer = (port: MessagePort) => Layer.succeed(Runner.PlatformRunner, platformRunnerImpl(port))
|