@evolu/common 7.4.1 → 8.0.0-next.1

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 (250) 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 +162 -0
  42. package/dist/src/Lookup.d.ts.map +1 -0
  43. package/dist/src/Lookup.js +200 -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 +29 -0
  63. package/dist/src/Polyfills.d.ts.map +1 -0
  64. package/dist/src/Polyfills.js +329 -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 +2274 -423
  105. package/dist/src/Task.d.ts.map +1 -1
  106. package/dist/src/Task.js +1499 -348
  107. package/dist/src/Test.d.ts +150 -0
  108. package/dist/src/Test.d.ts.map +1 -0
  109. package/dist/src/Test.js +97 -0
  110. package/dist/src/Time.d.ts +202 -48
  111. package/dist/src/Time.d.ts.map +1 -1
  112. package/dist/src/Time.js +199 -88
  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 +587 -209
  117. package/dist/src/Type.d.ts.map +1 -1
  118. package/dist/src/Type.js +370 -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 +130 -58
  123. package/dist/src/WebSocket.d.ts.map +1 -1
  124. package/dist/src/WebSocket.js +274 -113
  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 +621 -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 +34 -49
  153. package/dist/src/local-first/Relay.d.ts.map +1 -1
  154. package/dist/src/local-first/Relay.js +59 -177
  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 +54 -58
  162. package/dist/src/local-first/Storage.d.ts.map +1 -1
  163. package/dist/src/local-first/Storage.js +127 -179
  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 +37 -24
  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 +429 -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 +520 -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 +3696 -740
  206. package/src/Test.ts +202 -0
  207. package/src/Time.ts +357 -147
  208. package/src/Tracer.ts +54 -0
  209. package/src/Type.ts +866 -430
  210. package/src/Types.ts +202 -7
  211. package/src/WebSocket.ts +404 -167
  212. package/src/Worker.ts +527 -147
  213. package/src/index.ts +74 -3
  214. package/src/local-first/Db.ts +867 -597
  215. package/src/local-first/Error.ts +17 -0
  216. package/src/local-first/Evolu.ts +884 -824
  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 +441 -307
  220. package/src/local-first/Query.ts +243 -194
  221. package/src/local-first/Relay.ts +119 -273
  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 +211 -283
  225. package/src/local-first/Timestamp.ts +38 -45
  226. package/src/local-first/index.ts +2 -16
  227. package/dist/src/Instances.d.ts +0 -34
  228. package/dist/src/Instances.d.ts.map +0 -1
  229. package/dist/src/Instances.js +0 -44
  230. package/dist/src/Resources.d.ts +0 -118
  231. package/dist/src/Resources.d.ts.map +0 -1
  232. package/dist/src/Resources.js +0 -197
  233. package/dist/src/local-first/Platform.d.ts +0 -25
  234. package/dist/src/local-first/Platform.d.ts.map +0 -1
  235. package/dist/src/local-first/Platform.js +0 -1
  236. package/dist/src/local-first/Public.d.ts +0 -22
  237. package/dist/src/local-first/Public.d.ts.map +0 -1
  238. package/dist/src/local-first/Public.js +0 -15
  239. package/dist/src/local-first/PublicKysely.d.ts +0 -148
  240. package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
  241. package/dist/src/local-first/PublicKysely.js +0 -184
  242. package/dist/src/local-first/Sync.d.ts +0 -112
  243. package/dist/src/local-first/Sync.d.ts.map +0 -1
  244. package/dist/src/local-first/Sync.js +0 -526
  245. package/src/Instances.ts +0 -92
  246. package/src/Resources.ts +0 -367
  247. package/src/local-first/Platform.ts +0 -27
  248. package/src/local-first/Public.ts +0 -42
  249. package/src/local-first/PublicKysely.ts +0 -239
  250. package/src/local-first/Sync.ts +0 -949
package/src/Time.ts CHANGED
@@ -1,10 +1,36 @@
1
+ /**
2
+ * Time representations, durations, and scheduling utilities.
3
+ *
4
+ * @module
5
+ */
6
+
1
7
  import { assert } from "./Assert.js";
2
- import { DateIso, NonNegativeInt } from "./Type.js";
8
+ import type { Brand } from "./Brand.js";
9
+ import type { yieldNow } from "./Task.js";
10
+ import { brand, type DateIso, lessThan, NonNegativeInt } from "./Type.js";
11
+ import type {
12
+ Digit,
13
+ Digit1To23,
14
+ Digit1To51,
15
+ Digit1To59,
16
+ Digit1To6,
17
+ Digit1To9,
18
+ Digit1To99,
19
+ } from "./Types.js";
3
20
 
4
- /** Retrieves the current time in milliseconds, similar to `Date.now()`. */
21
+ /** Time and timer operations. */
5
22
  export interface Time {
6
- readonly now: () => number;
7
- readonly nowIso: () => DateIso;
23
+ /** Returns current time as Unix epoch milliseconds. */
24
+ readonly now: () => Millis;
25
+
26
+ /** Returns current time as an ISO 8601 UTC string. */
27
+ readonly nowDateIso: () => DateIso;
28
+
29
+ /** Schedules a callback after the specified delay. */
30
+ readonly setTimeout: (fn: () => void, delay: Duration) => TimeoutId;
31
+
32
+ /** Cancels a timeout scheduled with {@link Time.setTimeout}. */
33
+ readonly clearTimeout: (id: TimeoutId) => void;
8
34
  }
9
35
 
10
36
  export interface TimeDep {
@@ -12,194 +38,378 @@ export interface TimeDep {
12
38
  }
13
39
 
14
40
  /**
15
- * Creates a {@link Time} using Date.now().
41
+ * Opaque type for timeout handles.
16
42
  *
17
- * If the system clock is misconfigured (out of allowed range), the application
18
- * will fail with an assertion error. This is intentional - there's no
19
- * reasonable fallback when the system clock is fundamentally wrong.
43
+ * Use with {@link Time.clearTimeout} to cancel a pending timeout.
44
+ */
45
+ export type TimeoutId = Brand<"TimeoutId">;
46
+
47
+ /**
48
+ * Creates a {@link Time} using `Date.now()` and `globalThis.setTimeout`.
49
+ *
50
+ * Throws if the system clock returns an out-of-range value. This is intentional
51
+ * — there's no reasonable fallback for a misconfigured clock.
20
52
  */
21
53
  export const createTime = (): Time => {
22
- const time: Time = {
23
- now: () => {
24
- const iso = time.nowIso();
25
- return new globalThis.Date(iso).getTime();
26
- },
27
- nowIso: () => {
28
- const iso = new globalThis.Date().toISOString();
29
- assert(DateIso.is(iso), "System clock returned invalid ISO date");
30
- return iso;
54
+ const getNowMillis = (): Millis => Millis.orThrow(globalThis.Date.now());
55
+
56
+ return {
57
+ now: () => getNowMillis(),
58
+
59
+ nowDateIso: () => millisToDateIso(getNowMillis()),
60
+
61
+ setTimeout: (callback, delay) =>
62
+ globalThis.setTimeout(
63
+ callback,
64
+ durationToMillis(delay),
65
+ ) as unknown as TimeoutId,
66
+
67
+ clearTimeout: (id) => {
68
+ globalThis.clearTimeout(id as unknown as number);
31
69
  },
32
70
  };
33
- return time;
34
71
  };
35
72
 
36
73
  /**
37
- * Creates a {@link Time} that returns a monotonically increasing number based on
38
- * a queueMicrotask.
74
+ * Test {@link Time} with controllable timers.
75
+ *
76
+ * Call `advance(ms)` to move time forward and trigger any pending timeouts.
77
+ */
78
+ export interface TestTime extends Time {
79
+ /** Advances time by the specified duration, triggering pending timeouts. */
80
+ readonly advance: (duration: Duration) => void;
81
+ }
82
+
83
+ export interface TestTimeDep {
84
+ readonly time: TestTime;
85
+ }
86
+
87
+ /**
88
+ * Creates a {@link TestTime} with controllable timers for testing.
89
+ *
90
+ * Time starts at `startAt` (default 0) and only advances when `advance()` is
91
+ * called. Timeouts scheduled via `setTimeout` fire when time is advanced past
92
+ * their deadline.
93
+ *
94
+ * Set `autoIncrement` to automatically increment time by 1ms after each `now()`
95
+ * call. `"microtask"` increments after the current turn, while `"sync"`
96
+ * increments immediately after each read. Omit it to keep time fixed until
97
+ * `advance()` is called.
39
98
  */
40
- export const createTestTime = (): Time => {
41
- let now = 0;
42
- const time: Time = {
43
- now: () => {
44
- const current = now;
45
- queueMicrotask(() => {
46
- now++;
47
- });
48
- return current;
99
+ export const testCreateTime = (options?: {
100
+ readonly startAt?: Millis;
101
+ readonly autoIncrement?: "microtask" | "sync";
102
+ }): TestTime => {
103
+ let now = options?.startAt ?? minMillis;
104
+ const autoIncrement = options?.autoIncrement;
105
+ let nextId = 1;
106
+
107
+ const pending = new Map<number, { callback: () => void; runAt: number }>();
108
+ const incrementNow = (): void => {
109
+ now = Millis.orThrow(now + 1);
110
+ };
111
+
112
+ const getNowMillis = (): Millis => {
113
+ const result = now;
114
+ switch (autoIncrement) {
115
+ case "sync":
116
+ incrementNow();
117
+ break;
118
+ case "microtask":
119
+ queueMicrotask(incrementNow);
120
+ break;
121
+ }
122
+ return result;
123
+ };
124
+
125
+ return {
126
+ now: () => getNowMillis(),
127
+
128
+ nowDateIso: () => millisToDateIso(getNowMillis()),
129
+
130
+ setTimeout: (callback, delay) => {
131
+ const id = nextId++;
132
+ pending.set(id, { callback, runAt: now + durationToMillis(delay) });
133
+ return id as unknown as TimeoutId;
134
+ },
135
+
136
+ clearTimeout: (id) => {
137
+ pending.delete(id as unknown as number);
138
+ },
139
+
140
+ advance: (duration) => {
141
+ now = Millis.orThrow(now + durationToMillis(duration));
142
+
143
+ for (const [id, timeout] of pending) {
144
+ if (timeout.runAt <= now) {
145
+ pending.delete(id);
146
+ timeout.callback();
147
+ }
148
+ }
49
149
  },
50
- nowIso: () =>
51
- DateIso.orThrow(new globalThis.Date(time.now()).toISOString()),
52
150
  };
53
- return time;
54
151
  };
55
152
 
56
- /** Single digit 0-9. Used internally for {@link DurationString} validation. */
57
- export type D = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
153
+ // Literal (not expression like 1 + 2) to preserve type for Brand<"LessThan...">
154
+ const maxMillisWithInfinity = 281474976710655;
58
155
 
59
156
  /**
60
- * Minutes and seconds 1-59. Used internally for {@link DurationString}
61
- * validation. Uses single digits for 1-9, full numbers for 10-59.
157
+ * Milliseconds timestamp, like `Date.now()`.
158
+ *
159
+ * The maximum value is 281474976710654 (281474976710655 - 1, reserved for
160
+ * infinity). This enables efficient binary serialization, saving 2 bytes
161
+ * compared to typical 8-byte (64-bit) timestamps.
162
+ *
163
+ * `new Date(281474976710654).toString()` = Tue Aug 02 10889 07:31:49
164
+ *
165
+ * If a system clock exceeds this range, operations will throw. This is
166
+ * intentional — there's no reasonable fallback for a misconfigured clock.
62
167
  */
63
- export type MmSs =
64
- | Exclude<D, "0"> // 1..9 (single digit)
65
- | `1${D}` // 10..19
66
- | `2${D}` // 20..29
67
- | `3${D}` // 30..39
68
- | `4${D}` // 40..49
69
- | `5${D}`; // 50..59
168
+ export const Millis = /*#__PURE__*/ brand(
169
+ "Millis",
170
+ /*#__PURE__*/ lessThan(maxMillisWithInfinity)(NonNegativeInt),
171
+ );
172
+ export type Millis = typeof Millis.Type;
70
173
 
71
- /** Hours 1-23. Used internally for {@link DurationString} validation. */
72
- export type Hours =
73
- | Exclude<D, "0"> // 1-9
74
- | `1${D}` // 10-19
75
- | `2${"0" | "1" | "2" | "3"}`; // 20-23
174
+ /** Minimum {@link Millis} value. */
175
+ export const minMillis = 0 as Millis;
76
176
 
77
- /** Days 1-99. Used internally for {@link DurationString} validation. */
78
- export type Days = Exclude<D, "0"> | `${Exclude<D, "0">}${D}`;
177
+ /** Maximum {@link Millis} value. */
178
+ export const maxMillis = (maxMillisWithInfinity - 1) as Millis;
79
179
 
80
180
  /**
81
- * Template literal type for compile-time validated duration strings.
181
+ * Converts {@link Millis} to {@link DateIso}.
82
182
  *
83
- * Allowed patterns: basic units (ms, s, m, h, d) and logical combinations
84
- * (s+ms, m+s, h+m, d+h).
183
+ * This is a safe cast because {@link Millis} guarantees a valid timestamp range
184
+ * that always produces a valid ISO string.
185
+ */
186
+ export const millisToDateIso = (value: Millis): DateIso =>
187
+ new globalThis.Date(value).toISOString() as DateIso;
188
+
189
+ /**
190
+ * Duration can be either a {@link DurationLiteral} or milliseconds as
191
+ * {@link Millis}.
192
+ */
193
+ export type Duration = DurationLiteral | Millis;
194
+
195
+ /**
196
+ * Duration literal with compile-time validation.
85
197
  *
86
198
  * Supported formats:
87
199
  *
88
- * - Milliseconds: `0ms`, `1ms`, `500ms`, `999ms`
89
- * - Seconds: `5s`, `30s` (1-59, single digit for 1-9)
90
- * - Minutes: `5m`, `30m` (1-59, single digit for 1-9)
91
- * - Hours: `1h`, `12h`, `23h` (1-23)
92
- * - Days: `1d`, `30d`, `99d` (1-99)
93
- * - Combinations: `1s 250ms`, `30m 15s`, `2h 45m`, `7d 12h`
200
+ * - Milliseconds: `1ms`, `500ms`, `999ms` (1-999)
201
+ * - Seconds: `1s`, `59s`, `12.5s` (1-59, 1.1-59.9)
202
+ * - Minutes: `1m`, `59m`, `12.5m` (1-59, 1.1-59.9)
203
+ * - Hours: `1h`, `23h`, `12.5h` (1-23, 1.1-23.9)
204
+ * - Days: `1d`, `6d`, `1.5d` (1-6, 1.1-6.9)
205
+ * - Weeks: `1w`, `51w`, `1.5w` (1-51, 1.1-51.9)
206
+ * - Months: not supported (variable length)
207
+ * - Years: `1y`, `99y`, `1.5y` (1-99, 1.1-99.9)
208
+ *
209
+ * Each unit is limited to values that can't be expressed in the next larger
210
+ * unit, ensuring every duration has exactly one canonical representation (e.g.,
211
+ * 1000ms must be written as `"1s"`, not `"1000ms"`).
94
212
  *
95
- * Note: Duration strings are for developer experience only - they provide
96
- * readable, compile-time validated expressions but should never be persisted or
97
- * parsed from users as they are not localized. Always convert to NonNegativeInt
98
- * (milliseconds) for storage and APIs.
213
+ * Decimal values cover cases like 1.5s (1500ms) or 1.5h (90 minutes) without
214
+ * allowing redundant forms. For precise values that don't fit (e.g., 1050ms),
215
+ * use {@link Millis} directly.
216
+ *
217
+ * Zero duration (0ms) is not supported. For yielding without delay, use `await
218
+ * Promise.resolve()` for microtasks or the {@link yieldNow} for macrotasks.
219
+ *
220
+ * See {@link Duration} for a type that also accepts {@link Millis}. Use
221
+ * {@link durationToMillis} to convert to milliseconds.
222
+ */
223
+ export type DurationLiteral =
224
+ | DurationLiteralMilliseconds
225
+ | DurationLiteralSeconds
226
+ | DurationLiteralMinutes
227
+ | DurationLiteralHours
228
+ | DurationLiteralDays
229
+ | DurationLiteralWeeks
230
+ | DurationLiteralYears;
231
+
232
+ /** Milliseconds duration: `"1ms"` to `"999ms"`. See {@link DurationLiteral}. */
233
+ export type DurationLiteralMilliseconds =
234
+ | `${Digit1To9}ms` // 1-9
235
+ | `${Digit1To9}${Digit}ms` // 10-99
236
+ | `${Digit1To9}${Digit}${Digit}ms`; // 100-999
237
+
238
+ /**
239
+ * Seconds duration: `"1s"` to `"59s"` or `"1.1s"` to `"59.9s"`. See
240
+ * {@link DurationLiteral}.
99
241
  */
100
- export type DurationString =
101
- | `${D}ms`
102
- | `${D}${D}ms`
103
- | `${D}${D}${D}ms`
104
- | `${MmSs}s`
105
- | `${MmSs}m`
106
- | `${Hours}h`
107
- | `${Days}d`
108
- | `${MmSs}s ${D}ms`
109
- | `${MmSs}s ${D}${D}ms`
110
- | `${MmSs}s ${D}${D}${D}ms`
111
- | `${MmSs}m ${MmSs}s`
112
- | `${Hours}h ${MmSs}m`
113
- | `${Days}d ${Hours}h`;
242
+ export type DurationLiteralSeconds =
243
+ | `${Digit1To59}s` // 1-59
244
+ | `${Digit1To59}.${Digit1To9}s`; // 1.1-59.9
114
245
 
115
246
  /**
116
- * Duration can be either a {@link DurationString} or milliseconds as
117
- * {@link NonNegativeInt}.
247
+ * Minutes duration: `"1m"` to `"59m"` or `"1.1m"` to `"59.9m"`. See
248
+ * {@link DurationLiteral}.
118
249
  */
119
- export type Duration = DurationString | NonNegativeInt;
250
+ export type DurationLiteralMinutes =
251
+ | `${Digit1To59}m` // 1-59
252
+ | `${Digit1To59}.${Digit1To9}m`; // 1.1-59.9
253
+
254
+ /**
255
+ * Hours duration: `"1h"` to `"23h"` or `"1.1h"` to `"23.9h"`. See
256
+ * {@link DurationLiteral}.
257
+ */
258
+ export type DurationLiteralHours =
259
+ | `${Digit1To23}h` // 1-23
260
+ | `${Digit1To23}.${Digit1To9}h`; // 1.1-23.9
261
+
262
+ /**
263
+ * Days duration: `"1d"` to `"6d"` or `"1.1d"` to `"6.9d"`. See
264
+ * {@link DurationLiteral}.
265
+ */
266
+ export type DurationLiteralDays =
267
+ | `${Digit1To6}d` // 1-6
268
+ | `${Digit1To6}.${Digit1To9}d`; // 1.1-6.9
269
+
270
+ /**
271
+ * Weeks duration: `"1w"` to `"51w"` or `"1.1w"` to `"51.9w"`. See
272
+ * {@link DurationLiteral}.
273
+ */
274
+ export type DurationLiteralWeeks =
275
+ | `${Digit1To51}w` // 1-51
276
+ | `${Digit1To51}.${Digit1To9}w`; // 1.1-51.9
277
+
278
+ /**
279
+ * Years duration: `"1y"` to `"99y"` or `"1.1y"` to `"99.9y"`. See
280
+ * {@link DurationLiteral}.
281
+ */
282
+ export type DurationLiteralYears =
283
+ | `${Digit1To99}y` // 1-99
284
+ | `${Digit1To99}.${Digit1To9}y`; // 1.1-99.9
120
285
 
121
286
  /**
122
287
  * Converts a duration to milliseconds.
123
288
  *
124
- * Accepts either a {@link DurationString} (e.g., "5m", "1h 30m") or milliseconds
125
- * as {@link NonNegativeInt}.
289
+ * Accepts either a {@link DurationLiteral} (e.g., "5m", "1.5s") or
290
+ * {@link Millis}.
126
291
  *
127
292
  * ### Example
128
293
  *
129
294
  * ```ts
130
- * durationToNonNegativeInt("0ms"); // 0 ✅
131
- * durationToNonNegativeInt("500ms"); // 500
132
- * durationToNonNegativeInt("30s"); // 30000 ✅
133
- * durationToNonNegativeInt("5m"); // 300000 ✅
134
- * durationToNonNegativeInt("12h"); // 43200000 ✅
135
- * durationToNonNegativeInt("7d"); // 604800000 ✅
136
- * durationToNonNegativeInt("2h 45m"); // 9900000 ✅
137
- * durationToNonNegativeInt(5000); // 5000 (already milliseconds)
295
+ * durationToMillis("1ms"); // 1
296
+ * durationToMillis("500ms"); // 500
297
+ * durationToMillis("1.5s"); // 1500
298
+ * durationToMillis("30s"); // 30000
299
+ * durationToMillis("5m"); // 300000
300
+ * durationToMillis("12h"); // 43200000
301
+ * durationToMillis("7d"); // 604800000
302
+ * durationToMillis(Millis.orThrow(5000)); // 5000 (already Millis)
138
303
  * ```
139
304
  */
140
- export const durationToNonNegativeInt = (
141
- duration: Duration,
142
- ): NonNegativeInt => {
143
- // If it's already a NonNegativeInt (milliseconds), return as-is
144
- if (typeof duration === "number") {
145
- return duration;
146
- }
305
+ export const durationToMillis = (duration: Duration): Millis => {
306
+ if (typeof duration === "number") return duration;
147
307
 
148
- // Parse duration string without regex to avoid ReDoS vulnerabilities
149
- const units = {
150
- ms: 1,
151
- s: 1000,
152
- m: 60000,
153
- h: 3600000,
154
- d: 86400000, // 24 * 60 * 60 * 1000
155
- } as const;
156
-
157
- let total = 0;
158
- let i = 0;
159
-
160
- while (i < duration.length) {
161
- // Skip whitespace
162
- while (i < duration.length && duration[i] === " ") {
163
- i++;
164
- }
308
+ const num = parseFloat(duration);
309
+ const unit = duration.endsWith("ms") ? "ms" : duration.at(-1)!;
165
310
 
166
- if (i >= duration.length) break;
311
+ assert(unit in durationUnits, `Unknown duration unit: ${unit}`);
167
312
 
168
- // Parse number
169
- let numStr = "";
170
- while (i < duration.length && duration[i] >= "0" && duration[i] <= "9") {
171
- numStr += duration[i];
172
- i++;
173
- }
313
+ return Millis.orThrow(
314
+ Math.round(num * durationUnits[unit as keyof typeof durationUnits]),
315
+ );
316
+ };
174
317
 
175
- if (numStr === "") break;
176
-
177
- // Parse unit (ms or single char s/m/h/d)
178
- let unit = "";
179
- if (i < duration.length) {
180
- if (
181
- duration[i] === "m" &&
182
- i + 1 < duration.length &&
183
- duration[i + 1] === "s"
184
- ) {
185
- unit = "ms";
186
- i += 2;
187
- } else if (
188
- duration[i] === "s" ||
189
- duration[i] === "m" ||
190
- duration[i] === "h" ||
191
- duration[i] === "d"
192
- ) {
193
- unit = duration[i];
194
- i++;
195
- }
196
- }
318
+ /**
319
+ * Returns a Promise that resolves after the specified duration.
320
+ *
321
+ * Uses {@link Duration} parsing and `globalThis.setTimeout`.
322
+ *
323
+ * ### Example
324
+ *
325
+ * ```ts
326
+ * await setTimeout("1ms");
327
+ * ```
328
+ */
329
+ export const setTimeout = (duration: Duration): Promise<void> =>
330
+ new Promise((resolve) => {
331
+ globalThis.setTimeout(resolve, durationToMillis(duration));
332
+ });
197
333
 
198
- if (unit === "") break;
334
+ const durationUnits = {
335
+ ms: 1,
336
+ s: 1000,
337
+ m: 60000,
338
+ h: 3600000,
339
+ d: 86400000,
340
+ w: 604800000, // 7 days
341
+ y: 31536000000, // 365 days
342
+ } as const;
199
343
 
200
- const value = parseInt(numStr, 10);
201
- total += value * units[unit as keyof typeof units];
344
+ /**
345
+ * Frame budget at 60fps (16ms).
346
+ *
347
+ * Work exceeding this blocks a frame, causing visible jank in animations.
348
+ */
349
+ export const ms60fps = 16 as Millis;
350
+
351
+ /**
352
+ * Frame budget at 120fps (8ms).
353
+ *
354
+ * For high refresh rate displays. Work exceeding this blocks a frame.
355
+ */
356
+ export const ms120fps = 8 as Millis;
357
+
358
+ /**
359
+ * Long task threshold (50ms).
360
+ *
361
+ * Tasks exceeding this are "long tasks" per web standards. Use with
362
+ * {@link yieldNow} to yield periodically and keep UI responsive.
363
+ *
364
+ * @see https://web.dev/articles/optimize-long-tasks
365
+ */
366
+ export const msLongTask = 50 as Millis;
367
+
368
+ /**
369
+ * Formats {@link Millis} as a human-readable duration string.
370
+ *
371
+ * - Under 1 minute: `1.234s`
372
+ * - Under 1 hour: `1m30.000s`
373
+ * - 1 hour or more: `1h30m45.000s`
374
+ *
375
+ * ### Example
376
+ *
377
+ * ```ts
378
+ * formatMillisAsDuration(1234 as Millis); // "1.234s"
379
+ * formatMillisAsDuration(90000 as Millis); // "1m30.000s"
380
+ * formatMillisAsDuration(3661000 as Millis); // "1h1m1.000s"
381
+ * ```
382
+ */
383
+ export const formatMillisAsDuration = (millis: Millis): string => {
384
+ const elapsed = millis / 1000;
385
+ if (elapsed < 60) {
386
+ return `${elapsed.toFixed(3)}s`;
387
+ } else if (elapsed < 3600) {
388
+ const minutes = Math.floor(elapsed / 60);
389
+ const seconds = (elapsed % 60).toFixed(3);
390
+ return `${minutes}m${seconds}s`;
391
+ } else {
392
+ const hours = Math.floor(elapsed / 3600);
393
+ const minutes = Math.floor((elapsed % 3600) / 60);
394
+ const seconds = ((elapsed % 3600) % 60).toFixed(3);
395
+ return `${hours}h${minutes}m${seconds}s`;
202
396
  }
397
+ };
203
398
 
204
- return NonNegativeInt.orThrow(total);
399
+ /**
400
+ * Formats {@link Millis} as `HH:MM:SS.mmm`.
401
+ *
402
+ * ### Example
403
+ *
404
+ * ```ts
405
+ * formatMillisAsClockTime(Millis.orThrow(Date.now())); // "14:32:15.234"
406
+ * ```
407
+ */
408
+ export const formatMillisAsClockTime = (millis: Millis): string => {
409
+ const date = new globalThis.Date(millis);
410
+ const hours = date.getHours().toString().padStart(2, "0");
411
+ const minutes = date.getMinutes().toString().padStart(2, "0");
412
+ const seconds = date.getSeconds().toString().padStart(2, "0");
413
+ const ms = date.getMilliseconds().toString().padStart(3, "0");
414
+ return `${hours}:${minutes}:${seconds}.${ms}`;
205
415
  };
package/src/Tracer.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimal tracer interface for observability.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import type { Ref } from "./Ref.js";
8
+ import type { Run } from "./Task.js";
9
+
10
+ /**
11
+ * Minimal tracer interface for observability.
12
+ *
13
+ * Tracer provides unified observability for both sync and async code. Use it
14
+ * directly for sync operations, or let {@link Run} use it automatically for
15
+ * async tasks.
16
+ *
17
+ * ### Example
18
+ *
19
+ * ```ts
20
+ * // Sync code — use tracer directly
21
+ * const result = deps.tracer?.span("parseData", () => parseData(input));
22
+ *
23
+ * // Async code — Run traces automatically
24
+ * await using run = createRun(deps);
25
+ * const result = await run(fetchUser(id)); // Traced if tracer provided
26
+ * ```
27
+ *
28
+ * TODO: Complete implementation with OpenTelemetry adapter.
29
+ */
30
+ export interface Tracer {
31
+ /**
32
+ * Wraps a sync function in a traced span.
33
+ *
34
+ * Records start time, end time, and success/failure status.
35
+ */
36
+ readonly span: <T>(name: string, fn: () => T) => T;
37
+
38
+ /** Records an event/fact (e.g., "user.purchased", "sync.completed"). */
39
+ readonly event: (name: string, data?: unknown) => void;
40
+ }
41
+
42
+ export interface TracerDep {
43
+ readonly tracer: Tracer;
44
+ }
45
+
46
+ /** Configuration for tracing. */
47
+ export interface TracerConfig {
48
+ /** When `true`, enables trace data collection. Can be toggled at runtime. */
49
+ readonly tracing?: Ref<boolean>;
50
+ }
51
+
52
+ export interface TracerConfigDep {
53
+ readonly tracerConfig: TracerConfig;
54
+ }