@evolu/common 7.4.0 → 8.0.0-next.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 (251) hide show
  1. package/README.md +4 -61
  2. package/dist/src/Array.d.ts +343 -102
  3. package/dist/src/Array.d.ts.map +1 -1
  4. package/dist/src/Array.js +181 -85
  5. package/dist/src/Assert.d.ts +57 -11
  6. package/dist/src/Assert.d.ts.map +1 -1
  7. package/dist/src/Assert.js +47 -11
  8. package/dist/src/BigInt.d.ts +6 -1
  9. package/dist/src/BigInt.d.ts.map +1 -1
  10. package/dist/src/BigInt.js +5 -0
  11. package/dist/src/Brand.d.ts +40 -12
  12. package/dist/src/Brand.d.ts.map +1 -1
  13. package/dist/src/Brand.js +5 -0
  14. package/dist/src/Buffer.d.ts +6 -1
  15. package/dist/src/Buffer.d.ts.map +1 -1
  16. package/dist/src/Buffer.js +7 -2
  17. package/dist/src/Cache.d.ts +10 -2
  18. package/dist/src/Cache.d.ts.map +1 -1
  19. package/dist/src/Cache.js +6 -0
  20. package/dist/src/Callbacks.d.ts +10 -7
  21. package/dist/src/Callbacks.d.ts.map +1 -1
  22. package/dist/src/Callbacks.js +23 -13
  23. package/dist/src/Console.d.ts +361 -69
  24. package/dist/src/Console.d.ts.map +1 -1
  25. package/dist/src/Console.js +217 -117
  26. package/dist/src/Crypto.d.ts +70 -21
  27. package/dist/src/Crypto.d.ts.map +1 -1
  28. package/dist/src/Crypto.js +72 -27
  29. package/dist/src/Eq.d.ts +22 -2
  30. package/dist/src/Eq.d.ts.map +1 -1
  31. package/dist/src/Eq.js +21 -1
  32. package/dist/src/Error.d.ts +23 -12
  33. package/dist/src/Error.d.ts.map +1 -1
  34. package/dist/src/Error.js +27 -11
  35. package/dist/src/Function.d.ts +128 -56
  36. package/dist/src/Function.d.ts.map +1 -1
  37. package/dist/src/Function.js +118 -14
  38. package/dist/src/Identicon.d.ts +5 -0
  39. package/dist/src/Identicon.d.ts.map +1 -1
  40. package/dist/src/Identicon.js +6 -1
  41. package/dist/src/Lookup.d.ts +160 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +192 -0
  44. package/dist/src/Microtask.d.ts +21 -0
  45. package/dist/src/Microtask.d.ts.map +1 -0
  46. package/dist/src/Microtask.js +37 -0
  47. package/dist/src/Number.d.ts +18 -4
  48. package/dist/src/Number.d.ts.map +1 -1
  49. package/dist/src/Number.js +23 -1
  50. package/dist/src/Object.d.ts +110 -5
  51. package/dist/src/Object.d.ts.map +1 -1
  52. package/dist/src/Object.js +108 -5
  53. package/dist/src/Option.d.ts +58 -0
  54. package/dist/src/Option.d.ts.map +1 -0
  55. package/dist/src/Option.js +43 -0
  56. package/dist/src/Order.d.ts +5 -0
  57. package/dist/src/Order.d.ts.map +1 -1
  58. package/dist/src/Order.js +8 -3
  59. package/dist/src/Platform.d.ts +36 -2
  60. package/dist/src/Platform.d.ts.map +1 -1
  61. package/dist/src/Platform.js +10 -6
  62. package/dist/src/Polyfills.d.ts +27 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +299 -0
  65. package/dist/src/Random.d.ts +18 -7
  66. package/dist/src/Random.d.ts.map +1 -1
  67. package/dist/src/Random.js +9 -7
  68. package/dist/src/Redacted.d.ts +7 -4
  69. package/dist/src/Redacted.d.ts.map +1 -1
  70. package/dist/src/Redacted.js +5 -0
  71. package/dist/src/Ref.d.ts +31 -16
  72. package/dist/src/Ref.d.ts.map +1 -1
  73. package/dist/src/Ref.js +35 -7
  74. package/dist/src/RefCount.d.ts +62 -0
  75. package/dist/src/RefCount.d.ts.map +1 -0
  76. package/dist/src/RefCount.js +83 -0
  77. package/dist/src/Relation.d.ts +84 -29
  78. package/dist/src/Relation.d.ts.map +1 -1
  79. package/dist/src/Relation.js +83 -105
  80. package/dist/src/Resource.d.ts +263 -0
  81. package/dist/src/Resource.d.ts.map +1 -0
  82. package/dist/src/Resource.js +389 -0
  83. package/dist/src/Result.d.ts +390 -374
  84. package/dist/src/Result.d.ts.map +1 -1
  85. package/dist/src/Result.js +142 -70
  86. package/dist/src/Schedule.d.ts +953 -0
  87. package/dist/src/Schedule.d.ts.map +1 -0
  88. package/dist/src/Schedule.js +1199 -0
  89. package/dist/src/Set.d.ts +181 -0
  90. package/dist/src/Set.d.ts.map +1 -0
  91. package/dist/src/Set.js +137 -0
  92. package/dist/src/Skiplist.d.ts +6 -1
  93. package/dist/src/Skiplist.d.ts.map +1 -1
  94. package/dist/src/Skiplist.js +5 -0
  95. package/dist/src/Sqlite.d.ts +160 -60
  96. package/dist/src/Sqlite.d.ts.map +1 -1
  97. package/dist/src/Sqlite.js +290 -190
  98. package/dist/src/Store.d.ts +36 -28
  99. package/dist/src/Store.d.ts.map +1 -1
  100. package/dist/src/Store.js +55 -15
  101. package/dist/src/String.d.ts +5 -0
  102. package/dist/src/String.d.ts.map +1 -1
  103. package/dist/src/String.js +5 -0
  104. package/dist/src/Task.d.ts +2263 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1488 -348
  107. package/dist/src/Test.d.ts +122 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +66 -0
  110. package/dist/src/Time.d.ts +201 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +190 -86
  113. package/dist/src/Tracer.d.ts +48 -0
  114. package/dist/src/Tracer.d.ts.map +1 -0
  115. package/dist/src/Tracer.js +6 -0
  116. package/dist/src/Type.d.ts +582 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +362 -240
  119. package/dist/src/Types.d.ts +160 -6
  120. package/dist/src/Types.d.ts.map +1 -1
  121. package/dist/src/Types.js +22 -2
  122. package/dist/src/WebSocket.d.ts +113 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +210 -118
  125. package/dist/src/Worker.d.ts +245 -60
  126. package/dist/src/Worker.d.ts.map +1 -1
  127. package/dist/src/Worker.js +220 -64
  128. package/dist/src/index.d.ts +25 -3
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +19 -3
  131. package/dist/src/local-first/Db.d.ts +24 -213
  132. package/dist/src/local-first/Db.d.ts.map +1 -1
  133. package/dist/src/local-first/Db.js +622 -293
  134. package/dist/src/local-first/Error.d.ts +12 -0
  135. package/dist/src/local-first/Error.d.ts.map +1 -0
  136. package/dist/src/local-first/Error.js +6 -0
  137. package/dist/src/local-first/Evolu.d.ts +330 -285
  138. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  139. package/dist/src/local-first/Evolu.js +472 -402
  140. package/dist/src/local-first/LocalAuth.d.ts +10 -10
  141. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  142. package/dist/src/local-first/LocalAuth.js +6 -1
  143. package/dist/src/local-first/Owner.d.ts +70 -27
  144. package/dist/src/local-first/Owner.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.js +27 -14
  146. package/dist/src/local-first/Protocol.d.ts +58 -50
  147. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  148. package/dist/src/local-first/Protocol.js +253 -155
  149. package/dist/src/local-first/Query.d.ts +133 -43
  150. package/dist/src/local-first/Query.d.ts.map +1 -1
  151. package/dist/src/local-first/Query.js +139 -90
  152. package/dist/src/local-first/Relay.d.ts +18 -38
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -171
  155. package/dist/src/local-first/Schema.d.ts +139 -144
  156. package/dist/src/local-first/Schema.d.ts.map +1 -1
  157. package/dist/src/local-first/Schema.js +78 -163
  158. package/dist/src/local-first/Shared.d.ts +125 -0
  159. package/dist/src/local-first/Shared.d.ts.map +1 -0
  160. package/dist/src/local-first/Shared.js +569 -0
  161. package/dist/src/local-first/Storage.d.ts +50 -53
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +121 -173
  164. package/dist/src/local-first/Timestamp.d.ts +28 -35
  165. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  166. package/dist/src/local-first/Timestamp.js +24 -25
  167. package/dist/src/local-first/index.d.ts +2 -15
  168. package/dist/src/local-first/index.d.ts.map +1 -1
  169. package/dist/src/local-first/index.js +2 -15
  170. package/package.json +36 -26
  171. package/src/Array.ts +467 -106
  172. package/src/Assert.ts +87 -12
  173. package/src/BigInt.ts +7 -1
  174. package/src/Brand.ts +41 -12
  175. package/src/Buffer.ts +10 -4
  176. package/src/Cache.ts +11 -2
  177. package/src/Callbacks.ts +33 -23
  178. package/src/Console.ts +567 -178
  179. package/src/Crypto.ts +111 -62
  180. package/src/Eq.ts +30 -3
  181. package/src/Error.ts +50 -34
  182. package/src/Function.ts +140 -68
  183. package/src/Identicon.ts +7 -1
  184. package/src/Lookup.ts +415 -0
  185. package/src/Microtask.ts +58 -0
  186. package/src/Number.ts +40 -7
  187. package/src/Object.ts +155 -16
  188. package/src/Option.ts +74 -0
  189. package/src/Order.ts +11 -3
  190. package/src/Platform.ts +43 -7
  191. package/src/Polyfills.ts +465 -0
  192. package/src/Random.ts +25 -11
  193. package/src/Redacted.ts +8 -4
  194. package/src/Ref.ts +72 -21
  195. package/src/RefCount.ts +170 -0
  196. package/src/Relation.ts +188 -136
  197. package/src/Resource.ts +864 -0
  198. package/src/Result.ts +548 -381
  199. package/src/Schedule.ts +1471 -0
  200. package/src/Set.ts +247 -0
  201. package/src/Skiplist.ts +7 -1
  202. package/src/Sqlite.ts +409 -304
  203. package/src/Store.ts +94 -48
  204. package/src/String.ts +6 -0
  205. package/src/Task.ts +3685 -740
  206. package/src/Test.ts +162 -0
  207. package/src/Time.ts +348 -145
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +1064 -630
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +311 -178
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +871 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -823
  217. package/src/local-first/LocalAuth.ts +10 -10
  218. package/src/local-first/Owner.ts +94 -42
  219. package/src/local-first/Protocol.ts +440 -303
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +91 -252
  222. package/src/local-first/Schema.ts +284 -394
  223. package/src/local-first/Shared.ts +932 -0
  224. package/src/local-first/Storage.ts +209 -282
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/LICENSE +0 -21
  228. package/dist/src/Instances.d.ts +0 -34
  229. package/dist/src/Instances.d.ts.map +0 -1
  230. package/dist/src/Instances.js +0 -44
  231. package/dist/src/Resources.d.ts +0 -118
  232. package/dist/src/Resources.d.ts.map +0 -1
  233. package/dist/src/Resources.js +0 -197
  234. package/dist/src/local-first/Platform.d.ts +0 -25
  235. package/dist/src/local-first/Platform.d.ts.map +0 -1
  236. package/dist/src/local-first/Platform.js +0 -1
  237. package/dist/src/local-first/Public.d.ts +0 -22
  238. package/dist/src/local-first/Public.d.ts.map +0 -1
  239. package/dist/src/local-first/Public.js +0 -15
  240. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  241. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  242. package/dist/src/local-first/PublicKysely.js +0 -184
  243. package/dist/src/local-first/Sync.d.ts +0 -112
  244. package/dist/src/local-first/Sync.d.ts.map +0 -1
  245. package/dist/src/local-first/Sync.js +0 -529
  246. package/src/Instances.ts +0 -90
  247. package/src/Resources.ts +0 -367
  248. package/src/local-first/Platform.ts +0 -27
  249. package/src/local-first/Public.ts +0 -42
  250. package/src/local-first/PublicKysely.ts +0 -239
  251. package/src/local-first/Sync.ts +0 -960
package/src/Console.ts CHANGED
@@ -1,230 +1,619 @@
1
1
  /**
2
- * Console abstraction for Chrome 123+, Firefox 125+, Safari 18.1+, Node.js
3
- * 22.x+, and React Native 0.75+. Includes methods guaranteed to be available in
4
- * these environments and expected to remain compatible in future versions.
5
- * Output formatting may vary (e.g., interactive UI in browsers vs. text in
6
- * Node.js/React Native), but functionality is consistent across platforms.
2
+ * Platform-agnostic console with structured logging.
7
3
  *
8
- * **Convention**: Use a tag (e.g., `[db]`) as the first argument for log
9
- * filtering.
4
+ * @module
5
+ */
6
+
7
+ import { objectFrom } from "./Object.js";
8
+ import type { ReadonlyStore } from "./Store.js";
9
+ import { createStore } from "./Store.js";
10
+ import type { Task } from "./Task.js";
11
+ import {
12
+ createTime,
13
+ formatMillisAsClockTime,
14
+ formatMillisAsDuration,
15
+ type Millis,
16
+ type TimeDep,
17
+ } from "./Time.js";
18
+
19
+ /**
20
+ * Platform-agnostic console with structured logging.
21
+ *
22
+ * Captures structured log entries and routes them to configurable outputs.
23
+ * Provides methods guaranteed to be available across browsers, Node.js, and
24
+ * React Native.
25
+ *
26
+ * Key features:
27
+ *
28
+ * - Structured entries — logs are captured as {@link ConsoleEntry} objects with
29
+ * method, path, and args
30
+ * - Pluggable outputs — route logs to console, files, arrays, or custom
31
+ * destinations via {@link ConsoleOutput}
32
+ * - Child consoles — use {@link Console.child} to create derived consoles
33
+ * - Level filtering — see {@link ConsoleLevel} for severity ordering
34
+ * - Entry formatting — use {@link createConsoleFormatter} for timestamps and path
35
+ * prefixes
36
+ *
37
+ * Built-in outputs:
38
+ *
39
+ * - {@link createNativeConsoleOutput} — writes to `globalThis.console` (default)
40
+ * - {@link createConsoleArrayOutput} — captures entries to an array (testing)
41
+ * - {@link createConsoleStoreOutput} — stores latest entry in a
42
+ * {@link ReadonlyStore} for subscribing
43
+ * - {@link createMultiOutput} — fans out to multiple outputs
10
44
  *
11
45
  * ### Example
12
46
  *
13
47
  * ```ts
14
- * deps.console.log("[evolu]", "createEvoluInstance", { name });
15
- * ```
48
+ * // Basic usage - defaults to "log"
49
+ * const console = createConsole();
16
50
  *
17
- * **Tip**: In browser dev tools, you can filter logs by tag (e.g., `[db]`) to
18
- * quickly find relevant messages. In Node.js, use `grep` to filter output:
51
+ * // With formatting (timestamps and path prefixes)
52
+ * const console = createConsole({
53
+ * level: "info",
54
+ * formatter: createConsoleFormatter()({ timestampFormat: "relative" }),
55
+ * });
19
56
  *
20
- * ```bash
21
- * node app.js | grep "\[relay\]" # Show only relay logs
22
- * node app.js | grep -E "\[db\]|\[sql\]" # Show db and sql logs
23
- * node app.js | grep -v "\[debug\]" # Hide debug logs
24
- * ```
57
+ * // Children inherit level at creation, then are independent
58
+ * const console = run.deps.console.child("relay");
59
+ * console.setLevel("silent");
25
60
  *
26
- * Or add to package.json scripts:
61
+ * // Tip: Wrap logged values in objects for labeled output in DevTools
62
+ * console.info("Creating instance", { config }); // Good — expandable "config:" label
63
+ * console.info("Creating instance", config); // Avoid — anonymous object, no label
27
64
  *
28
- * ```json
29
- * {
30
- * "scripts": {
31
- * "dev:relay": "node app.js | grep \"\\[relay\\]\"",
32
- * "dev:db": "node app.js | grep -E \"\\[db\\]|\\[sql\\]\""
33
- * }
34
- * }
65
+ * // Batch update via children
66
+ * const setLevelRecursive = (c: Console, level: ConsoleLevel): void => {
67
+ * c.setLevel(level);
68
+ * for (const child of c.children) setLevelRecursive(child, level);
69
+ * };
35
70
  * ```
71
+ *
72
+ * Console intentionally does not use {@link Task}. Logging must be as fast as
73
+ * possible and always work, even during error handling or shutdown.
74
+ *
75
+ * For testing, use {@link testCreateConsole} which creates a {@link TestConsole}
76
+ * with array output and snapshot helpers.
77
+ *
78
+ * @see {@link createConsole}
36
79
  */
37
80
  export interface Console {
38
- /** Controls whether console methods produce output (default: true) */
39
- enabled?: boolean;
81
+ /** Name of this console. Empty for root. */
82
+ readonly name: string;
83
+
84
+ /** Child consoles created via {@link Console.child}. */
85
+ readonly children: ReadonlySet<Console>;
86
+
87
+ /**
88
+ * Returns the effective log level.
89
+ *
90
+ * If this console has its own level set via {@link Console.setLevel}, returns
91
+ * that. Otherwise returns the inherited level from creation time.
92
+ */
93
+ readonly getLevel: () => ConsoleLevel;
94
+
95
+ /**
96
+ * Sets the log level for this console.
97
+ *
98
+ * Pass a level to override the inherited level, or `null` to revert to the
99
+ * inherited level.
100
+ */
101
+ readonly setLevel: (level: ConsoleLevel | null) => void;
40
102
 
41
- /** Outputs a message to the console */
42
- log: (...args: Array<any>) => void;
103
+ /** Returns true if this console has its own level set (not inherited). */
104
+ readonly hasOwnLevel: () => boolean;
43
105
 
44
- /** Outputs an informational message (often same as log) */
45
- info: (...args: Array<any>) => void;
106
+ /**
107
+ * Creates a child console with the given name added to the path.
108
+ *
109
+ * Child inherits the parent's configured level (not any runtime override).
110
+ * Use {@link Console.children} to access all children for batch operations.
111
+ */
112
+ readonly child: (name: string) => Console;
46
113
 
47
- /** Outputs a warning message */
48
- warn: (...args: Array<any>) => void;
114
+ /** Outputs a stack trace. */
115
+ readonly trace: (...args: ReadonlyArray<unknown>) => void;
49
116
 
50
- /** Outputs an error message */
51
- error: (...args: Array<any>) => void;
117
+ /** Development diagnostics. */
118
+ readonly debug: (...args: ReadonlyArray<unknown>) => void;
52
119
 
53
- /** Outputs a debug message */
54
- debug: (...args: Array<any>) => void;
120
+ /** General-purpose messages. */
121
+ readonly log: (...args: ReadonlyArray<unknown>) => void;
55
122
 
56
- /** Starts a timer with an optional label */
57
- time: (label?: string) => void;
123
+ /** Operational milestones (startup, shutdown). */
124
+ readonly info: (...args: ReadonlyArray<unknown>) => void;
58
125
 
59
- /** Logs the elapsed time for a timer without ending it */
60
- timeLog: (label?: string, ...data: Array<any>) => void;
126
+ /** Recoverable issues that may need attention. */
127
+ readonly warn: (...args: ReadonlyArray<unknown>) => void;
61
128
 
62
- /** Ends a timer and logs the elapsed time */
63
- timeEnd: (label?: string) => void;
129
+ /** Failures requiring immediate attention. */
130
+ readonly error: (...args: ReadonlyArray<unknown>) => void;
64
131
 
65
- /** Displays an object's properties in a detailed format */
66
- dir: (object: any, options?: any) => void;
132
+ /** Displays an object with expandable properties. Level: debug. */
133
+ readonly dir: (item: unknown) => void;
67
134
 
68
- /** Displays tabular data as a table */
69
- table: (tabularData: any, properties?: Array<string>) => void;
135
+ /** Displays tabular data. Level: debug. */
136
+ readonly table: (data: unknown) => void;
70
137
 
71
- /** Logs the number of times this has been called with the given label */
72
- count: (label?: string) => void;
138
+ /** Starts a timer with the given label. Level: debug. */
139
+ readonly time: (label: string) => void;
73
140
 
74
- /** Resets the counter for the given label */
75
- countReset: (label?: string) => void;
141
+ /** Logs elapsed time for a timer. Level: debug. */
142
+ readonly timeLog: (label: string, ...args: ReadonlyArray<unknown>) => void;
76
143
 
77
- /** Writes a message if the value is falsy, otherwise does nothing */
78
- assert: (value: any, message?: string, ...optionalParams: Array<any>) => void;
144
+ /** Ends a timer and logs elapsed time. Level: debug. */
145
+ readonly timeEnd: (label: string) => void;
79
146
 
80
- /** Prints a stack trace with an optional message */
81
- trace: (message?: any, ...optionalParams: Array<any>) => void;
147
+ /** Increments and logs a counter. Level: debug. */
148
+ readonly count: (label?: string) => void;
149
+
150
+ /** Resets a counter. Level: debug. */
151
+ readonly countReset: (label?: string) => void;
152
+
153
+ /**
154
+ * Writes a pre-built {@link ConsoleEntry} directly to the output, bypassing
155
+ * level filtering. Used to replay entries from another context (e.g., a
156
+ * SharedWorker) where filtering was already applied.
157
+ */
158
+ readonly write: (entry: ConsoleEntry) => void;
82
159
  }
83
160
 
84
- /** Dependency interface for injecting a Console instance. */
85
161
  export interface ConsoleDep {
86
162
  readonly console: Console;
87
163
  }
88
164
 
165
+ /**
166
+ * Log level controlling which messages are output.
167
+ *
168
+ * Setting a level enables all logs at that level and above (ordered by
169
+ * severity):
170
+ *
171
+ * - `"trace"` — Stack traces and detailed execution flow
172
+ * - `"debug"` — Development diagnostics, timers, counters
173
+ * - `"log"` — General-purpose messages
174
+ * - `"info"` — Operational milestones (startup, shutdown)
175
+ * - `"warn"` — Recoverable issues that may need attention
176
+ * - `"error"` — Failures requiring immediate attention
177
+ * - `"silent"` — Disables all logging
178
+ */
179
+ export type ConsoleLevel =
180
+ | "trace"
181
+ | "debug"
182
+ | "log"
183
+ | "info"
184
+ | "warn"
185
+ | "error"
186
+ | "silent";
187
+
188
+ /**
189
+ * Structured log entry captured by {@link Console}.
190
+ *
191
+ * Contains all information needed for outputs to route the log: method for
192
+ * routing, path for context, and the original arguments.
193
+ */
194
+ export interface ConsoleEntry {
195
+ /** The console method that was called. */
196
+ readonly method: ConsoleMethod;
197
+
198
+ /** Hierarchical path from {@link Console.child} calls (e.g., ["relay", "db"]). */
199
+ readonly path: ReadonlyArray<string>;
200
+
201
+ /** Original arguments passed to the console method. */
202
+ readonly args: ReadonlyArray<unknown>;
203
+ }
204
+
205
+ /**
206
+ * Console method being called.
207
+ *
208
+ * Used in {@link ConsoleEntry} to identify which console method was invoked.
209
+ * Outputs can route or format differently based on the method.
210
+ */
211
+ export type ConsoleMethod =
212
+ | "trace"
213
+ | "debug"
214
+ | "log"
215
+ | "info"
216
+ | "warn"
217
+ | "error"
218
+ | "dir"
219
+ | "table"
220
+ | "time"
221
+ | "timeLog"
222
+ | "timeEnd"
223
+ | "count"
224
+ | "countReset";
225
+
226
+ /**
227
+ * Output destination for {@link Console}.
228
+ *
229
+ * Implement this interface to create custom log destinations (file, network,
230
+ * array for testing, etc.).
231
+ *
232
+ * Use {@link createNativeConsoleOutput} for native console output.
233
+ */
234
+ export interface ConsoleOutput {
235
+ /** Write a log entry to this output. */
236
+ readonly write: (entry: ConsoleEntry, formatter?: ConsoleFormatter) => void;
237
+ }
238
+
239
+ /**
240
+ * Transforms a {@link ConsoleEntry} before output.
241
+ *
242
+ * Used by {@link ConsoleConfig.formatter} and {@link ConsoleOutput.write}. Create
243
+ * one with {@link createConsoleFormatter}.
244
+ */
245
+ export type ConsoleFormatter = (entry: ConsoleEntry) => ReadonlyArray<unknown>;
246
+
247
+ /** Configuration for {@link createConsole}. */
89
248
  export interface ConsoleConfig {
249
+ /** Name of this console. Defaults to empty string. */
250
+ readonly name?: string;
251
+
252
+ /** Initial log level. Defaults to `"log"`. */
253
+ readonly level?: ConsoleLevel;
254
+
90
255
  /**
91
- * Enable or disable console logging (default: false). When true, logs are
92
- * output to the {@link Console}; when false, logging is disabled for all
93
- * methods except `error`, which always outputs to ensure critical issues are
94
- * not missed.
256
+ * Output destination for log entries. Defaults to
257
+ * {@link createNativeConsoleOutput}.
95
258
  */
96
- readonly enableLogging?: boolean;
259
+ readonly output?: ConsoleOutput;
260
+
261
+ /** Path prefix for this console. Defaults to `[]`. */
262
+ readonly path?: ReadonlyArray<string>;
263
+
264
+ /**
265
+ * Transforms entry args before writing (e.g., adds timestamps, path
266
+ * prefixes).
267
+ *
268
+ * Receives the entry and returns modified args. Use
269
+ * {@link createConsoleFormatter} for common formatting options.
270
+ */
271
+ readonly formatter?: ConsoleFormatter;
97
272
  }
98
273
 
99
- /** Creates a {@link Console} for logging with configurable output. */
100
- export const createConsole = (config: ConsoleConfig = {}): Console => {
101
- const instance: Console = {
102
- enabled: config.enableLogging ?? false,
274
+ /** Configuration for {@link createConsoleFormatter}. */
275
+ export interface ConsoleFormatterConfig {
276
+ /**
277
+ * Timestamp format to prepend to log messages.
278
+ *
279
+ * - `"relative"` — elapsed since start: `+0.000s`, `+1.500s`, `+1m30.000s`
280
+ * - `"absolute"` — local clock time: `14:32:15.234`
281
+ * - `"iso"` — ISO 8601 UTC: `2026-01-28T14:30:00.123Z`
282
+ * - `"none"` — no timestamp (default)
283
+ */
284
+ readonly timestampFormat?: ConsoleEntryTimestampFormat;
103
285
 
104
- log: (...args) => {
105
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
106
- if (instance.enabled) console.log(...args);
107
- },
108
- info: (...args) => {
109
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
110
- if (instance.enabled) console.info(...args);
111
- },
112
- warn: (...args) => {
113
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
114
- if (instance.enabled) console.warn(...args);
115
- },
116
- error: (...args) => {
117
- // Always log errors, even if disabled
118
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
119
- console.error(...args);
120
- },
121
- debug: (...args) => {
122
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
123
- if (instance.enabled) console.debug(...args);
124
- },
125
- time: (label) => {
126
- // eslint-disable-next-line no-console
127
- if (instance.enabled) console.time(label);
128
- },
129
- timeLog: (label, ...data) => {
130
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
131
- if (instance.enabled) console.timeLog(label, ...data);
132
- },
133
- timeEnd: (label) => {
134
- // eslint-disable-next-line no-console
135
- if (instance.enabled) console.timeEnd(label);
136
- },
137
- dir: (object, options) => {
138
- // eslint-disable-next-line no-console
139
- if (instance.enabled) console.dir(object, options);
140
- },
141
- table: (tabularData, properties) => {
142
- // eslint-disable-next-line no-console
143
- if (instance.enabled) console.table(tabularData, properties);
144
- },
145
- count: (label) => {
146
- // eslint-disable-next-line no-console
147
- if (instance.enabled) console.count(label);
148
- },
149
- countReset: (label) => {
150
- // eslint-disable-next-line no-console
151
- if (instance.enabled) console.countReset(label);
286
+ /**
287
+ * Start time for relative timestamps. Defaults to first entry timestamp.
288
+ *
289
+ * Pass a {@link Millis} value to use a custom start time, useful when multiple
290
+ * consoles should share the same relative timeline.
291
+ */
292
+ readonly startTime?: Millis;
293
+ }
294
+
295
+ /** Timestamp format for {@link ConsoleFormatterConfig}. */
296
+ export type ConsoleEntryTimestampFormat =
297
+ | "relative"
298
+ | "absolute"
299
+ | "iso"
300
+ | "none";
301
+
302
+ /**
303
+ * A {@link ConsoleOutput} that stores the latest entry in a
304
+ * {@link ReadonlyStore}.
305
+ *
306
+ * Subscribe to {@link ConsoleStoreOutput.entry} to observe all log entries.
307
+ *
308
+ * ### Example
309
+ *
310
+ * ```ts
311
+ * const storeOutput = createConsoleStoreOutput();
312
+ * const console = createConsole({ output: storeOutput });
313
+ *
314
+ * storeOutput.entry.subscribe(() => {
315
+ * const entry = storeOutput.entry.get();
316
+ * if (entry) forwardToClient(entry);
317
+ * });
318
+ * ```
319
+ */
320
+ export interface ConsoleStoreOutput extends ConsoleOutput {
321
+ /** Latest entry written to this output. */
322
+ readonly entry: ReadonlyStore<ConsoleEntry | null>;
323
+ }
324
+
325
+ /**
326
+ * Dependency providing the latest {@link ConsoleEntry} from a
327
+ * {@link ConsoleStoreOutput}.
328
+ */
329
+ export interface ConsoleStoreOutputEntryDep {
330
+ readonly consoleStoreOutputEntry: ReadonlyStore<ConsoleEntry | null>;
331
+ }
332
+
333
+ /**
334
+ * A test console that captures all output for assertions.
335
+ *
336
+ * Use as a drop-in replacement for {@link Console} in tests.
337
+ */
338
+ export interface TestConsole extends Console {
339
+ /** Gets all captured entries and clears the internal buffer. */
340
+ readonly getEntriesSnapshot: () => ReadonlyArray<ConsoleEntry>;
341
+
342
+ /** Clears all captured entries. */
343
+ readonly clearEntries: () => void;
344
+ }
345
+
346
+ export interface TestConsoleDep {
347
+ readonly console: TestConsole;
348
+ }
349
+
350
+ const levelOrder: Record<ConsoleLevel, number> = {
351
+ trace: 0,
352
+ debug: 1,
353
+ log: 2,
354
+ info: 3,
355
+ warn: 4,
356
+ error: 5,
357
+ silent: 6,
358
+ };
359
+
360
+ /** Creates a {@link Console}. */
361
+ export const createConsole = ({
362
+ name = "",
363
+ level = "log",
364
+ output = createNativeConsoleOutput(),
365
+ path = [],
366
+ formatter,
367
+ }: ConsoleConfig = {}): Console => {
368
+ const childrenSet = new Set<Console>();
369
+ let ownLevel: ConsoleLevel | null = null;
370
+
371
+ const getLevel = (): ConsoleLevel => ownLevel ?? level;
372
+
373
+ const createMethod =
374
+ (
375
+ method: ConsoleMethod,
376
+ methodLevel: ConsoleLevel,
377
+ formatter?: ConsoleFormatter,
378
+ ) =>
379
+ (...args: ReadonlyArray<unknown>): void => {
380
+ if (levelOrder[methodLevel] >= levelOrder[getLevel()])
381
+ output.write({ method, path, args }, formatter);
382
+ };
383
+
384
+ const levelMethod = (method: ConsoleLevel & ConsoleMethod) =>
385
+ createMethod(method, method, formatter);
386
+
387
+ const debugMethod = (method: ConsoleMethod) => createMethod(method, "debug");
388
+
389
+ return {
390
+ name,
391
+ children: childrenSet,
392
+ getLevel,
393
+ setLevel: (level) => {
394
+ ownLevel = level;
152
395
  },
153
- assert: (value, message, ...optionalParams) => {
154
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
155
- if (instance.enabled) console.assert(value, message, ...optionalParams);
396
+ hasOwnLevel: () => ownLevel !== null,
397
+
398
+ child: (name) => {
399
+ const childConsole = createConsole({
400
+ name,
401
+ level,
402
+ output,
403
+ path: [...path, name],
404
+ ...(formatter && { formatter }),
405
+ });
406
+ childrenSet.add(childConsole);
407
+ return childConsole;
156
408
  },
157
- trace: (message, ...optionalParams) => {
158
- // eslint-disable-next-line no-console, @typescript-eslint/no-unsafe-argument
159
- if (instance.enabled) console.trace(message, ...optionalParams);
409
+
410
+ ...objectFrom(
411
+ ["trace", "debug", "log", "info", "warn", "error"],
412
+ levelMethod,
413
+ ),
414
+ ...objectFrom(
415
+ ["dir", "table", "time", "timeLog", "timeEnd", "count", "countReset"],
416
+ debugMethod,
417
+ ),
418
+
419
+ write: (entry) => {
420
+ output.write(entry, formatter);
160
421
  },
161
422
  };
162
-
163
- return instance;
164
423
  };
165
424
 
166
- export interface ConsoleWithTimeConfig extends ConsoleConfig {
167
- /**
168
- * Type of timestamp to prepend to log messages.
169
- *
170
- * - 'absolute': Shows actual time (e.g., "14:32:15.234")
171
- * - 'relative': Shows time since console creation (e.g., "+1.234s")
172
- */
173
- readonly timestampType: "absolute" | "relative";
174
- }
425
+ /**
426
+ * Creates a {@link ConsoleOutput} that writes to `globalThis.console`.
427
+ *
428
+ * Pure transport - just calls the native console method with the entry args.
429
+ * Use {@link createConsoleFormatter} with {@link ConsoleConfig.formatter} for
430
+ * timestamps and path prefixes.
431
+ *
432
+ * ### Example
433
+ *
434
+ * ```ts
435
+ * const output = createNativeConsoleOutput();
436
+ * ```
437
+ */
438
+ export const createNativeConsoleOutput = (): ConsoleOutput => ({
439
+ write: (entry, formatter) => {
440
+ const args = formatter ? formatter(entry) : entry.args;
441
+ const fn = globalThis.console[entry.method] as (
442
+ ...args: Array<unknown>
443
+ ) => void;
444
+ fn(...args);
445
+ },
446
+ });
447
+
448
+ /**
449
+ * Creates a {@link ConsoleFormatter} for {@link ConsoleConfig.formatter}.
450
+ *
451
+ * Prepends timestamps and path prefixes to entry args.
452
+ *
453
+ * ### Example
454
+ *
455
+ * ```ts
456
+ * const root = createConsole({
457
+ * formatter: createConsoleFormatter()({
458
+ * timestampFormat: "relative",
459
+ * }),
460
+ * });
461
+ *
462
+ * // Relative — elapsed since start
463
+ * const relay = root.child("relay");
464
+ * relay.log("connected"); // +0.000s [relay] connected
465
+ * relay.log("synced"); // +1.500s [relay] synced
466
+ *
467
+ * // Nested children
468
+ * const db = relay.child("db");
469
+ * db.log("opened"); // +1.500s [relay] [db] opened
470
+ *
471
+ * // Absolute — local clock time (HH:MM:SS.mmm)
472
+ * // relay.log("connected"); // 15:30:15.123 [relay] connected
473
+ * ```
474
+ */
475
+ export const createConsoleFormatter =
476
+ ({ time = createTime() }: Partial<TimeDep> = {}) =>
477
+ (config: ConsoleFormatterConfig = {}): ConsoleFormatter => {
478
+ const format = config.timestampFormat ?? "none";
479
+ let startTime = config.startTime;
175
480
 
176
- /** Creates a console instance with timestamp prefixes. */
177
- export const createConsoleWithTime = (
178
- config: ConsoleWithTimeConfig = { timestampType: "relative" },
179
- ): Console => {
180
- const console = createConsole(config);
181
- const startTime = performance.now();
182
-
183
- const getTimestamp = (): string => {
184
- if (config.timestampType === "relative") {
185
- const elapsed = (performance.now() - startTime) / 1000;
186
-
187
- // Format for better readability at different time scales
188
- if (elapsed < 60) {
189
- // Under 1 minute: show seconds with millisecond precision
190
- return `+${elapsed.toFixed(3)}s`;
191
- } else if (elapsed < 3600) {
192
- // 1 minute to 1 hour: show minutes and seconds with millisecond precision
193
- const minutes = Math.floor(elapsed / 60);
194
- const seconds = (elapsed % 60).toFixed(3);
195
- return `+${minutes}m${seconds}s`;
196
- } else {
197
- // Over 1 hour: show hours, minutes, and seconds with millisecond precision
198
- const hours = Math.floor(elapsed / 3600);
199
- const minutes = Math.floor((elapsed % 3600) / 60);
200
- const seconds = ((elapsed % 3600) % 60).toFixed(3);
201
- return `+${hours}h${minutes}m${seconds}s`;
481
+ return (entry) => {
482
+ const now = time.now();
483
+ startTime ??= now;
484
+
485
+ let timestamp: string;
486
+ switch (format) {
487
+ case "none":
488
+ timestamp = "";
489
+ break;
490
+ case "relative":
491
+ timestamp = `+${formatMillisAsDuration((now - startTime) as Millis)}`;
492
+ break;
493
+ case "absolute":
494
+ timestamp = formatMillisAsClockTime(now);
495
+ break;
496
+ case "iso":
497
+ timestamp = new globalThis.Date(now).toISOString();
498
+ break;
202
499
  }
203
- } else {
204
- // Absolute time - format as HH:MM:SS.mmm
205
- const now = new Date();
206
- const hours = now.getHours().toString().padStart(2, "0");
207
- const minutes = now.getMinutes().toString().padStart(2, "0");
208
- const seconds = now.getSeconds().toString().padStart(2, "0");
209
- const milliseconds = now.getMilliseconds().toString().padStart(3, "0");
210
- return `${hours}:${minutes}:${seconds}.${milliseconds}`;
211
- }
212
- };
213
500
 
214
- const withTimestamp =
215
- (fn: (...args: Array<any>) => void) =>
216
- (...args: Array<any>) => {
217
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
218
- fn(`[${getTimestamp()}]`, ...args);
501
+ const path =
502
+ entry.path.length > 0 ? entry.path.map((p) => `[${p}]`).join(" ") : "";
503
+
504
+ const prefix =
505
+ timestamp && path ? `${timestamp} ${path}` : timestamp || path;
506
+ return prefix ? [prefix, ...entry.args] : entry.args;
219
507
  };
508
+ };
220
509
 
221
- // Override methods that should have timestamps
222
- console.log = withTimestamp(console.log);
223
- console.info = withTimestamp(console.info);
224
- console.warn = withTimestamp(console.warn);
225
- console.error = withTimestamp(console.error);
226
- console.debug = withTimestamp(console.debug);
227
- console.trace = withTimestamp(console.trace);
510
+ /** Creates a {@link ConsoleStoreOutput}. */
511
+ export const createConsoleStoreOutput = (): ConsoleStoreOutput => {
512
+ const entry = createStore<ConsoleEntry | null>(null);
513
+ return {
514
+ write: entry.set,
515
+ entry,
516
+ };
517
+ };
228
518
 
229
- return console;
519
+ /**
520
+ * Creates a {@link ConsoleOutput} that captures entries to an array.
521
+ *
522
+ * Useful for testing. Pass your own array to inspect captured entries.
523
+ *
524
+ * ### Example
525
+ *
526
+ * ```ts
527
+ * const entries: Array<ConsoleEntry> = [];
528
+ * const output = createConsoleArrayOutput(entries);
529
+ *
530
+ * // After logging...
531
+ * expect(entries).toMatchInlineSnapshot();
532
+ * ```
533
+ */
534
+ export const createConsoleArrayOutput = (
535
+ entries: Array<ConsoleEntry>,
536
+ ): ConsoleOutput => ({
537
+ write: (entry) => {
538
+ entries.push(entry);
539
+ },
540
+ });
541
+
542
+ /**
543
+ * Creates a {@link ConsoleOutput} that fans out to multiple outputs.
544
+ *
545
+ * Each entry is written to all outputs in order. Useful for combining a native
546
+ * console output with a store output for forwarding.
547
+ *
548
+ * ### Example
549
+ *
550
+ * ```ts
551
+ * const storeOutput = createConsoleStoreOutput();
552
+ * const console = createConsole({
553
+ * output: createMultiOutput([createNativeConsoleOutput(), storeOutput]),
554
+ * });
555
+ * ```
556
+ */
557
+ export const createMultiOutput = (
558
+ outputs: ReadonlyArray<ConsoleOutput>,
559
+ ): ConsoleOutput => ({
560
+ write: (entry, formatter) => {
561
+ for (const output of outputs) output.write(entry, formatter);
562
+ },
563
+ });
564
+
565
+ /**
566
+ * Creates a {@link TestConsole} that captures all output for testing.
567
+ *
568
+ * Unlike {@link createConsole}, this doesn't require dependencies and uses a
569
+ * simple incrementing counter for timestamps (starting at 0).
570
+ *
571
+ * ### Example
572
+ *
573
+ * ```ts
574
+ * test("logging", () => {
575
+ * const console = testCreateConsole();
576
+ * console.info("Hello");
577
+ *
578
+ * expect(console.getEntriesSnapshot()).toMatchInlineSnapshot(`
579
+ * [
580
+ * {
581
+ * "method": "info",
582
+ * "path": [],
583
+ * "args": ["Hello"]
584
+ * }
585
+ * ]
586
+ * `);
587
+ * });
588
+ *
589
+ * test("level filtering", () => {
590
+ * const console = testCreateConsole({ level: "warn" });
591
+ * console.debug("ignored");
592
+ * console.warn("logged");
593
+ * expect(console.getEntriesSnapshot()).toHaveLength(1);
594
+ * });
595
+ * ```
596
+ */
597
+ export const testCreateConsole = ({
598
+ level = "trace",
599
+ }: {
600
+ level?: ConsoleLevel;
601
+ } = {}): TestConsole => {
602
+ const entries: Array<ConsoleEntry> = [];
603
+ const console = createConsole({
604
+ level,
605
+ output: createConsoleArrayOutput(entries),
606
+ });
607
+
608
+ return {
609
+ ...console,
610
+ getEntriesSnapshot: () => {
611
+ const snapshot = [...entries];
612
+ entries.length = 0;
613
+ return snapshot;
614
+ },
615
+ clearEntries: () => {
616
+ entries.length = 0;
617
+ },
618
+ };
230
619
  };