@livestore/utils 0.4.0-dev.2 → 0.4.0-dev.21

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 (192) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.d.ts.map +1 -1
  3. package/dist/NoopTracer.js +5 -1
  4. package/dist/NoopTracer.js.map +1 -1
  5. package/dist/browser/Opfs/Opfs.d.ts +51 -0
  6. package/dist/browser/Opfs/Opfs.d.ts.map +1 -0
  7. package/dist/browser/Opfs/Opfs.js +345 -0
  8. package/dist/browser/Opfs/Opfs.js.map +1 -0
  9. package/dist/browser/Opfs/debug-utils.d.ts +20 -0
  10. package/dist/browser/Opfs/debug-utils.d.ts.map +1 -0
  11. package/dist/browser/Opfs/debug-utils.js +94 -0
  12. package/dist/browser/Opfs/debug-utils.js.map +1 -0
  13. package/dist/browser/Opfs/mod.d.ts +4 -0
  14. package/dist/browser/Opfs/mod.d.ts.map +1 -0
  15. package/dist/browser/Opfs/mod.js +4 -0
  16. package/dist/browser/Opfs/mod.js.map +1 -0
  17. package/dist/browser/Opfs/utils.d.ts +71 -0
  18. package/dist/browser/Opfs/utils.d.ts.map +1 -0
  19. package/dist/browser/Opfs/utils.js +218 -0
  20. package/dist/browser/Opfs/utils.js.map +1 -0
  21. package/dist/browser/QuotaExceededError.d.ts +59 -0
  22. package/dist/browser/QuotaExceededError.d.ts.map +1 -0
  23. package/dist/browser/QuotaExceededError.js +2 -0
  24. package/dist/browser/QuotaExceededError.js.map +1 -0
  25. package/dist/browser/WebChannelBrowser.d.ts +22 -0
  26. package/dist/browser/WebChannelBrowser.d.ts.map +1 -0
  27. package/dist/browser/WebChannelBrowser.js +76 -0
  28. package/dist/browser/WebChannelBrowser.js.map +1 -0
  29. package/dist/browser/WebError.d.ts +425 -0
  30. package/dist/browser/WebError.d.ts.map +1 -0
  31. package/dist/browser/WebError.js +414 -0
  32. package/dist/browser/WebError.js.map +1 -0
  33. package/dist/browser/WebError.test.d.ts +2 -0
  34. package/dist/browser/WebError.test.d.ts.map +1 -0
  35. package/dist/browser/WebError.test.js +46 -0
  36. package/dist/browser/WebError.test.js.map +1 -0
  37. package/dist/browser/WebLock.d.ts.map +1 -0
  38. package/dist/browser/WebLock.js.map +1 -0
  39. package/dist/{browser.d.ts → browser/detect.d.ts} +1 -1
  40. package/dist/browser/detect.d.ts.map +1 -0
  41. package/dist/{browser.js → browser/detect.js} +1 -1
  42. package/dist/browser/detect.js.map +1 -0
  43. package/dist/browser/mod.d.ts +8 -0
  44. package/dist/browser/mod.d.ts.map +1 -0
  45. package/dist/browser/mod.js +8 -0
  46. package/dist/browser/mod.js.map +1 -0
  47. package/dist/effect/Debug.d.ts +36 -0
  48. package/dist/effect/Debug.d.ts.map +1 -0
  49. package/dist/effect/Debug.js +346 -0
  50. package/dist/effect/Debug.js.map +1 -0
  51. package/dist/effect/Effect.d.ts +9 -3
  52. package/dist/effect/Effect.d.ts.map +1 -1
  53. package/dist/effect/Effect.js +4 -2
  54. package/dist/effect/Effect.js.map +1 -1
  55. package/dist/effect/Error.d.ts +1 -1
  56. package/dist/effect/Error.js.map +1 -1
  57. package/dist/effect/Logger.d.ts +4 -1
  58. package/dist/effect/Logger.d.ts.map +1 -1
  59. package/dist/effect/Logger.js +12 -3
  60. package/dist/effect/Logger.js.map +1 -1
  61. package/dist/effect/OtelTracer.d.ts +5 -0
  62. package/dist/effect/OtelTracer.d.ts.map +1 -0
  63. package/dist/effect/OtelTracer.js +8 -0
  64. package/dist/effect/OtelTracer.js.map +1 -0
  65. package/dist/effect/RpcClient.d.ts +32 -0
  66. package/dist/effect/RpcClient.d.ts.map +1 -0
  67. package/dist/effect/RpcClient.js +149 -0
  68. package/dist/effect/RpcClient.js.map +1 -0
  69. package/dist/effect/Schema/debug-diff.test.js +1 -1
  70. package/dist/effect/Schema/debug-diff.test.js.map +1 -1
  71. package/dist/effect/Schema/index.d.ts +2 -2
  72. package/dist/effect/Schema/index.d.ts.map +1 -1
  73. package/dist/effect/Schema/index.js +12 -2
  74. package/dist/effect/Schema/index.js.map +1 -1
  75. package/dist/effect/Stream.d.ts +73 -2
  76. package/dist/effect/Stream.d.ts.map +1 -1
  77. package/dist/effect/Stream.js +68 -1
  78. package/dist/effect/Stream.js.map +1 -1
  79. package/dist/effect/Stream.test.d.ts +2 -0
  80. package/dist/effect/Stream.test.d.ts.map +1 -0
  81. package/dist/effect/Stream.test.js +84 -0
  82. package/dist/effect/Stream.test.js.map +1 -0
  83. package/dist/effect/SubscriptionRef.d.ts +2 -2
  84. package/dist/effect/SubscriptionRef.d.ts.map +1 -1
  85. package/dist/effect/SubscriptionRef.js +6 -1
  86. package/dist/effect/SubscriptionRef.js.map +1 -1
  87. package/dist/effect/WebChannel/WebChannel.d.ts +2 -21
  88. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -1
  89. package/dist/effect/WebChannel/WebChannel.js +5 -81
  90. package/dist/effect/WebChannel/WebChannel.js.map +1 -1
  91. package/dist/effect/WebChannel/WebChannel.test.js +1 -1
  92. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -1
  93. package/dist/effect/WebChannel/common.d.ts +1 -1
  94. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  95. package/dist/effect/WebSocket.d.ts.map +1 -1
  96. package/dist/effect/WebSocket.js +12 -12
  97. package/dist/effect/WebSocket.js.map +1 -1
  98. package/dist/effect/mod.d.ts +32 -0
  99. package/dist/effect/mod.d.ts.map +1 -0
  100. package/dist/effect/mod.js +35 -0
  101. package/dist/effect/mod.js.map +1 -0
  102. package/dist/global.d.ts +4 -0
  103. package/dist/global.d.ts.map +1 -1
  104. package/dist/global.js.map +1 -1
  105. package/dist/guards.d.ts +14 -0
  106. package/dist/guards.d.ts.map +1 -1
  107. package/dist/guards.js +14 -0
  108. package/dist/guards.js.map +1 -1
  109. package/dist/misc.js +1 -1
  110. package/dist/misc.js.map +1 -1
  111. package/dist/mod.d.ts +197 -3
  112. package/dist/mod.d.ts.map +1 -1
  113. package/dist/mod.js +153 -4
  114. package/dist/mod.js.map +1 -1
  115. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
  116. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +66 -10
  117. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
  118. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +177 -3
  119. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +1 -1
  120. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +14 -5
  121. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +1 -1
  122. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +7 -1
  123. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +1 -1
  124. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +13 -3
  125. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +1 -1
  126. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +16 -0
  127. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
  128. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +98 -2
  129. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
  130. package/dist/node/mod.d.ts +8 -2
  131. package/dist/node/mod.d.ts.map +1 -1
  132. package/dist/node/mod.js +11 -3
  133. package/dist/node/mod.js.map +1 -1
  134. package/dist/qr.d.ts +38 -0
  135. package/dist/qr.d.ts.map +1 -0
  136. package/dist/qr.js +109 -0
  137. package/dist/qr.js.map +1 -0
  138. package/package.json +54 -44
  139. package/src/NoopTracer.ts +5 -1
  140. package/src/browser/Opfs/Opfs.ts +428 -0
  141. package/src/browser/Opfs/debug-utils.ts +151 -0
  142. package/src/browser/Opfs/mod.ts +3 -0
  143. package/src/browser/Opfs/utils.ts +286 -0
  144. package/src/browser/QuotaExceededError.ts +59 -0
  145. package/src/browser/WebChannelBrowser.ts +131 -0
  146. package/src/browser/WebError.test.ts +66 -0
  147. package/src/browser/WebError.ts +599 -0
  148. package/src/browser/mod.ts +8 -0
  149. package/src/effect/Debug.ts +452 -0
  150. package/src/effect/Effect.ts +31 -4
  151. package/src/effect/Error.ts +1 -1
  152. package/src/effect/Logger.ts +14 -4
  153. package/src/effect/OtelTracer.ts +11 -0
  154. package/src/effect/RpcClient.ts +212 -0
  155. package/src/effect/Schema/debug-diff.test.ts +2 -2
  156. package/src/effect/Schema/index.ts +17 -3
  157. package/src/effect/Stream.test.ts +127 -0
  158. package/src/effect/Stream.ts +111 -2
  159. package/src/effect/SubscriptionRef.ts +14 -2
  160. package/src/effect/WebChannel/WebChannel.test.ts +1 -1
  161. package/src/effect/WebChannel/WebChannel.ts +13 -135
  162. package/src/effect/WebChannel/common.ts +1 -1
  163. package/src/effect/WebSocket.ts +11 -10
  164. package/src/effect/{index.ts → mod.ts} +45 -15
  165. package/src/global.ts +5 -0
  166. package/src/guards.ts +15 -0
  167. package/src/misc.ts +1 -1
  168. package/src/mod.ts +206 -5
  169. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +71 -10
  170. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +258 -3
  171. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +14 -1
  172. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +16 -3
  173. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +111 -3
  174. package/src/node/mod.ts +13 -6
  175. package/src/qr.ts +125 -0
  176. package/dist/browser.d.ts.map +0 -1
  177. package/dist/browser.js.map +0 -1
  178. package/dist/effect/Schema/msgpack.d.ts +0 -3
  179. package/dist/effect/Schema/msgpack.d.ts.map +0 -1
  180. package/dist/effect/Schema/msgpack.js +0 -7
  181. package/dist/effect/Schema/msgpack.js.map +0 -1
  182. package/dist/effect/WebLock.d.ts.map +0 -1
  183. package/dist/effect/WebLock.js.map +0 -1
  184. package/dist/effect/index.d.ts +0 -27
  185. package/dist/effect/index.d.ts.map +0 -1
  186. package/dist/effect/index.js +0 -31
  187. package/dist/effect/index.js.map +0 -1
  188. package/src/effect/Schema/msgpack.ts +0 -8
  189. /package/dist/{effect → browser}/WebLock.d.ts +0 -0
  190. /package/dist/{effect → browser}/WebLock.js +0 -0
  191. /package/src/{effect → browser}/WebLock.ts +0 -0
  192. /package/src/{browser.ts → browser/detect.ts} +0 -0
@@ -0,0 +1,452 @@
1
+ import * as Cause from 'effect/Cause'
2
+ import * as Context from 'effect/Context'
3
+ import * as Effect from 'effect/Effect'
4
+ import type * as Exit from 'effect/Exit'
5
+ import type * as Fiber from 'effect/Fiber'
6
+ import * as Graph from 'effect/Graph'
7
+ import * as Option from 'effect/Option'
8
+ import * as RuntimeFlags from 'effect/RuntimeFlags'
9
+ import * as Scope from 'effect/Scope'
10
+ import type * as Tracer from 'effect/Tracer'
11
+
12
+ interface SpanEvent {
13
+ readonly name: string
14
+ readonly startTime: bigint
15
+ readonly attributes?: Record<string, unknown>
16
+ }
17
+
18
+ interface GraphNodeInfo {
19
+ readonly span: Tracer.AnySpan
20
+ readonly exitTag: 'Success' | 'Failure' | 'Interrupted' | undefined
21
+ readonly events: Array<SpanEvent>
22
+ }
23
+
24
+ export type MutableSpanGraph = Graph.MutableGraph<GraphNodeInfo, void>
25
+ export type MutableSpanGraphInfo = {
26
+ readonly graph: MutableSpanGraph
27
+ readonly nodeIdBySpanId: Map<string, number>
28
+ }
29
+
30
+ const graphByTraceId = new Map<string, MutableSpanGraphInfo>()
31
+
32
+ function ensureSpan(traceId: string, spanId: string): [MutableSpanGraph, number] {
33
+ let info = graphByTraceId.get(traceId)
34
+ if (info === undefined) {
35
+ info = {
36
+ graph: Graph.beginMutation(Graph.directed<GraphNodeInfo, void>()),
37
+ nodeIdBySpanId: new Map<string, number>(),
38
+ }
39
+ graphByTraceId.set(traceId, info)
40
+ }
41
+ let nodeId = info.nodeIdBySpanId.get(spanId)
42
+ if (nodeId === undefined) {
43
+ nodeId = Graph.addNode(info.graph, {
44
+ span: { _tag: 'ExternalSpan', spanId, traceId, sampled: false, context: Context.empty() },
45
+ events: [],
46
+ exitTag: undefined,
47
+ })
48
+ info.nodeIdBySpanId.set(spanId, nodeId)
49
+ }
50
+ return [info.graph, nodeId]
51
+ }
52
+
53
+ function sortSpan(
54
+ prev: Tracer.AnySpan,
55
+ next: Tracer.AnySpan,
56
+ ): [info: Tracer.AnySpan, isUpgrade: boolean, timingUpdated: boolean] {
57
+ if (prev._tag === 'ExternalSpan' && next._tag === 'Span') return [next, true, true]
58
+ if (prev._tag === 'Span' && next._tag === 'Span' && next.status._tag === 'Ended') return [next, false, true]
59
+ return [prev, false, false]
60
+ }
61
+
62
+ function addNode(span: Tracer.AnySpan) {
63
+ const [mutableGraph, nodeId] = ensureSpan(span.traceId, span.spanId)
64
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
65
+ const [latestInfo, upgraded] = sortSpan(previousInfo.span, span)
66
+ if (upgraded && latestInfo._tag === 'Span' && Option.isSome(latestInfo.parent)) {
67
+ const parentNodeId = addNode(latestInfo.parent.value)
68
+ Graph.addEdge(mutableGraph, parentNodeId, nodeId, undefined)
69
+ }
70
+ return { ...previousInfo, span: latestInfo }
71
+ })
72
+ return nodeId
73
+ }
74
+
75
+ function addEvent(traceId: string, spanId: string, event: SpanEvent) {
76
+ const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
77
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
78
+ return { ...previousInfo, events: [...previousInfo.events, event] }
79
+ })
80
+ return nodeId
81
+ }
82
+ function addNodeExit(traceId: string, spanId: string, exit: Exit.Exit<any, any>) {
83
+ const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
84
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
85
+ const isInterruptedOnly = exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause)
86
+ return {
87
+ ...previousInfo,
88
+ exitTag: isInterruptedOnly ? ('Interrupted' as const) : exit._tag,
89
+ }
90
+ })
91
+ return nodeId
92
+ }
93
+
94
+ function createPropertyInterceptor<T extends object, K extends keyof T>(
95
+ obj: T,
96
+ property: K,
97
+ interceptor: (value: T[K]) => void,
98
+ ): void {
99
+ const descriptor = Object.getOwnPropertyDescriptor(obj, property)
100
+
101
+ const previousSetter = descriptor?.set
102
+
103
+ let currentValue: T[K]
104
+ const previousGetter = descriptor?.get
105
+
106
+ if (!previousGetter) {
107
+ currentValue = obj[property]
108
+ }
109
+
110
+ Object.defineProperty(obj, property, {
111
+ get(): T[K] {
112
+ if (previousGetter) {
113
+ return previousGetter.call(obj)
114
+ }
115
+ return currentValue
116
+ },
117
+ set(value: T[K]) {
118
+ if (previousSetter) {
119
+ previousSetter.call(obj, value)
120
+ } else {
121
+ currentValue = value
122
+ }
123
+ interceptor(value)
124
+ },
125
+ enumerable: descriptor?.enumerable ?? true,
126
+ configurable: descriptor?.configurable ?? true,
127
+ })
128
+ }
129
+
130
+ type EffectDevtoolsHookEvent =
131
+ | {
132
+ _tag: 'FiberAllocated'
133
+ fiber: Fiber.RuntimeFiber<any, any>
134
+ }
135
+ | {
136
+ _tag: 'ScopeAllocated'
137
+ scope: Scope.Scope
138
+ }
139
+
140
+ type GlobalWithFiberCurrent = {
141
+ 'effect/FiberCurrent': Fiber.RuntimeFiber<any, any> | undefined
142
+ 'effect/DevtoolsHook'?: {
143
+ onEvent: (event: EffectDevtoolsHookEvent) => void
144
+ }
145
+ }
146
+
147
+ const patchedTracer = new WeakSet<Tracer.Tracer>()
148
+ function ensureTracerPatched(currentTracer: Tracer.Tracer) {
149
+ if (patchedTracer.has(currentTracer)) {
150
+ return
151
+ }
152
+ patchedTracer.add(currentTracer)
153
+
154
+ const oldSpanConstructor = currentTracer.span
155
+ currentTracer.span = function (...args) {
156
+ const span = oldSpanConstructor.apply(this, args)
157
+ addNode(span)
158
+
159
+ const oldSpanEnd = span.end
160
+ span.end = function (endTime, exit, ...args) {
161
+ oldSpanEnd.apply(this, [endTime, exit, ...args])
162
+ addNodeExit(this.traceId, this.spanId, exit)
163
+ }
164
+
165
+ const oldSpanEvent = span.event
166
+ span.event = function (name, startTime, attributes, ...args) {
167
+ oldSpanEvent.apply(this, [name, startTime, attributes, ...args])
168
+ addEvent(this.traceId, this.spanId, { name, startTime, attributes: attributes ?? {} })
169
+ }
170
+
171
+ return span
172
+ }
173
+
174
+ const oldContext = currentTracer.context
175
+ currentTracer.context = function (f, fiber, ...args) {
176
+ const context = oldContext.apply(this, [f, fiber, ...args])
177
+ ensureFiberPatched(fiber)
178
+ return context as any
179
+ }
180
+ }
181
+
182
+ interface ScopeImpl extends Scope.Scope {
183
+ readonly state:
184
+ | {
185
+ readonly _tag: 'Open'
186
+ readonly finalizers: Map<{}, Scope.Scope.Finalizer>
187
+ }
188
+ | {
189
+ readonly _tag: 'Closed'
190
+ readonly exit: Exit.Exit<unknown, unknown>
191
+ }
192
+ }
193
+
194
+ const knownScopes = new Map<
195
+ ScopeImpl,
196
+ { id: number; allocationFiber: Fiber.RuntimeFiber<any, any> | undefined; allocationSpan: Tracer.AnySpan | undefined }
197
+ >()
198
+ let lastScopeId = 0
199
+ function ensureScopePatched(scope: ScopeImpl, allocationFiber: Fiber.RuntimeFiber<any, any> | undefined) {
200
+ if (scope.state._tag === 'Closed') return
201
+ if (knownScopes.has(scope)) return
202
+ const id = lastScopeId++
203
+ if (patchScopeClose) {
204
+ const oldClose = (scope as any).close
205
+ ;(scope as any).close = function (...args: any[]) {
206
+ return oldClose.apply(this as any, args).pipe(
207
+ Effect.withSpan(`scope.${id}.closeRunFinalizers`),
208
+ Effect.ensuring(
209
+ Effect.sync(() => {
210
+ knownScopes.delete(scope)
211
+ }),
212
+ ),
213
+ )
214
+ }
215
+ } else {
216
+ cleanupScopes()
217
+ }
218
+ const allocationSpan = allocationFiber?.currentSpan
219
+ knownScopes.set(scope, { id, allocationFiber, allocationSpan })
220
+ }
221
+ const cleanupScopes = () => {
222
+ for (const [scope] of knownScopes) {
223
+ if (scope.state._tag === 'Closed') knownScopes.delete(scope)
224
+ }
225
+ }
226
+
227
+ const knownFibers = new Set<Fiber.RuntimeFiber<any, any>>()
228
+ function ensureFiberPatched(fiber: Fiber.RuntimeFiber<any, any>) {
229
+ // patch tracer
230
+ ensureTracerPatched(fiber.currentTracer)
231
+ // patch scope
232
+ const currentScope = Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined)
233
+ if (currentScope) ensureScopePatched(currentScope as any as ScopeImpl, undefined)
234
+ // patch fiber
235
+ if (knownFibers.has(fiber)) return
236
+ knownFibers.add(fiber)
237
+ fiber.addObserver((exit) => {
238
+ knownFibers.delete(fiber)
239
+ onFiberCompleted?.(fiber, exit)
240
+ })
241
+ }
242
+
243
+ let patchScopeClose = false
244
+ let onFiberResumed: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
245
+ let onFiberSuspended: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
246
+ let onFiberCompleted: undefined | ((fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void)
247
+ export function attachSlowDebugInstrumentation(options: {
248
+ /** If set to true, the scope prototype will be patched to attach a span to visualize pending scope closing */
249
+ readonly patchScopeClose?: boolean
250
+ /** An optional callback that will be called when any fiber resumes performing a run loop */
251
+ readonly onFiberResumed?: (fiber: Fiber.RuntimeFiber<any, any>) => void
252
+ /** An optional callback that will be called when any fiber stops performing a run loop */
253
+ readonly onFiberSuspended?: (fiber: Fiber.RuntimeFiber<any, any>) => void
254
+ /** An optional callback that will be called when any fiber completes with a exit */
255
+ readonly onFiberCompleted?: (fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void
256
+ }) {
257
+ const _globalThis = globalThis as any as GlobalWithFiberCurrent
258
+ if (_globalThis['effect/DevtoolsHook']) {
259
+ return console.error(
260
+ 'attachDebugInstrumentation has already been called! To show the tree, call attachDebugInstrumentation() in the root/main file of your program to ensure it is loaded as soon as possible.',
261
+ )
262
+ }
263
+ patchScopeClose = options.patchScopeClose ?? false
264
+ onFiberResumed = options.onFiberResumed
265
+ onFiberSuspended = options.onFiberSuspended
266
+ onFiberCompleted = options.onFiberCompleted
267
+ let lastFiber: undefined | Fiber.RuntimeFiber<any, any>
268
+ createPropertyInterceptor(globalThis as any as GlobalWithFiberCurrent, 'effect/FiberCurrent', (value) => {
269
+ if (value && knownFibers.has(value)) onFiberResumed?.(value)
270
+ if (value) ensureFiberPatched(value)
271
+ if (!value && lastFiber && knownFibers.has(lastFiber)) onFiberSuspended?.(lastFiber)
272
+ lastFiber = value
273
+ })
274
+ _globalThis['effect/DevtoolsHook'] = {
275
+ onEvent: (event) => {
276
+ console.log('onEvent', event)
277
+ switch (event._tag) {
278
+ case 'ScopeAllocated':
279
+ ensureScopePatched(event.scope as any as ScopeImpl, _globalThis['effect/FiberCurrent'])
280
+ break
281
+ case 'FiberAllocated':
282
+ ensureFiberPatched(event.fiber)
283
+ break
284
+ }
285
+ },
286
+ }
287
+ }
288
+
289
+ function formatDuration(startTime: bigint, endTime: bigint | undefined): string {
290
+ if (endTime === undefined) return '[running]'
291
+ const durationMs = Number(endTime - startTime) / 1000000 // Convert nanoseconds to milliseconds
292
+ if (durationMs < 1000) return `${durationMs.toFixed(0)}ms`
293
+ if (durationMs < 60000) return `${(durationMs / 1000).toFixed(2)}s`
294
+ return `${(durationMs / 60000).toFixed(2)}m`
295
+ }
296
+
297
+ function getSpanName(span: Tracer.AnySpan): string {
298
+ if (span._tag === 'ExternalSpan') return `[external] ${span.spanId}`
299
+ return span.name
300
+ }
301
+
302
+ function getSpanStatus(info: GraphNodeInfo): string {
303
+ if (info.span._tag === 'ExternalSpan') return '?'
304
+ if (info.exitTag === 'Success') return '✓'
305
+ if (info.exitTag === 'Failure') return '✗'
306
+ if (info.exitTag === 'Interrupted') return '!'
307
+ return '⋮'
308
+ }
309
+
310
+ function getSpanDuration(span: Tracer.AnySpan): string {
311
+ if (span._tag === 'ExternalSpan') return ''
312
+ const endTime = span.status._tag === 'Ended' ? span.status.endTime : undefined
313
+ return formatDuration(span.status.startTime, endTime)
314
+ }
315
+
316
+ function filterGraphKeepAncestors<N, E>(
317
+ graph: Graph.Graph<N, E>,
318
+ predicate: (nodeData: N, nodeId: number) => boolean,
319
+ ): Graph.Graph<N, E> {
320
+ // Find all root nodes (nodes with no incoming edges)
321
+ const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
322
+ const shouldInclude = new Set<number>()
323
+
324
+ // Use postorder DFS to evaluate children before parents
325
+ for (const nodeId of Graph.indices(Graph.dfsPostOrder(graph, { start: rootNodes, direction: 'outgoing' }))) {
326
+ const node = Graph.getNode(graph, nodeId)
327
+ if (Option.isNone(node)) continue
328
+
329
+ const matchesPredicate = predicate(node.value, nodeId)
330
+ if (matchesPredicate) {
331
+ shouldInclude.add(nodeId)
332
+ } else {
333
+ const children = Graph.neighborsDirected(graph, nodeId, 'outgoing')
334
+ const hasMatchingChildren = children.some((childId) => shouldInclude.has(childId))
335
+ if (hasMatchingChildren) shouldInclude.add(nodeId)
336
+ }
337
+ }
338
+
339
+ // Create a filtered copy of the graph
340
+ return Graph.mutate(graph, (mutable) => {
341
+ for (const [nodeId] of mutable.nodes) {
342
+ if (shouldInclude.has(nodeId)) continue
343
+ Graph.removeNode(mutable, nodeId)
344
+ }
345
+ })
346
+ }
347
+
348
+ function renderSpanNode(graph: Graph.Graph<GraphNodeInfo, void, 'directed'>, nodeId: number): string[] {
349
+ const node = Graph.getNode(graph, nodeId)
350
+ if (Option.isNone(node)) return []
351
+ const info = node.value
352
+ const status = getSpanStatus(info)
353
+ const name = getSpanName(info.span)
354
+ const duration = getSpanDuration(info.span)
355
+ const durationStr = duration ? ` ${duration}` : ''
356
+
357
+ const fiberIds = Array.from(knownFibers)
358
+ .filter(
359
+ (fiber) => fiber.currentSpan?.spanId === info.span.spanId && fiber.currentSpan?.traceId === info.span.traceId,
360
+ )
361
+ .map((fiber) => `#${fiber.id().id}`)
362
+ .join(', ')
363
+ const runningOnFibers = fiberIds.length > 0 ? ` [fibers ${fiberIds}]` : ''
364
+
365
+ return [` ${status} ${name}${durationStr}${runningOnFibers}`]
366
+ }
367
+
368
+ function renderTree<N, E, T extends Graph.Kind>(
369
+ graph: Graph.Graph<N, E, T>,
370
+ nodeIds: Array<number>,
371
+ renderNode: (graph: Graph.Graph<N, E, T>, nodeId: number) => string[],
372
+ ): string[] {
373
+ let lines: string[] = []
374
+ for (let childIndex = 0; childIndex < nodeIds.length; childIndex++) {
375
+ const isLastChild = childIndex === nodeIds.length - 1
376
+ const childLines = renderNode(graph, nodeIds[childIndex]!).concat(
377
+ renderTree(graph, Graph.neighborsDirected(graph, nodeIds[childIndex]!, 'outgoing'), renderNode),
378
+ )
379
+ lines = [
380
+ ...lines,
381
+ ...childLines.map((l, lineIndex) => {
382
+ if (lineIndex === 0) {
383
+ return (isLastChild ? ' └─' : ' ├─') + l
384
+ }
385
+ return (isLastChild ? ' ' : ' │') + l
386
+ }),
387
+ ]
388
+ }
389
+
390
+ return lines
391
+ }
392
+
393
+ export interface LogDebugOptions {
394
+ readonly regex?: RegExp
395
+ readonly title?: string
396
+ }
397
+
398
+ export const logDebug = (options: LogDebugOptions = {}) => {
399
+ const _globalThis = globalThis as any as GlobalWithFiberCurrent
400
+ if (!_globalThis['effect/DevtoolsHook']) {
401
+ return console.error(
402
+ 'attachDebugInstrumentation has not been called! To show the tree, call attachDebugInstrumentation() in the root/main file of your program to ensure it is loaded as soon as possible.',
403
+ )
404
+ }
405
+
406
+ let lines: Array<string> = [`----------------${options.title ?? ''}----------------`]
407
+
408
+ // fibers
409
+ lines = [...lines, 'Active Fibers:']
410
+ for (const fiber of knownFibers) {
411
+ const interruptible = RuntimeFlags.interruptible((fiber as any).currentRuntimeFlags)
412
+ lines = [...lines, `- #${fiber.id().id}${!interruptible ? ' [uninterruptible]' : ''}`]
413
+ }
414
+ if (knownFibers.size === 0) {
415
+ lines = [...lines, '- No active effect fibers']
416
+ }
417
+ lines = [...lines, '']
418
+
419
+ // spans
420
+ for (const [traceId, info] of graphByTraceId) {
421
+ const graph = Graph.endMutation(info.graph)
422
+ const filteredGraph = options.regex
423
+ ? filterGraphKeepAncestors(graph, (nodeData, _nodeId) => {
424
+ const name = getSpanName(nodeData.span)
425
+ return options.regex!.test(name)
426
+ })
427
+ : graph
428
+ const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
429
+
430
+ lines = [...lines, `Spans Trace ${traceId}:`, ...renderTree(filteredGraph, filteredRootNodes, renderSpanNode)]
431
+ }
432
+ lines = [...lines, '? external span - ✓ success - ✗ failure - ! interrupted', '']
433
+
434
+ // scopes
435
+ lines = [...lines, 'Open Scopes:']
436
+ for (const [scope, info] of knownScopes) {
437
+ const fiberIds = Array.from(knownFibers)
438
+ .filter((fiber) => Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined) === scope)
439
+ .map((fiber) => `#${fiber.id().id}`)
440
+ .join(', ')
441
+ const usedByFibers = fiberIds.length > 0 ? ` [used by: ${fiberIds}]` : ''
442
+ const allocationFiber = info.allocationFiber ? ` [allocated in fiber #${info.allocationFiber.id().id}]` : ''
443
+ const allocationSpan = info.allocationSpan ? ` [allocated in span: ${getSpanName(info.allocationSpan)}]` : ''
444
+ lines = [...lines, `- #${info.id}${usedByFibers}${allocationFiber}${allocationSpan}`]
445
+ }
446
+ if (knownScopes.size === 0) {
447
+ lines = [...lines, '- No active scopes']
448
+ }
449
+ lines = [...lines, '']
450
+
451
+ console.log(lines.join('\n'))
452
+ }
@@ -1,9 +1,22 @@
1
1
  import * as OtelTracer from '@effect/opentelemetry/Tracer'
2
- import type { Context, Duration, Stream } from 'effect'
3
- import { Cause, Deferred, Effect, Fiber, FiberRef, HashSet, Logger, pipe, Scope } from 'effect'
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
- import type { LazyArg } from 'effect/Function'
18
+ import { dual, type LazyArg } from 'effect/Function'
19
+ import type { Predicate, Refinement } from 'effect/Predicate'
7
20
 
8
21
  import { isPromise } from '../mod.ts'
9
22
  import { UnknownError } from './Error.ts'
@@ -94,6 +107,20 @@ export const tapCauseLogPretty = <R, E, A>(eff: Effect.Effect<A, E, R>): Effect.
94
107
  }),
95
108
  )
96
109
 
110
+ export const ignoreIf: {
111
+ <E, EB extends E>(
112
+ refinement: Refinement<NoInfer<E>, EB>,
113
+ ): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, Exclude<E, EB>, R>
114
+ <E>(predicate: Predicate<NoInfer<E>>): <A, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<void, E, R>
115
+ <A, E, R, EB extends E>(
116
+ self: Effect.Effect<A, E, R>,
117
+ refinement: Refinement<E, EB>,
118
+ ): Effect.Effect<void, Exclude<E, EB>, R>
119
+ <A, E, R>(self: Effect.Effect<A, E, R>, predicate: Predicate<E>): Effect.Effect<void, E, R>
120
+ } = dual(2, <A, E, R>(self: Effect.Effect<A, E, R>, predicate: Predicate<E>) =>
121
+ self.pipe(Effect.catchIf(predicate, () => Effect.void)),
122
+ )
123
+
97
124
  export const eventListener = <TEvent = unknown>(
98
125
  target: Stream.EventListener<TEvent>,
99
126
  type: string,
@@ -166,7 +193,7 @@ export const logDuration =
166
193
  const start = Date.now()
167
194
  const res = yield* eff
168
195
  const end = Date.now()
169
- yield* Effect.log(`${label}: ${end - start}ms`)
196
+ yield* Effect.log(`${label}: ${Duration.format(end - start)}`)
170
197
  return res
171
198
  })
172
199
 
@@ -1,6 +1,6 @@
1
1
  import { Schema } from 'effect'
2
2
 
3
- export class UnknownError extends Schema.TaggedError<'UnknownError'>()('UnknownError', {
3
+ export class UnknownError extends Schema.TaggedError<UnknownError>()('UnknownError', {
4
4
  cause: Schema.Any,
5
5
  payload: Schema.optional(Schema.Any),
6
6
  }) {}
@@ -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
- consoleFn(`[${defaultDateFormat(date)} ${threadName}]`, ...messages, annotationsObj)
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
+ })