@livestore/utils 0.0.54-dev.22 → 0.0.54-dev.23

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.
@@ -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
- Effect.sync(() => {
12
- console.log(message, ...rest)
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
- Effect.sync(() => {
17
- console.warn(message, ...rest)
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
- Effect.sync(() => {
22
- console.error(message, ...rest)
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
- Effect.forkDaemon,
112
+ tapCauseLogPretty,
113
+ Effect.forkScoped,
78
114
  ),
79
115
  ),
80
116
  )
@@ -1,5 +1,8 @@
1
+ import { Transferable } from '@effect/platform'
1
2
  import { Schema } from '@effect/schema'
2
- import { Hash } from 'effect'
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 type { Effect } from 'effect'
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 = dual<
11
- <A>(predicate: (a: A) => boolean) => (sref: SubscriptionRef.SubscriptionRef<A>) => Effect.Effect<void>,
12
- <A>(sref: SubscriptionRef.SubscriptionRef<A>, predicate: (a: A) => boolean) => Effect.Effect<void>
13
- >(2, <A>(sref: SubscriptionRef.SubscriptionRef<A>, predicate: (a: A) => boolean) =>
14
- pipe(changeStreamIncludingCurrent(sref), Stream.filter(predicate), Stream.take(1), Stream.runDrain),
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
  )
@@ -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
- (lockName: string, options?: Omit<LockOptions, 'signal'>) =>
7
- <Ctx, E, A>(eff: Effect.Effect<A, E, Ctx>): Effect.Effect<A | void, E, Ctx> =>
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) return
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
- navigator.locks.request(lockName, { signal, mode: 'exclusive', ifAvailable: false }, async (_lock) => {
37
- // immediately continuing calling Effect since we have the lock
38
- cb(Effect.void)
54
+ if (signal.aborted) return
39
55
 
40
- // the code below is still running
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
- // holding lock until deferred is resolved
43
- await Effect.runPromise(Deferred.await(deferred))
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 }, async (lock) => {
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
- await Effect.runPromise(Deferred.await(deferred))
83
+ return Effect.runPromise(Deferred.await(deferred))
56
84
  })
57
85
  })
@@ -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))