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

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 (184) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.d.ts.map +1 -1
  3. package/dist/NoopTracer.js +1 -0
  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 +68 -0
  18. package/dist/browser/Opfs/utils.d.ts.map +1 -0
  19. package/dist/browser/Opfs/utils.js +206 -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 +38 -0
  48. package/dist/effect/Debug.d.ts.map +1 -0
  49. package/dist/effect/Debug.js +287 -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/index.d.ts +2 -2
  70. package/dist/effect/Schema/index.d.ts.map +1 -1
  71. package/dist/effect/Schema/index.js +12 -2
  72. package/dist/effect/Schema/index.js.map +1 -1
  73. package/dist/effect/Stream.d.ts +73 -2
  74. package/dist/effect/Stream.d.ts.map +1 -1
  75. package/dist/effect/Stream.js +68 -1
  76. package/dist/effect/Stream.js.map +1 -1
  77. package/dist/effect/Stream.test.d.ts +2 -0
  78. package/dist/effect/Stream.test.d.ts.map +1 -0
  79. package/dist/effect/Stream.test.js +84 -0
  80. package/dist/effect/Stream.test.js.map +1 -0
  81. package/dist/effect/SubscriptionRef.d.ts +2 -2
  82. package/dist/effect/SubscriptionRef.d.ts.map +1 -1
  83. package/dist/effect/SubscriptionRef.js +6 -1
  84. package/dist/effect/SubscriptionRef.js.map +1 -1
  85. package/dist/effect/WebChannel/WebChannel.d.ts +2 -21
  86. package/dist/effect/WebChannel/WebChannel.d.ts.map +1 -1
  87. package/dist/effect/WebChannel/WebChannel.js +5 -81
  88. package/dist/effect/WebChannel/WebChannel.js.map +1 -1
  89. package/dist/effect/WebChannel/WebChannel.test.js +1 -1
  90. package/dist/effect/WebChannel/WebChannel.test.js.map +1 -1
  91. package/dist/effect/WebChannel/common.d.ts +1 -1
  92. package/dist/effect/WebChannel/common.d.ts.map +1 -1
  93. package/dist/effect/WebSocket.d.ts.map +1 -1
  94. package/dist/effect/WebSocket.js +12 -12
  95. package/dist/effect/WebSocket.js.map +1 -1
  96. package/dist/effect/mod.d.ts +32 -0
  97. package/dist/effect/mod.d.ts.map +1 -0
  98. package/dist/effect/mod.js +35 -0
  99. package/dist/effect/mod.js.map +1 -0
  100. package/dist/global.d.ts +1 -0
  101. package/dist/global.d.ts.map +1 -1
  102. package/dist/global.js.map +1 -1
  103. package/dist/misc.js +1 -1
  104. package/dist/misc.js.map +1 -1
  105. package/dist/mod.d.ts +3 -1
  106. package/dist/mod.d.ts.map +1 -1
  107. package/dist/mod.js +5 -1
  108. package/dist/mod.js.map +1 -1
  109. package/dist/node/ChildProcessRunner/ChildProcessRunner.d.ts.map +1 -1
  110. package/dist/node/ChildProcessRunner/ChildProcessRunner.js +66 -10
  111. package/dist/node/ChildProcessRunner/ChildProcessRunner.js.map +1 -1
  112. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js +177 -3
  113. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.js.map +1 -1
  114. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts +14 -5
  115. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.d.ts.map +1 -1
  116. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js +7 -1
  117. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/schema.js.map +1 -1
  118. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js +13 -3
  119. package/dist/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.js.map +1 -1
  120. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts +16 -0
  121. package/dist/node/ChildProcessRunner/ChildProcessWorker.d.ts.map +1 -1
  122. package/dist/node/ChildProcessRunner/ChildProcessWorker.js +98 -2
  123. package/dist/node/ChildProcessRunner/ChildProcessWorker.js.map +1 -1
  124. package/dist/node/mod.d.ts +8 -2
  125. package/dist/node/mod.d.ts.map +1 -1
  126. package/dist/node/mod.js +11 -3
  127. package/dist/node/mod.js.map +1 -1
  128. package/dist/qr.d.ts +38 -0
  129. package/dist/qr.d.ts.map +1 -0
  130. package/dist/qr.js +109 -0
  131. package/dist/qr.js.map +1 -0
  132. package/package.json +54 -44
  133. package/src/NoopTracer.ts +1 -0
  134. package/src/browser/Opfs/Opfs.ts +428 -0
  135. package/src/browser/Opfs/debug-utils.ts +151 -0
  136. package/src/browser/Opfs/mod.ts +3 -0
  137. package/src/browser/Opfs/utils.ts +270 -0
  138. package/src/browser/QuotaExceededError.ts +59 -0
  139. package/src/browser/WebChannelBrowser.ts +131 -0
  140. package/src/browser/WebError.test.ts +66 -0
  141. package/src/browser/WebError.ts +599 -0
  142. package/src/browser/mod.ts +8 -0
  143. package/src/effect/Debug.ts +375 -0
  144. package/src/effect/Effect.ts +31 -4
  145. package/src/effect/Error.ts +1 -1
  146. package/src/effect/Logger.ts +14 -4
  147. package/src/effect/OtelTracer.ts +11 -0
  148. package/src/effect/RpcClient.ts +212 -0
  149. package/src/effect/Schema/index.ts +17 -3
  150. package/src/effect/Stream.test.ts +127 -0
  151. package/src/effect/Stream.ts +111 -2
  152. package/src/effect/SubscriptionRef.ts +14 -2
  153. package/src/effect/WebChannel/WebChannel.test.ts +1 -1
  154. package/src/effect/WebChannel/WebChannel.ts +13 -135
  155. package/src/effect/WebChannel/common.ts +1 -1
  156. package/src/effect/WebSocket.ts +11 -10
  157. package/src/effect/{index.ts → mod.ts} +42 -15
  158. package/src/global.ts +1 -0
  159. package/src/misc.ts +1 -1
  160. package/src/mod.ts +10 -1
  161. package/src/node/ChildProcessRunner/ChildProcessRunner.ts +71 -10
  162. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/ChildProcessRunner.test.ts +258 -3
  163. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/schema.ts +14 -1
  164. package/src/node/ChildProcessRunner/ChildProcessRunnerTest/serializedWorker.ts +16 -3
  165. package/src/node/ChildProcessRunner/ChildProcessWorker.ts +111 -3
  166. package/src/node/mod.ts +13 -6
  167. package/src/qr.ts +125 -0
  168. package/dist/browser.d.ts.map +0 -1
  169. package/dist/browser.js.map +0 -1
  170. package/dist/effect/Schema/msgpack.d.ts +0 -3
  171. package/dist/effect/Schema/msgpack.d.ts.map +0 -1
  172. package/dist/effect/Schema/msgpack.js +0 -7
  173. package/dist/effect/Schema/msgpack.js.map +0 -1
  174. package/dist/effect/WebLock.d.ts.map +0 -1
  175. package/dist/effect/WebLock.js.map +0 -1
  176. package/dist/effect/index.d.ts +0 -27
  177. package/dist/effect/index.d.ts.map +0 -1
  178. package/dist/effect/index.js +0 -31
  179. package/dist/effect/index.js.map +0 -1
  180. package/src/effect/Schema/msgpack.ts +0 -8
  181. /package/dist/{effect → browser}/WebLock.d.ts +0 -0
  182. /package/dist/{effect → browser}/WebLock.js +0 -0
  183. /package/src/{effect → browser}/WebLock.ts +0 -0
  184. /package/src/{browser.ts → browser/detect.ts} +0 -0
@@ -0,0 +1,375 @@
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'
5
+ import * as Effect from 'effect/Effect'
6
+ import { pipe } from 'effect/Function'
7
+ import * as Graph from 'effect/Graph'
8
+ import * as Layer from 'effect/Layer'
9
+ import * as Option from 'effect/Option'
10
+
11
+ interface SpanNodeInfo {
12
+ readonly span: Domain.ParentSpan
13
+ readonly events: readonly Domain.SpanEvent[]
14
+ }
15
+
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
39
+ }
40
+
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]
46
+ }
47
+
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
59
+ }
60
+
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
+ }))
67
+ }
68
+
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
+ )
81
+
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
+ )
89
+
90
+ function formatDuration(startTime: bigint, endTime: bigint | undefined): string {
91
+ if (endTime === undefined) return 'running'
92
+ const durationMs = Number(endTime - startTime) / 1000000 // Convert nanoseconds to milliseconds
93
+ if (durationMs < 1000) return `${durationMs.toFixed(0)}ms`
94
+ if (durationMs < 60000) return `${(durationMs / 1000).toFixed(2)}s`
95
+ return `${(durationMs / 60000).toFixed(2)}m`
96
+ }
97
+
98
+ function getSpanName(span: Domain.ParentSpan): string {
99
+ if (span._tag === 'ExternalSpan') return `[external] ${span.spanId}`
100
+ return span.name
101
+ }
102
+
103
+ function getSpanStatus(span: Domain.ParentSpan): string {
104
+ if (span._tag === 'ExternalSpan') return '?'
105
+ if (span.status._tag === 'Ended') return '✓'
106
+ return '●'
107
+ }
108
+
109
+ function getSpanDuration(span: Domain.ParentSpan): string {
110
+ if (span._tag === 'ExternalSpan') return ''
111
+ const endTime = span.status._tag === 'Ended' ? span.status.endTime : undefined
112
+ return formatDuration(span.status.startTime, endTime)
113
+ }
114
+
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>(
124
+ graph: Graph.Graph<N, E>,
125
+ predicate: (nodeData: N, nodeId: number) => boolean,
126
+ ): Graph.Graph<N, E> {
127
+ // Find all root nodes (nodes with no incoming edges)
128
+ const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
129
+
130
+ const shouldInclude = new Map<number, boolean>()
131
+
132
+ // 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
141
+ 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)
146
+ }
147
+ }
148
+
149
+ // Create a filtered copy of the graph
150
+ return Graph.mutate(graph, (mutable) => {
151
+ for (const [nodeId] of mutable.nodes) {
152
+ if (shouldInclude.get(nodeId) === true) continue
153
+ Graph.removeNode(mutable, nodeId)
154
+ }
155
+ })
156
+ }
157
+
158
+ function renderNode(graph: SpanNodeGraph, nodeId: number, prefix = '', isLast = true): string[] {
159
+ const node = Graph.getNode(graph, nodeId)
160
+ if (Option.isNone(node)) return []
161
+
162
+ const info = node.value
163
+ const status = getSpanStatus(info.span)
164
+ const name = getSpanName(info.span)
165
+ const duration = getSpanDuration(info.span)
166
+ const durationStr = duration ? ` ${duration}` : ''
167
+
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
+ }
237
+
238
+ return events
239
+ }
240
+
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
+ }
256
+ }
257
+
258
+ // Sort events by timestamp
259
+ events.sort((a, b) => a.ts - b.ts)
260
+
261
+ return events
262
+ }
263
+
264
+ export interface LogTreeOptions {
265
+ readonly regex?: RegExp
266
+ readonly title?: string
267
+ }
268
+
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)
279
+
280
+ // Find root nodes (nodes with no incoming edges) using externals
281
+ const rootNodes = Array.from(Graph.indices(Graph.externals(graph, { direction: 'incoming' })))
282
+
283
+ if (rootNodes.length === 0) {
284
+ return yield* Console.log('(empty trace)')
285
+ }
286
+
287
+ // Apply filter to create a filtered copy of the graph
288
+ const filteredGraph = options.regex
289
+ ? filterGraphWithAncestors(graph, (nodeData, _nodeId) => {
290
+ const name = getSpanName(nodeData.span)
291
+ return options.regex!.test(name)
292
+ })
293
+ : graph
294
+
295
+ // Find root nodes in the filtered graph
296
+ const filteredRootNodes = Array.from(Graph.indices(Graph.externals(filteredGraph, { direction: 'incoming' })))
297
+
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
+ })
319
+
320
+ export interface LogPerformanceTraceOptions {
321
+ readonly regex?: RegExp
322
+ }
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
+ })
@@ -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
+ })