@livestore/utils 0.2.0-dev.2 → 0.3.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) 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/nanoid/index.browser.d.ts +2 -0
  61. package/dist/nanoid/index.browser.d.ts.map +1 -0
  62. package/dist/nanoid/index.browser.js +3 -0
  63. package/dist/nanoid/index.browser.js.map +1 -0
  64. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts +5 -0
  65. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -0
  66. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +55 -0
  67. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -0
  68. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +10 -0
  69. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -0
  70. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +46 -0
  71. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -0
  72. package/dist/node/mod.d.ts +22 -0
  73. package/dist/node/mod.d.ts.map +1 -0
  74. package/dist/node/mod.js +83 -0
  75. package/dist/node/mod.js.map +1 -0
  76. package/dist/node-vitest/mod.d.ts +2 -0
  77. package/dist/node-vitest/mod.d.ts.map +1 -0
  78. package/dist/node-vitest/mod.js +2 -0
  79. package/dist/node-vitest/mod.js.map +1 -0
  80. package/dist/node-vitest/polyfill.d.ts +2 -0
  81. package/dist/node-vitest/polyfill.d.ts.map +1 -0
  82. package/dist/node-vitest/polyfill.js +3 -0
  83. package/dist/node-vitest/polyfill.js.map +1 -0
  84. package/dist/object/index.d.ts.map +1 -1
  85. package/dist/object/pick.d.ts.map +1 -1
  86. package/package.json +50 -15
  87. package/src/NoopTracer.ts +1 -1
  88. package/src/base64.ts +1 -1
  89. package/src/effect/BucketQueue.ts +22 -0
  90. package/src/effect/Effect.ts +41 -17
  91. package/src/effect/Logger.ts +17 -0
  92. package/src/effect/Schema/index.ts +1 -1
  93. package/src/effect/TaskTracing.ts +43 -0
  94. package/src/effect/WebChannel/broadcastChannelWithAck.ts +126 -0
  95. package/src/effect/WebChannel/common.ts +17 -0
  96. package/src/effect/WebChannel.ts +223 -49
  97. package/src/effect/WebLock.ts +75 -0
  98. package/src/effect/WebSocket.ts +72 -0
  99. package/src/effect/index.ts +14 -1
  100. package/src/env.ts +31 -0
  101. package/src/index.ts +3 -11
  102. package/src/nanoid/index.browser.ts +2 -0
  103. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +63 -0
  104. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +66 -0
  105. package/src/node/mod.ts +115 -0
  106. package/src/node-vitest/mod.ts +1 -0
  107. package/src/node-vitest/polyfill.ts +1 -0
  108. package/tmp/effect-deferred-repro.ts +29 -0
  109. package/tmp/effect-semaphore-repro.ts +93 -0
@@ -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
+ })
@@ -39,9 +39,11 @@ export {
39
39
  HashSet,
40
40
  MutableHashSet,
41
41
  MutableHashMap,
42
+ TQueue,
42
43
  Option,
43
44
  LogLevel,
44
- Logger,
45
+ // Logger,
46
+ Config,
45
47
  Layer,
46
48
  STM,
47
49
  TRef,
@@ -51,15 +53,21 @@ export {
51
53
  identity,
52
54
  GlobalValue,
53
55
  Match,
56
+ TestServices,
54
57
  } from 'effect'
55
58
 
56
59
  export { dual } from 'effect/Function'
57
60
 
58
61
  export * as Stream from './Stream.js'
59
62
 
63
+ export * as BucketQueue from './BucketQueue.js'
64
+
60
65
  export * as SubscriptionRef from './SubscriptionRef.js'
61
66
 
67
+ export * as Logger from './Logger.js'
68
+
62
69
  export * as WebChannel from './WebChannel.js'
70
+ export * as WebSocket from './WebSocket.js'
63
71
 
64
72
  export * as SchemaAST from 'effect/SchemaAST'
65
73
  export { TreeFormatter } from 'effect/ParseResult'
@@ -67,6 +75,8 @@ export { ParseResult, Pretty } from 'effect'
67
75
  export type { Serializable, SerializableWithResult } from 'effect/Schema'
68
76
  export * as Schema from './Schema/index.js'
69
77
  export * as OtelTracer from '@effect/opentelemetry/Tracer'
78
+ // export * as OtelResource from '@effect/opentelemetry/Resource'
79
+ export * as TaskTracing from './TaskTracing.js'
70
80
 
71
81
  export {
72
82
  Transferable,
@@ -81,9 +91,12 @@ export {
81
91
  HttpClientRequest,
82
92
  HttpClientResponse,
83
93
  FetchHttpClient,
94
+ Socket,
84
95
  } from '@effect/platform'
85
96
  export { BrowserWorker, BrowserWorkerRunner } from '@effect/platform-browser'
86
97
 
98
+ // export { DevTools as EffectDevtools } from '@effect/experimental'
99
+
87
100
  export * as Effect from './Effect.js'
88
101
  export * as Schedule from './Schedule.js'
89
102
  export * as Scheduler from './Scheduler.js'
package/src/env.ts ADDED
@@ -0,0 +1,31 @@
1
+ export const env = (name: string): string | undefined => {
2
+ if (typeof process !== 'undefined' && process.env !== undefined) {
3
+ return process.env[name]
4
+ }
5
+
6
+ // TODO re-enable the full guard code once `import.meta` is supported in Expo
7
+ // if (import.meta !== undefined && import.meta.env !== undefined) {
8
+ if (import.meta.env !== undefined) {
9
+ return import.meta.env[name]
10
+ }
11
+
12
+ return undefined
13
+ }
14
+
15
+ export const isDevEnv = () => {
16
+ if (typeof process !== 'undefined' && process.env !== undefined) {
17
+ return process.env.NODE_ENV !== 'production'
18
+ }
19
+
20
+ // TODO re-enable the full guard code once `import.meta` is supported in Expo
21
+ // if (import.meta !== undefined && import.meta.env !== undefined) {
22
+ if (import.meta.env !== undefined) {
23
+ return import.meta.env.DEV
24
+ }
25
+
26
+ return false
27
+ }
28
+
29
+ export const TRACE_VERBOSE = env('LS_TRACE_VERBOSE') !== undefined || env('VITE_LS_TRACE_VERBOSE') !== undefined
30
+
31
+ export const LS_DEV = env('LS_DEV') !== undefined || env('VITE_LS_DEV') !== undefined
package/src/index.ts CHANGED
@@ -8,12 +8,14 @@ export * from './set.js'
8
8
  export * from './browser.js'
9
9
  export * from './Deferred.js'
10
10
  export * from './misc.js'
11
+ export * from './env.js'
11
12
  export * from './fast-deep-equal.js'
12
13
  export * as base64 from './base64.js'
13
14
  export { default as prettyBytes } from 'pretty-bytes'
14
15
 
15
16
  import type * as otel from '@opentelemetry/api'
16
17
 
18
+ import { isDevEnv } from './env.js'
17
19
  import { objectToString } from './misc.js'
18
20
 
19
21
  export type Prettify<T> = T extends infer U ? { [K in keyof U]: Prettify<U[K]> } : never
@@ -88,7 +90,7 @@ export function casesHandled(unexpectedCase: never): never {
88
90
 
89
91
  export const shouldNeverHappen = (msg?: string, ...args: any[]): never => {
90
92
  console.error(msg, ...args)
91
- if (isDev()) {
93
+ if (isDevEnv()) {
92
94
  debugger
93
95
  }
94
96
 
@@ -228,13 +230,3 @@ export const isPromise = (value: any): value is Promise<unknown> => typeof value
228
230
  export const isIterable = <T>(value: any): value is Iterable<T> => typeof value?.[Symbol.iterator] === 'function'
229
231
 
230
232
  export { objectToString as errorToString } from './misc.js'
231
-
232
- const isDev = memoizeByRef(() => {
233
- if (import.meta.env !== undefined) {
234
- return import.meta.env.DEV || import.meta.env.VITE_DEV
235
- } else if (typeof process !== 'undefined' && process.env !== undefined) {
236
- return process.env.DEV
237
- } else {
238
- return false
239
- }
240
- })
@@ -0,0 +1,2 @@
1
+ // @ts-expect-error TODO
2
+ export { nanoid } from 'nanoid/index.browser.js'
@@ -0,0 +1,63 @@
1
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
+ import process from 'node:process'
3
+
4
+ import { WorkerError } from '@effect/platform/WorkerError'
5
+ import * as Runner from '@effect/platform/WorkerRunner'
6
+ import { FiberId } from 'effect'
7
+ import * as Context from 'effect/Context'
8
+ import * as Deferred from 'effect/Deferred'
9
+ import * as Effect from 'effect/Effect'
10
+ import * as Exit from 'effect/Exit'
11
+ import * as FiberSet from 'effect/FiberSet'
12
+ import * as Layer from 'effect/Layer'
13
+ import * as Runtime from 'effect/Runtime'
14
+ import * as Scope from 'effect/Scope'
15
+
16
+ const platformRunnerImpl = Runner.PlatformRunner.of({
17
+ [Runner.PlatformRunnerTypeId]: Runner.PlatformRunnerTypeId,
18
+ start<I, O>() {
19
+ return Effect.gen(function* () {
20
+ if (!process.send) {
21
+ return yield* new WorkerError({ reason: 'spawn', cause: new Error('not in a child process') })
22
+ }
23
+ const port = {
24
+ postMessage: (message: any) => process.send!(message),
25
+ on: (event: string, handler: (message: any) => void) => process.on(event, handler),
26
+ close: () => process.disconnect(),
27
+ }
28
+ const send = (_portId: number, message: O, _transfers?: ReadonlyArray<unknown>) =>
29
+ Effect.sync(() => port.postMessage([1, message] /*, transfers as any*/))
30
+ const run = <A, E, R>(handler: (portId: number, message: I) => Effect.Effect<A, E, R>) =>
31
+ Effect.uninterruptibleMask((restore) =>
32
+ Effect.gen(function* () {
33
+ const runtime = (yield* Effect.runtime<R | Scope.Scope>()).pipe(
34
+ Runtime.updateContext(Context.omit(Scope.Scope)),
35
+ ) as Runtime.Runtime<R>
36
+ const fiberSet = yield* FiberSet.make<any, WorkerError | E>()
37
+ const runFork = Runtime.runFork(runtime)
38
+ port.on('message', (message: Runner.BackingRunner.Message<I>) => {
39
+ if (message[0] === 0) {
40
+ FiberSet.unsafeAdd(fiberSet, runFork(restore(handler(0, message[1]))))
41
+ } else {
42
+ Deferred.unsafeDone(fiberSet.deferred, Exit.interrupt(FiberId.none))
43
+ port.close()
44
+ }
45
+ })
46
+ port.on('messageerror', (cause) => {
47
+ Deferred.unsafeDone(fiberSet.deferred, new WorkerError({ reason: 'decode', cause }))
48
+ })
49
+ port.on('error', (cause) => {
50
+ Deferred.unsafeDone(fiberSet.deferred, new WorkerError({ reason: 'unknown', cause }))
51
+ })
52
+ port.postMessage([0])
53
+ return (yield* restore(FiberSet.join(fiberSet))) as never
54
+ }).pipe(Effect.scoped),
55
+ )
56
+
57
+ return { run, send }
58
+ })
59
+ },
60
+ })
61
+
62
+ /** @internal */
63
+ export const layer = Layer.succeed(Runner.PlatformRunner, platformRunnerImpl)
@@ -0,0 +1,66 @@
1
+ /* eslint-disable prefer-arrow/prefer-arrow-functions */
2
+
3
+ import type * as ChildProcess from 'node:child_process'
4
+
5
+ import * as Worker from '@effect/platform/Worker'
6
+ import { WorkerError } from '@effect/platform/WorkerError'
7
+ import * as Deferred from 'effect/Deferred'
8
+ import * as Effect from 'effect/Effect'
9
+ import * as Exit from 'effect/Exit'
10
+ import * as Layer from 'effect/Layer'
11
+ import * as Scope from 'effect/Scope'
12
+
13
+ const platformWorkerImpl = Worker.makePlatform<ChildProcess.ChildProcess>()({
14
+ setup({ scope, worker: childProcess }) {
15
+ return Effect.flatMap(Deferred.make<void, WorkerError>(), (exitDeferred) => {
16
+ childProcess.on('exit', () => {
17
+ Deferred.unsafeDone(exitDeferred, Exit.void)
18
+ })
19
+ return Effect.as(
20
+ Scope.addFinalizer(
21
+ scope,
22
+ Effect.suspend(() => {
23
+ childProcess.send([1])
24
+ return Deferred.await(exitDeferred)
25
+ }).pipe(
26
+ Effect.interruptible,
27
+ Effect.timeout(5000),
28
+ Effect.catchAllCause(() => Effect.sync(() => childProcess.kill())),
29
+ ),
30
+ ),
31
+ {
32
+ postMessage: (message: any) => childProcess.send(message),
33
+ on: (event: string, handler: (message: any) => void) => childProcess.on(event, handler),
34
+ },
35
+ )
36
+ })
37
+ },
38
+ listen({ deferred, emit, port }) {
39
+ port.on('message', (message) => {
40
+ emit(message)
41
+ })
42
+ port.on('messageerror', (cause) => {
43
+ Deferred.unsafeDone(deferred, new WorkerError({ reason: 'decode', cause }))
44
+ })
45
+ port.on('error', (cause) => {
46
+ Deferred.unsafeDone(deferred, new WorkerError({ reason: 'unknown', cause }))
47
+ })
48
+ port.on('exit', (code) => {
49
+ Deferred.unsafeDone(
50
+ deferred,
51
+ new WorkerError({ reason: 'unknown', cause: new Error(`exited with code ${code}`) }),
52
+ )
53
+ })
54
+ return Effect.void
55
+ },
56
+ })
57
+
58
+ /** @internal */
59
+ export const layerWorker = Layer.succeed(Worker.PlatformWorker, platformWorkerImpl)
60
+
61
+ /** @internal */
62
+ export const layerManager = Layer.provide(Worker.layerManager, layerWorker)
63
+
64
+ /** @internal */
65
+ export const layer = (spawn: (id: number) => ChildProcess.ChildProcess) =>
66
+ Layer.merge(layerManager, Worker.layerSpawner(spawn))
@@ -0,0 +1,115 @@
1
+ import * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
2
+ import type * as otel from '@opentelemetry/api'
3
+ import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
4
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
5
+ import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'
6
+ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
7
+ import { Config, Effect, Layer } from 'effect'
8
+ import type { ParentSpan } from 'effect/Tracer'
9
+
10
+ import { tapCauseLogPretty } from '../effect/Effect.js'
11
+ import type { OtelTracer } from '../effect/index.js'
12
+
13
+ // import { tapCauseLogPretty } from '../effect/Effect.js'
14
+
15
+ export * as Cli from '@effect/cli'
16
+ export * as PlatformBun from '@effect/platform-bun'
17
+ export * as PlatformNode from '@effect/platform-node'
18
+ export * as SocketServer from '@effect/experimental/SocketServer'
19
+ export * as OtelResource from '@effect/opentelemetry/Resource'
20
+
21
+ export { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
22
+ export { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http'
23
+ export * as OtelNodeSdk from '@effect/opentelemetry/NodeSdk'
24
+
25
+ export * as ChildProcessRunner from './ChildProcessRunner/ChildProcessRunner.js'
26
+ export * as ChildProcessWorker from './ChildProcessRunner/ChildProcessWorker.js'
27
+
28
+ // Enable debug logging for OpenTelemetry
29
+ // otel.diag.setLogger(new otel.DiagConsoleLogger(), otel.DiagLogLevel.ERROR)
30
+
31
+ // export const OtelLiveHttp = (args: any): Layer.Layer<never> => Layer.empty
32
+
33
+ export const OtelLiveHttp = ({
34
+ serviceName,
35
+ rootSpanName,
36
+ skipLogUrl,
37
+ }: { serviceName?: string; rootSpanName?: string; skipLogUrl?: boolean } = {}): Layer.Layer<
38
+ OtelTracer.OtelTracer | ParentSpan,
39
+ never,
40
+ never
41
+ > =>
42
+ Effect.gen(function* () {
43
+ const config = yield* Config.all({
44
+ exporterUrlTracing: Config.string('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'),
45
+ exporterUrlMracing: Config.string('OTEL_EXPORTER_OTLP_METRICS_ENDPOINT'),
46
+ serviceName: serviceName
47
+ ? Config.succeed(serviceName)
48
+ : Config.string('OTEL_SERVICE_NAME').pipe(Config.withDefault('overtone-node-utils-default-service')),
49
+ rootSpanName: rootSpanName
50
+ ? Config.succeed(rootSpanName)
51
+ : Config.string('OTEL_ROOT_SPAN_NAME').pipe(Config.withDefault('RootSpan')),
52
+ }).pipe(tapCauseLogPretty, Effect.orDie)
53
+
54
+ const resource = { serviceName: config.serviceName }
55
+
56
+ // METRICS
57
+ const metricExporter = new OTLPMetricExporter({ url: config.exporterUrlMracing })
58
+
59
+ const metricReader = new PeriodicExportingMetricReader({
60
+ exporter: metricExporter,
61
+ exportIntervalMillis: 1000,
62
+ })
63
+
64
+ // TRACING
65
+ const OtelLive = OtelNodeSdk.layer(() => ({
66
+ resource,
67
+ metricReader,
68
+ spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({ url: config.exporterUrlTracing, headers: {} })),
69
+ }))
70
+
71
+ const RootSpanLive = Layer.span(config.rootSpanName, {
72
+ attributes: { config },
73
+ onEnd: skipLogUrl ? undefined : (span: any) => logTraceUiUrlForSpan()(span.span),
74
+ })
75
+
76
+ return RootSpanLive.pipe(Layer.provideMerge(OtelLive))
77
+ }).pipe(Layer.unwrapEffect) as any
78
+
79
+ export const logTraceUiUrlForSpan = (printMsg?: (url: string) => string) => (span: otel.Span) =>
80
+ getTracingBackendUrl(span).pipe(
81
+ Effect.tap((url) => {
82
+ if (url === undefined) {
83
+ console.warn('No tracing backend url found')
84
+ } else {
85
+ if (printMsg) {
86
+ console.log(printMsg(url))
87
+ } else {
88
+ console.log(`Trace URL: ${url}`)
89
+ }
90
+ }
91
+ }),
92
+ )
93
+
94
+ export const getTracingBackendUrl = (span: otel.Span) =>
95
+ Effect.gen(function* () {
96
+ const endpoint = yield* Config.string('TRACING_UI_ENDPOINT').pipe(Config.option, Effect.orDie)
97
+ if (endpoint._tag === 'None') return
98
+
99
+ const traceId = span.spanContext().traceId
100
+
101
+ // Grafana + Tempo
102
+
103
+ const grafanaEndpoint = endpoint.value
104
+ const searchParams = new URLSearchParams({
105
+ orgId: '1',
106
+ left: JSON.stringify({
107
+ datasource: 'tempo',
108
+ queries: [{ query: traceId, queryType: 'traceql', refId: 'A' }],
109
+ range: { from: 'now-1h', to: 'now' },
110
+ }),
111
+ })
112
+
113
+ // TODO make dynamic via env var
114
+ return `${grafanaEndpoint}/explore?${searchParams.toString()}`
115
+ })
@@ -0,0 +1 @@
1
+ export * as Vitest from '@effect/vitest'
@@ -0,0 +1 @@
1
+ process.stdout.isTTY = true
@@ -0,0 +1,29 @@
1
+ /// <reference lib="es2024" />
2
+
3
+ import { Effect, Logger } from 'effect'
4
+
5
+ const main = Effect.gen(function* () {
6
+ yield* Effect.addFinalizer((_) => Effect.log('finalizer', _))
7
+
8
+ // const finished = yield* Deferred.make<void>()
9
+
10
+ const def = Promise.withResolvers()
11
+
12
+ setTimeout(() => {
13
+ def.resolve(undefined)
14
+ }, 1000)
15
+
16
+ console.log('awaiting')
17
+
18
+ // const fiber = yield* Deferred.await(finished).pipe(Effect.fork)
19
+ // yield* Fiber.join(fiber)
20
+ // yield* finished
21
+ yield* Effect.promise(() => def.promise)
22
+ console.log('finished')
23
+ })
24
+
25
+ main
26
+ .pipe(Effect.scoped, Effect.provide(Logger.pretty), Effect.tapErrorCause(Effect.logError), Effect.runPromise)
27
+ .finally(() => {
28
+ console.log('finally')
29
+ })
@@ -0,0 +1,93 @@
1
+ /// <reference lib="es2024" />
2
+ import * as NodePlatform from '@effect/platform-node'
3
+ import { Deferred, Effect, FiberSet, Logger, Ref, Scheduler } from 'effect'
4
+
5
+ const main = Effect.gen(function* () {
6
+ const semaphore = yield* Effect.makeSemaphore(1)
7
+
8
+ const fiberset = yield* FiberSet.make()
9
+
10
+ // setInterval(() => {
11
+ // // semaphore.release(1)
12
+ // }, 1_000_000)
13
+
14
+ const stateRef = yield* Ref.make<'open' | 'closed'>('open')
15
+
16
+ // let counter = 0
17
+ // const nextExpectedCounter = 0
18
+ // const def = Promise.withResolvers()
19
+
20
+ const finished = yield* Deferred.make<void>()
21
+
22
+ const MAX = 100
23
+
24
+ const doWorkWithSemaphore = (counter: number) =>
25
+ Effect.gen(function* () {
26
+ // yield* Effect.log(`doWorkWithSemaphore: ${counter} - waiting for semaphore`)
27
+ // yield* semaphore.take(1)
28
+ // if (counter !== nextExpectedCounter) {
29
+ // throw new Error(`Expected counter ${nextExpectedCounter} but got ${counter}`)
30
+ // }
31
+ // nextExpectedCounter = counter + 1
32
+ yield* Effect.log(`doWorkWithSemaphore: ${counter} - got semaphore`)
33
+ // setTimeout(() => {}, 120)
34
+ yield* Effect.sleep(40)
35
+ // yield* Effect.yieldNow()
36
+ yield* Effect.log(`doWorkWithSemaphore: ${counter} - releasing semaphore`)
37
+ yield* Ref.set(stateRef, 'open')
38
+ yield* semaphore.release(1)
39
+
40
+ if (counter === MAX - 1) {
41
+ yield* Deferred.succeed(finished, undefined)
42
+ // def.resolve(undefined)
43
+ }
44
+ }).pipe(Effect.scoped)
45
+
46
+ for (let i = 0; i < MAX; i++) {
47
+ yield* doWorkWithSemaphore(i).pipe(FiberSet.run(fiberset))
48
+ // yield* Effect.sleep(20)
49
+ yield* Effect.gen(function* () {
50
+ const state = yield* Ref.get(stateRef)
51
+ if (state === 'open') {
52
+ yield* Effect.logWarning(`loop: ${i} - semaphore is open, taking`)
53
+ } else {
54
+ yield* Effect.log(`loop: ${i} - semaphore is closed, waiting`)
55
+ }
56
+
57
+ yield* semaphore.take(1)
58
+ yield* Ref.set(stateRef, 'closed')
59
+
60
+ yield* doWorkWithSemaphore(i).pipe(Effect.fork)
61
+
62
+ const sleepTime = Math.random() * 40
63
+ yield* Effect.log(`loop: ${i} - sleeping for ${sleepTime}ms`)
64
+ yield* Effect.sleep(sleepTime)
65
+
66
+ // yield* Effect.yieldNow()
67
+ }).pipe(FiberSet.run(fiberset))
68
+ }
69
+
70
+ // yield* Effect.never
71
+
72
+ yield* finished
73
+ // yield* Effect.promise(() => def.promise)
74
+ // console.log('finished')
75
+ })
76
+
77
+ export const messageChannel = (shouldYield: Scheduler.Scheduler['shouldYield'] = Scheduler.defaultShouldYield) =>
78
+ Scheduler.makeBatched((task) => {
79
+ const messageChannel = new MessageChannel()
80
+
81
+ messageChannel.port1.postMessage(undefined)
82
+
83
+ // eslint-disable-next-line unicorn/prefer-add-event-listener
84
+ messageChannel.port2.onmessage = task
85
+ }, shouldYield)
86
+
87
+ main.pipe(
88
+ Effect.scoped,
89
+ Effect.provide(Logger.pretty),
90
+ Effect.tapErrorCause(Effect.logError),
91
+ Effect.withScheduler(messageChannel()),
92
+ NodePlatform.NodeRuntime.runMain,
93
+ )