@livestore/utils 0.4.0-dev.20 → 0.4.0-dev.22

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 (43) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.d.ts.map +1 -1
  3. package/dist/NoopTracer.js +6 -1
  4. package/dist/NoopTracer.js.map +1 -1
  5. package/dist/browser/Opfs/utils.d.ts +7 -4
  6. package/dist/browser/Opfs/utils.d.ts.map +1 -1
  7. package/dist/browser/Opfs/utils.js +30 -18
  8. package/dist/browser/Opfs/utils.js.map +1 -1
  9. package/dist/effect/Debug.d.ts +33 -30
  10. package/dist/effect/Debug.d.ts.map +1 -1
  11. package/dist/effect/Debug.js +280 -221
  12. package/dist/effect/Debug.js.map +1 -1
  13. package/dist/effect/Schema/debug-diff.test.js +1 -1
  14. package/dist/effect/Schema/debug-diff.test.js.map +1 -1
  15. package/dist/effect/mod.d.ts +1 -1
  16. package/dist/effect/mod.d.ts.map +1 -1
  17. package/dist/effect/mod.js +1 -1
  18. package/dist/effect/mod.js.map +1 -1
  19. package/dist/global.d.ts +3 -0
  20. package/dist/global.d.ts.map +1 -1
  21. package/dist/global.js.map +1 -1
  22. package/dist/guards.d.ts +14 -0
  23. package/dist/guards.d.ts.map +1 -1
  24. package/dist/guards.js +14 -0
  25. package/dist/guards.js.map +1 -1
  26. package/dist/mod.d.ts +195 -3
  27. package/dist/mod.d.ts.map +1 -1
  28. package/dist/mod.js +149 -4
  29. package/dist/mod.js.map +1 -1
  30. package/dist/node/mod.d.ts +31 -4
  31. package/dist/node/mod.d.ts.map +1 -1
  32. package/dist/node/mod.js +32 -5
  33. package/dist/node/mod.js.map +1 -1
  34. package/package.json +23 -23
  35. package/src/NoopTracer.ts +6 -1
  36. package/src/browser/Opfs/utils.ts +36 -20
  37. package/src/effect/Debug.ts +375 -292
  38. package/src/effect/Schema/debug-diff.test.ts +2 -2
  39. package/src/effect/mod.ts +3 -0
  40. package/src/global.ts +4 -0
  41. package/src/guards.ts +15 -0
  42. package/src/mod.ts +197 -5
  43. package/src/node/mod.ts +35 -5
@@ -1,375 +1,458 @@
1
- import * as C from '@effect/experimental/DevTools/Client'
2
- import type * as Domain from '@effect/experimental/DevTools/Domain'
3
- import { Duration, Schedule } from 'effect'
4
- import * as Console from 'effect/Console'
1
+ import * as Cause from 'effect/Cause'
2
+ import * as Context from 'effect/Context'
5
3
  import * as Effect from 'effect/Effect'
6
- import { pipe } from 'effect/Function'
4
+ import type * as Exit from 'effect/Exit'
5
+ import type * as Fiber from 'effect/Fiber'
7
6
  import * as Graph from 'effect/Graph'
8
- import * as Layer from 'effect/Layer'
9
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'
10
11
 
11
- interface SpanNodeInfo {
12
- readonly span: Domain.ParentSpan
13
- readonly events: readonly Domain.SpanEvent[]
12
+ /**
13
+ * How to use:
14
+ * 1. Call `Debug.attachSlowDebugInstrumentation` in the root/main file of your program to ensure it is loaded as soon as possible.
15
+ * 2. Call `Debug.logDebug` to log the current state of the effect system.
16
+ */
17
+
18
+ interface SpanEvent {
19
+ readonly name: string
20
+ readonly startTime: bigint
21
+ readonly attributes?: Record<string, unknown>
22
+ }
23
+
24
+ interface GraphNodeInfo {
25
+ readonly span: Tracer.AnySpan
26
+ readonly exitTag: 'Success' | 'Failure' | 'Interrupted' | undefined
27
+ readonly events: Array<SpanEvent>
14
28
  }
15
29
 
16
- export type SpanNodeGraph = Graph.Graph<SpanNodeInfo, any>
17
-
18
- class DebugInfo extends Effect.Service<DebugInfo>()('@mattiamanzati/debug/DebugInfo', {
19
- effect: Effect.sync(() => ({
20
- mutableGraph: Graph.beginMutation(Graph.directed<SpanNodeInfo, any>()),
21
- nodeIdBySpanId: new Map<string, number>(),
22
- })),
23
- }) {}
24
-
25
- const layerClientInMemoryGraph = Layer.effect(
26
- C.Client,
27
- Effect.gen(function* () {
28
- const { mutableGraph, nodeIdBySpanId } = yield* DebugInfo
29
-
30
- function ensureNode(traceId: string, spanId: string) {
31
- const existingNodeId = nodeIdBySpanId.get(spanId)
32
- if (existingNodeId !== undefined) return existingNodeId
33
- const nodeId = Graph.addNode(mutableGraph, {
34
- span: { _tag: 'ExternalSpan', spanId, traceId, sampled: false },
35
- events: [],
36
- })
37
- nodeIdBySpanId.set(spanId, nodeId)
38
- return nodeId
30
+ export type MutableSpanGraph = Graph.MutableGraph<GraphNodeInfo, void>
31
+ export type MutableSpanGraphInfo = {
32
+ readonly graph: MutableSpanGraph
33
+ readonly nodeIdBySpanId: Map<string, number>
34
+ }
35
+
36
+ const graphByTraceId = new Map<string, MutableSpanGraphInfo>()
37
+
38
+ function ensureSpan(traceId: string, spanId: string): [MutableSpanGraph, number] {
39
+ let info = graphByTraceId.get(traceId)
40
+ if (info === undefined) {
41
+ info = {
42
+ graph: Graph.beginMutation(Graph.directed<GraphNodeInfo, void>()),
43
+ nodeIdBySpanId: new Map<string, number>(),
39
44
  }
45
+ graphByTraceId.set(traceId, info)
46
+ }
47
+ let nodeId = info.nodeIdBySpanId.get(spanId)
48
+ if (nodeId === undefined) {
49
+ nodeId = Graph.addNode(info.graph, {
50
+ span: { _tag: 'ExternalSpan', spanId, traceId, sampled: false, context: Context.empty() },
51
+ events: [],
52
+ exitTag: undefined,
53
+ })
54
+ info.nodeIdBySpanId.set(spanId, nodeId)
55
+ }
56
+ return [info.graph, nodeId]
57
+ }
40
58
 
41
- function upgradeInfo(prev: Domain.ParentSpan, next: Domain.ParentSpan): [Domain.ParentSpan, boolean] {
42
- if (prev._tag === 'ExternalSpan' && next._tag === 'Span') return [next, true]
43
- if (prev._tag === 'Span' && next._tag === 'ExternalSpan') return [prev, false]
44
- if (prev._tag === 'Span' && prev.status._tag === 'Ended') return [prev, false]
45
- return [next, false]
59
+ function sortSpan(
60
+ prev: Tracer.AnySpan,
61
+ next: Tracer.AnySpan,
62
+ ): [info: Tracer.AnySpan, isUpgrade: boolean, timingUpdated: boolean] {
63
+ if (prev._tag === 'ExternalSpan' && next._tag === 'Span') return [next, true, true]
64
+ if (prev._tag === 'Span' && next._tag === 'Span' && next.status._tag === 'Ended') return [next, false, true]
65
+ return [prev, false, false]
66
+ }
67
+
68
+ function addNode(span: Tracer.AnySpan) {
69
+ const [mutableGraph, nodeId] = ensureSpan(span.traceId, span.spanId)
70
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
71
+ const [latestInfo, upgraded] = sortSpan(previousInfo.span, span)
72
+ if (upgraded && latestInfo._tag === 'Span' && Option.isSome(latestInfo.parent)) {
73
+ const parentNodeId = addNode(latestInfo.parent.value)
74
+ Graph.addEdge(mutableGraph, parentNodeId, nodeId, undefined)
46
75
  }
76
+ return { ...previousInfo, span: latestInfo }
77
+ })
78
+ return nodeId
79
+ }
47
80
 
48
- function addNode(span: Domain.ParentSpan) {
49
- const nodeId = ensureNode(span.traceId, span.spanId)
50
- Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
51
- const [latestInfo, upgraded] = upgradeInfo(previousInfo.span, span)
52
- if (upgraded && latestInfo._tag === 'Span' && Option.isSome(latestInfo.parent)) {
53
- const parentNodeId = addNode(latestInfo.parent.value)
54
- Graph.addEdge(mutableGraph, parentNodeId, nodeId, undefined)
55
- }
56
- return { ...previousInfo, span: latestInfo }
57
- })
58
- return nodeId
81
+ function addEvent(traceId: string, spanId: string, event: SpanEvent) {
82
+ const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
83
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
84
+ return { ...previousInfo, events: [...previousInfo.events, event] }
85
+ })
86
+ return nodeId
87
+ }
88
+ function addNodeExit(traceId: string, spanId: string, exit: Exit.Exit<any, any>) {
89
+ const [mutableGraph, nodeId] = ensureSpan(traceId, spanId)
90
+ Graph.updateNode(mutableGraph, nodeId, (previousInfo) => {
91
+ const isInterruptedOnly = exit._tag === 'Failure' && Cause.isInterruptedOnly(exit.cause)
92
+ return {
93
+ ...previousInfo,
94
+ exitTag: isInterruptedOnly ? ('Interrupted' as const) : exit._tag,
59
95
  }
96
+ })
97
+ return nodeId
98
+ }
99
+
100
+ function createPropertyInterceptor<T extends object, K extends keyof T>(
101
+ obj: T,
102
+ property: K,
103
+ interceptor: (value: T[K]) => void,
104
+ ): void {
105
+ const descriptor = Object.getOwnPropertyDescriptor(obj, property)
106
+
107
+ const previousSetter = descriptor?.set
108
+
109
+ let currentValue: T[K]
110
+ const previousGetter = descriptor?.get
111
+
112
+ if (!previousGetter) {
113
+ currentValue = obj[property]
114
+ }
115
+
116
+ Object.defineProperty(obj, property, {
117
+ get(): T[K] {
118
+ if (previousGetter) {
119
+ return previousGetter.call(obj)
120
+ }
121
+ return currentValue
122
+ },
123
+ set(value: T[K]) {
124
+ if (previousSetter) {
125
+ previousSetter.call(obj, value)
126
+ } else {
127
+ currentValue = value
128
+ }
129
+ interceptor(value)
130
+ },
131
+ enumerable: descriptor?.enumerable ?? true,
132
+ configurable: descriptor?.configurable ?? true,
133
+ })
134
+ }
60
135
 
61
- function addEvent(event: Domain.SpanEvent) {
62
- const nodeId = ensureNode(event.traceId, event.spanId)
63
- Graph.updateNode(mutableGraph, nodeId, (previousInfo) => ({
64
- ...previousInfo,
65
- events: [...previousInfo.events, event],
66
- }))
136
+ type EffectDevtoolsHookEvent =
137
+ | {
138
+ _tag: 'FiberAllocated'
139
+ fiber: Fiber.RuntimeFiber<any, any>
140
+ }
141
+ | {
142
+ _tag: 'ScopeAllocated'
143
+ scope: Scope.Scope
67
144
  }
68
145
 
69
- return C.Client.of({
70
- unsafeAddSpan: (span) => {
71
- switch (span._tag) {
72
- case 'SpanEvent':
73
- return addEvent(span)
74
- case 'Span':
75
- return addNode(span)
76
- }
77
- },
78
- })
79
- }),
80
- )
146
+ type GlobalWithFiberCurrent = {
147
+ 'effect/FiberCurrent': Fiber.RuntimeFiber<any, any> | undefined
148
+ 'effect/DevtoolsHook'?: {
149
+ onEvent: (event: EffectDevtoolsHookEvent) => void
150
+ }
151
+ }
81
152
 
82
- export const layerDebug = pipe(
83
- C.makeTracer,
84
- Effect.map(Layer.setTracer),
85
- Layer.unwrapEffect,
86
- Layer.provide(layerClientInMemoryGraph),
87
- Layer.provideMerge(DebugInfo.Default),
88
- )
153
+ const patchedTracer = new WeakSet<Tracer.Tracer>()
154
+ function ensureTracerPatched(currentTracer: Tracer.Tracer) {
155
+ if (patchedTracer.has(currentTracer)) {
156
+ return
157
+ }
158
+ patchedTracer.add(currentTracer)
159
+
160
+ const oldSpanConstructor = currentTracer.span
161
+ currentTracer.span = function (...args) {
162
+ const span = oldSpanConstructor.apply(this, args)
163
+ addNode(span)
164
+
165
+ const oldSpanEnd = span.end
166
+ span.end = function (endTime, exit, ...args) {
167
+ oldSpanEnd.apply(this, [endTime, exit, ...args])
168
+ addNodeExit(this.traceId, this.spanId, exit)
169
+ }
170
+
171
+ const oldSpanEvent = span.event
172
+ span.event = function (name, startTime, attributes, ...args) {
173
+ oldSpanEvent.apply(this, [name, startTime, attributes, ...args])
174
+ addEvent(this.traceId, this.spanId, { name, startTime, attributes: attributes ?? {} })
175
+ }
176
+
177
+ return span
178
+ }
179
+
180
+ const oldContext = currentTracer.context
181
+ currentTracer.context = function (f, fiber, ...args) {
182
+ const context = oldContext.apply(this, [f, fiber, ...args])
183
+ ensureFiberPatched(fiber)
184
+ return context as any
185
+ }
186
+ }
187
+
188
+ interface ScopeImpl extends Scope.Scope {
189
+ readonly state:
190
+ | {
191
+ readonly _tag: 'Open'
192
+ readonly finalizers: Map<{}, Scope.Scope.Finalizer>
193
+ }
194
+ | {
195
+ readonly _tag: 'Closed'
196
+ readonly exit: Exit.Exit<unknown, unknown>
197
+ }
198
+ }
199
+
200
+ const knownScopes = new Map<
201
+ ScopeImpl,
202
+ { id: number; allocationFiber: Fiber.RuntimeFiber<any, any> | undefined; allocationSpan: Tracer.AnySpan | undefined }
203
+ >()
204
+ let lastScopeId = 0
205
+ function ensureScopePatched(scope: ScopeImpl, allocationFiber: Fiber.RuntimeFiber<any, any> | undefined) {
206
+ if (scope.state._tag === 'Closed') return
207
+ if (knownScopes.has(scope)) return
208
+ const id = lastScopeId++
209
+ if (patchScopeClose) {
210
+ const oldClose = (scope as any).close
211
+ ;(scope as any).close = function (...args: any[]) {
212
+ return oldClose.apply(this as any, args).pipe(
213
+ Effect.withSpan(`scope.${id}.closeRunFinalizers`),
214
+ Effect.ensuring(
215
+ Effect.sync(() => {
216
+ knownScopes.delete(scope)
217
+ }),
218
+ ),
219
+ )
220
+ }
221
+ } else {
222
+ cleanupScopes()
223
+ }
224
+ const allocationSpan = allocationFiber?.currentSpan
225
+ knownScopes.set(scope, { id, allocationFiber, allocationSpan })
226
+ }
227
+ const cleanupScopes = () => {
228
+ for (const [scope] of knownScopes) {
229
+ if (scope.state._tag === 'Closed') knownScopes.delete(scope)
230
+ }
231
+ }
232
+
233
+ const knownFibers = new Set<Fiber.RuntimeFiber<any, any>>()
234
+ function ensureFiberPatched(fiber: Fiber.RuntimeFiber<any, any>) {
235
+ // patch tracer
236
+ ensureTracerPatched(fiber.currentTracer)
237
+ // patch scope
238
+ const currentScope = Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined)
239
+ if (currentScope) ensureScopePatched(currentScope as any as ScopeImpl, undefined)
240
+ // patch fiber
241
+ if (knownFibers.has(fiber)) return
242
+ knownFibers.add(fiber)
243
+ fiber.addObserver((exit) => {
244
+ knownFibers.delete(fiber)
245
+ onFiberCompleted?.(fiber, exit)
246
+ })
247
+ }
248
+
249
+ let patchScopeClose = false
250
+ let onFiberResumed: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
251
+ let onFiberSuspended: undefined | ((fiber: Fiber.RuntimeFiber<any, any>) => void)
252
+ let onFiberCompleted: undefined | ((fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void)
253
+ export function attachSlowDebugInstrumentation(options: {
254
+ /** If set to true, the scope prototype will be patched to attach a span to visualize pending scope closing */
255
+ readonly patchScopeClose?: boolean
256
+ /** An optional callback that will be called when any fiber resumes performing a run loop */
257
+ readonly onFiberResumed?: (fiber: Fiber.RuntimeFiber<any, any>) => void
258
+ /** An optional callback that will be called when any fiber stops performing a run loop */
259
+ readonly onFiberSuspended?: (fiber: Fiber.RuntimeFiber<any, any>) => void
260
+ /** An optional callback that will be called when any fiber completes with a exit */
261
+ readonly onFiberCompleted?: (fiber: Fiber.RuntimeFiber<any, any>, exit: Exit.Exit<any, any>) => void
262
+ }) {
263
+ const _globalThis = globalThis as any as GlobalWithFiberCurrent
264
+ if (_globalThis['effect/DevtoolsHook']) {
265
+ return console.error(
266
+ '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.',
267
+ )
268
+ }
269
+ patchScopeClose = options.patchScopeClose ?? false
270
+ onFiberResumed = options.onFiberResumed
271
+ onFiberSuspended = options.onFiberSuspended
272
+ onFiberCompleted = options.onFiberCompleted
273
+ let lastFiber: undefined | Fiber.RuntimeFiber<any, any>
274
+ createPropertyInterceptor(globalThis as any as GlobalWithFiberCurrent, 'effect/FiberCurrent', (value) => {
275
+ if (value && knownFibers.has(value)) onFiberResumed?.(value)
276
+ if (value) ensureFiberPatched(value)
277
+ if (!value && lastFiber && knownFibers.has(lastFiber)) onFiberSuspended?.(lastFiber)
278
+ lastFiber = value
279
+ })
280
+ _globalThis['effect/DevtoolsHook'] = {
281
+ onEvent: (event) => {
282
+ console.log('onEvent', event)
283
+ switch (event._tag) {
284
+ case 'ScopeAllocated':
285
+ ensureScopePatched(event.scope as any as ScopeImpl, _globalThis['effect/FiberCurrent'])
286
+ break
287
+ case 'FiberAllocated':
288
+ ensureFiberPatched(event.fiber)
289
+ break
290
+ }
291
+ },
292
+ }
293
+ }
89
294
 
90
295
  function formatDuration(startTime: bigint, endTime: bigint | undefined): string {
91
- if (endTime === undefined) return 'running'
296
+ if (endTime === undefined) return '[running]'
92
297
  const durationMs = Number(endTime - startTime) / 1000000 // Convert nanoseconds to milliseconds
93
298
  if (durationMs < 1000) return `${durationMs.toFixed(0)}ms`
94
299
  if (durationMs < 60000) return `${(durationMs / 1000).toFixed(2)}s`
95
300
  return `${(durationMs / 60000).toFixed(2)}m`
96
301
  }
97
302
 
98
- function getSpanName(span: Domain.ParentSpan): string {
303
+ function getSpanName(span: Tracer.AnySpan): string {
99
304
  if (span._tag === 'ExternalSpan') return `[external] ${span.spanId}`
100
305
  return span.name
101
306
  }
102
307
 
103
- function getSpanStatus(span: Domain.ParentSpan): string {
104
- if (span._tag === 'ExternalSpan') return '?'
105
- if (span.status._tag === 'Ended') return '✓'
106
- return ''
308
+ function getSpanStatus(info: GraphNodeInfo): string {
309
+ if (info.span._tag === 'ExternalSpan') return '?'
310
+ if (info.exitTag === 'Success') return '✓'
311
+ if (info.exitTag === 'Failure') return ''
312
+ if (info.exitTag === 'Interrupted') return '!'
313
+ return '⋮'
107
314
  }
108
315
 
109
- function getSpanDuration(span: Domain.ParentSpan): string {
316
+ function getSpanDuration(span: Tracer.AnySpan): string {
110
317
  if (span._tag === 'ExternalSpan') return ''
111
318
  const endTime = span.status._tag === 'Ended' ? span.status.endTime : undefined
112
319
  return formatDuration(span.status.startTime, endTime)
113
320
  }
114
321
 
115
- /**
116
- * Filters a graph by keeping only nodes that match the predicate or have descendants that match.
117
- * This ensures parent nodes are included even if they don't match, as long as they have matching descendants.
118
- *
119
- * @param graph - The graph to filter
120
- * @param predicate - A function that tests whether a node should be included
121
- * @returns A new filtered graph containing only matching nodes and their ancestors
122
- */
123
- function filterGraphWithAncestors<N, E>(
322
+ function filterGraphKeepAncestors<N, E>(
124
323
  graph: Graph.Graph<N, E>,
125
324
  predicate: (nodeData: N, nodeId: number) => boolean,
126
325
  ): Graph.Graph<N, E> {
127
326
  // Find all root nodes (nodes with no incoming edges)
128
327
  const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
129
-
130
- const shouldInclude = new Map<number, boolean>()
328
+ const shouldInclude = new Set<number>()
131
329
 
132
330
  // Use postorder DFS to evaluate children before parents
133
- for (const rootId of rootNodes) {
134
- for (const nodeId of Graph.indices(Graph.dfsPostOrder(graph, { startNodes: [rootId], direction: 'outgoing' }))) {
135
- const node = Graph.getNode(graph, nodeId)
136
- if (Option.isNone(node)) continue
137
-
138
- const matchesPredicate = predicate(node.value, nodeId)
139
-
140
- // Check if any children should be included
331
+ for (const nodeId of Graph.indices(Graph.dfsPostOrder(graph, { start: rootNodes, direction: 'outgoing' }))) {
332
+ const node = Graph.getNode(graph, nodeId)
333
+ if (Option.isNone(node)) continue
334
+
335
+ const matchesPredicate = predicate(node.value, nodeId)
336
+ if (matchesPredicate) {
337
+ shouldInclude.add(nodeId)
338
+ } else {
141
339
  const children = Graph.neighborsDirected(graph, nodeId, 'outgoing')
142
- const hasMatchingChildren = children.some((childId) => shouldInclude.get(childId) === true)
143
-
144
- const include = matchesPredicate || hasMatchingChildren
145
- shouldInclude.set(nodeId, include)
340
+ const hasMatchingChildren = children.some((childId) => shouldInclude.has(childId))
341
+ if (hasMatchingChildren) shouldInclude.add(nodeId)
146
342
  }
147
343
  }
148
344
 
149
345
  // Create a filtered copy of the graph
150
346
  return Graph.mutate(graph, (mutable) => {
151
347
  for (const [nodeId] of mutable.nodes) {
152
- if (shouldInclude.get(nodeId) === true) continue
348
+ if (shouldInclude.has(nodeId)) continue
153
349
  Graph.removeNode(mutable, nodeId)
154
350
  }
155
351
  })
156
352
  }
157
353
 
158
- function renderNode(graph: SpanNodeGraph, nodeId: number, prefix = '', isLast = true): string[] {
354
+ function renderSpanNode(graph: Graph.Graph<GraphNodeInfo, void, 'directed'>, nodeId: number): string[] {
159
355
  const node = Graph.getNode(graph, nodeId)
160
356
  if (Option.isNone(node)) return []
161
-
162
357
  const info = node.value
163
- const status = getSpanStatus(info.span)
358
+ const status = getSpanStatus(info)
164
359
  const name = getSpanName(info.span)
165
360
  const duration = getSpanDuration(info.span)
166
361
  const durationStr = duration ? ` ${duration}` : ''
167
362
 
168
- const connector = isLast ? '└─ ' : '├─ '
169
- const lines: string[] = [`${prefix}${connector}${status} ${name}${durationStr}`]
170
-
171
- // Get children
172
- const children = Graph.neighborsDirected(graph, nodeId, 'outgoing')
173
- const childCount = children.length
174
-
175
- children.forEach((childId: number, index: number) => {
176
- const isLastChild = index === childCount - 1
177
- const childPrefix = prefix + (isLast ? ' ' : '│ ')
178
- const childLines = renderNode(graph, childId, childPrefix, isLastChild)
179
- lines.push(...childLines)
180
- })
181
-
182
- return lines
183
- }
184
-
185
- interface ChromeTraceEvent {
186
- name: string
187
- cat: string
188
- ph: string // Phase: 'B' for begin, 'E' for end
189
- ts: number // Timestamp in microseconds
190
- pid: number // Process ID
191
- tid: number // Thread ID
192
- args?: Record<string, unknown>
193
- }
194
-
195
- /**
196
- * Converts a span node to Chrome Trace Events (Begin and End)
197
- */
198
- function spanToTraceEvents(span: Domain.ParentSpan, _nodeId: number, processId = 1): ChromeTraceEvent[] {
199
- const events: ChromeTraceEvent[] = []
200
-
201
- if (span._tag === 'ExternalSpan') {
202
- // External spans don't have timing info, skip them
203
- return events
204
- }
205
-
206
- const name = span.name
207
- const threadId = 1 // Could use spanId hash or other identifier
208
-
209
- // Begin event
210
- const startTimeUs = Number(span.status.startTime) / 1000 // Convert nanoseconds to microseconds
211
- events.push({
212
- name,
213
- cat: 'span',
214
- ph: 'B',
215
- ts: startTimeUs,
216
- pid: processId,
217
- tid: threadId,
218
- args: {
219
- spanId: span.spanId,
220
- traceId: span.traceId,
221
- attributes: Object.fromEntries(span.attributes),
222
- },
223
- })
224
-
225
- // End event (only if span has ended)
226
- if (span.status._tag === 'Ended') {
227
- const endTimeUs = Number(span.status.endTime) / 1000 // Convert nanoseconds to microseconds
228
- events.push({
229
- name,
230
- cat: 'span',
231
- ph: 'E',
232
- ts: endTimeUs,
233
- pid: processId,
234
- tid: threadId,
235
- })
236
- }
363
+ const fiberIds = Array.from(knownFibers)
364
+ .filter(
365
+ (fiber) => fiber.currentSpan?.spanId === info.span.spanId && fiber.currentSpan?.traceId === info.span.traceId,
366
+ )
367
+ .map((fiber) => `#${fiber.id().id}`)
368
+ .join(', ')
369
+ const runningOnFibers = fiberIds.length > 0 ? ` [fibers ${fiberIds}]` : ''
237
370
 
238
- return events
371
+ return [` ${status} ${name}${durationStr}${runningOnFibers}`]
239
372
  }
240
373
 
241
- /**
242
- * Traverses the graph and collects all trace events
243
- */
244
- function collectTraceEvents(graph: SpanNodeGraph, rootNodes: number[]): ChromeTraceEvent[] {
245
- const events: ChromeTraceEvent[] = []
246
-
247
- // Use DFS to traverse all nodes
248
- for (const rootId of rootNodes) {
249
- for (const nodeId of Graph.indices(Graph.dfs(graph, { startNodes: [rootId], direction: 'outgoing' }))) {
250
- const node = Graph.getNode(graph, nodeId)
251
- if (Option.isNone(node)) continue
252
-
253
- const nodeEvents = spanToTraceEvents(node.value.span, nodeId)
254
- events.push(...nodeEvents)
255
- }
374
+ function renderTree<N, E, T extends Graph.Kind>(
375
+ graph: Graph.Graph<N, E, T>,
376
+ nodeIds: Array<number>,
377
+ renderNode: (graph: Graph.Graph<N, E, T>, nodeId: number) => string[],
378
+ ): string[] {
379
+ let lines: string[] = []
380
+ for (let childIndex = 0; childIndex < nodeIds.length; childIndex++) {
381
+ const isLastChild = childIndex === nodeIds.length - 1
382
+ const childLines = renderNode(graph, nodeIds[childIndex]!).concat(
383
+ renderTree(graph, Graph.neighborsDirected(graph, nodeIds[childIndex]!, 'outgoing'), renderNode),
384
+ )
385
+ lines = [
386
+ ...lines,
387
+ ...childLines.map((l, lineIndex) => {
388
+ if (lineIndex === 0) {
389
+ return (isLastChild ? ' └─' : ' ├─') + l
390
+ }
391
+ return (isLastChild ? ' ' : ' │') + l
392
+ }),
393
+ ]
256
394
  }
257
395
 
258
- // Sort events by timestamp
259
- events.sort((a, b) => a.ts - b.ts)
260
-
261
- return events
396
+ return lines
262
397
  }
263
398
 
264
- export interface LogTreeOptions {
399
+ export interface LogDebugOptions {
265
400
  readonly regex?: RegExp
266
401
  readonly title?: string
267
402
  }
268
403
 
269
- export const logTree = (options: LogTreeOptions = {}) =>
270
- Effect.gen(function* () {
271
- const maybeInfo = yield* Effect.serviceOption(DebugInfo)
272
- if (Option.isNone(maybeInfo))
273
- return yield* Console.log(
274
- '(no debug info provided! To show the tree, provide the layerDebug layer in the root of your program)',
275
- )
276
-
277
- const { mutableGraph } = maybeInfo.value
278
- const graph = Graph.endMutation(mutableGraph)
404
+ export const logDebug = (options: LogDebugOptions = {}) => {
405
+ const _globalThis = globalThis as any as GlobalWithFiberCurrent
406
+ if (!_globalThis['effect/DevtoolsHook']) {
407
+ return console.error(
408
+ '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.',
409
+ )
410
+ }
279
411
 
280
- // Find root nodes (nodes with no incoming edges) using externals
281
- const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
412
+ let lines: Array<string> = [`----------------${options.title ?? ''}----------------`]
282
413
 
283
- if (rootNodes.length === 0) {
284
- return yield* Console.log('(empty trace)')
285
- }
414
+ // fibers
415
+ lines = [...lines, 'Active Fibers:']
416
+ for (const fiber of knownFibers) {
417
+ const interruptible = RuntimeFlags.interruptible((fiber as any).currentRuntimeFlags)
418
+ lines = [...lines, `- #${fiber.id().id}${!interruptible ? ' [uninterruptible]' : ''}`]
419
+ }
420
+ if (knownFibers.size === 0) {
421
+ lines = [...lines, '- No active effect fibers']
422
+ }
423
+ lines = [...lines, '']
286
424
 
287
- // Apply filter to create a filtered copy of the graph
425
+ // spans
426
+ for (const [traceId, info] of graphByTraceId) {
427
+ const graph = Graph.endMutation(info.graph)
288
428
  const filteredGraph = options.regex
289
- ? filterGraphWithAncestors(graph, (nodeData, _nodeId) => {
429
+ ? filterGraphKeepAncestors(graph, (nodeData, _nodeId) => {
290
430
  const name = getSpanName(nodeData.span)
291
431
  return options.regex!.test(name)
292
432
  })
293
433
  : graph
294
-
295
- // Find root nodes in the filtered graph
296
434
  const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
297
435
 
298
- if (filteredRootNodes.length === 0) {
299
- return yield* Console.log(options.title ? `${options.title}\n(no matches)` : '(no matches)')
300
- }
301
-
302
- const lines: string[] = []
303
-
304
- // Add title if provided
305
- if (options.title) {
306
- lines.push(options.title)
307
- }
308
-
309
- // Render root nodes using the same logic as regular children
310
- const rootCount = filteredRootNodes.length
311
- filteredRootNodes.forEach((rootId: number, index: number) => {
312
- const isLastRoot = index === rootCount - 1
313
- const rootLines = renderNode(filteredGraph, rootId, '', isLastRoot)
314
- lines.push(...rootLines)
315
- })
316
-
317
- return yield* Console.log(lines.join('\n'))
318
- })
436
+ lines = [...lines, `Spans Trace ${traceId}:`, ...renderTree(filteredGraph, filteredRootNodes, renderSpanNode)]
437
+ }
438
+ lines = [...lines, '? external span - ✓ success - ✗ failure - ! interrupted', '']
439
+
440
+ // scopes
441
+ lines = [...lines, 'Open Scopes:']
442
+ for (const [scope, info] of knownScopes) {
443
+ const fiberIds = Array.from(knownFibers)
444
+ .filter((fiber) => Context.getOrElse(fiber.currentContext, Scope.Scope, () => undefined) === scope)
445
+ .map((fiber) => `#${fiber.id().id}`)
446
+ .join(', ')
447
+ const usedByFibers = fiberIds.length > 0 ? ` [used by: ${fiberIds}]` : ''
448
+ const allocationFiber = info.allocationFiber ? ` [allocated in fiber #${info.allocationFiber.id().id}]` : ''
449
+ const allocationSpan = info.allocationSpan ? ` [allocated in span: ${getSpanName(info.allocationSpan)}]` : ''
450
+ lines = [...lines, `- #${info.id}${usedByFibers}${allocationFiber}${allocationSpan}`]
451
+ }
452
+ if (knownScopes.size === 0) {
453
+ lines = [...lines, '- No active scopes']
454
+ }
455
+ lines = [...lines, '']
319
456
 
320
- export interface LogPerformanceTraceOptions {
321
- readonly regex?: RegExp
457
+ console.log(lines.join('\n'))
322
458
  }
323
-
324
- /**
325
- * Logs the span tree in Chrome Performance Trace Event Format.
326
- * The output can be loaded in Chrome DevTools Performance tab or chrome://tracing
327
- *
328
- * @see https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview
329
- */
330
- export const logPerformanceTrace = (options: LogPerformanceTraceOptions = {}) =>
331
- Effect.gen(function* () {
332
- const maybeInfo = yield* Effect.serviceOption(DebugInfo)
333
- if (Option.isNone(maybeInfo)) {
334
- return yield* Console.log(
335
- '(no debug info provided! To show the trace, provide the layerDebug layer in the root of your program)',
336
- )
337
- }
338
-
339
- const { mutableGraph } = maybeInfo.value
340
- const graph = Graph.endMutation(mutableGraph)
341
-
342
- // Find root nodes (nodes with no incoming edges) using externals
343
- const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
344
-
345
- if (rootNodes.length === 0) {
346
- return yield* Console.log('[]')
347
- }
348
-
349
- // Apply filter if provided
350
- const filteredGraph = options.regex
351
- ? filterGraphWithAncestors(graph, (nodeData, _nodeId) => {
352
- const name = getSpanName(nodeData.span)
353
- return options.regex!.test(name)
354
- })
355
- : graph
356
-
357
- // Find root nodes in the filtered graph
358
- const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
359
-
360
- // Collect all trace events
361
- const events = collectTraceEvents(filteredGraph, filteredRootNodes)
362
-
363
- // Output as compact JSON array
364
- const json = JSON.stringify(events)
365
- return yield* Console.log(json)
366
- })
367
-
368
- export const logScopeState = ({ label }: { label: string }) =>
369
- Effect.gen(function* () {
370
- const scope = yield* Effect.scope
371
- yield* Effect.log(`scope.state[${label}]`, (scope as any).state._tag).pipe(
372
- Effect.repeat({ schedule: Schedule.fixed(Duration.millis(100)) }),
373
- Effect.forkScoped,
374
- )
375
- })