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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/.tsbuildinfo.json +1 -1
  2. package/dist/NoopTracer.d.ts.map +1 -1
  3. package/dist/NoopTracer.js +6 -1
  4. package/dist/NoopTracer.js.map +1 -1
  5. package/dist/browser/Opfs/utils.d.ts +7 -4
  6. package/dist/browser/Opfs/utils.d.ts.map +1 -1
  7. package/dist/browser/Opfs/utils.js +30 -18
  8. package/dist/browser/Opfs/utils.js.map +1 -1
  9. package/dist/effect/Debug.d.ts +33 -30
  10. package/dist/effect/Debug.d.ts.map +1 -1
  11. package/dist/effect/Debug.js +280 -221
  12. package/dist/effect/Debug.js.map +1 -1
  13. package/dist/effect/Schema/debug-diff.test.js +1 -1
  14. package/dist/effect/Schema/debug-diff.test.js.map +1 -1
  15. package/dist/effect/mod.d.ts +1 -1
  16. package/dist/effect/mod.d.ts.map +1 -1
  17. package/dist/effect/mod.js +1 -1
  18. package/dist/effect/mod.js.map +1 -1
  19. package/dist/global.d.ts +3 -0
  20. package/dist/global.d.ts.map +1 -1
  21. package/dist/global.js.map +1 -1
  22. package/dist/guards.d.ts +14 -0
  23. package/dist/guards.d.ts.map +1 -1
  24. package/dist/guards.js +14 -0
  25. package/dist/guards.js.map +1 -1
  26. package/dist/mod.d.ts +195 -3
  27. package/dist/mod.d.ts.map +1 -1
  28. package/dist/mod.js +149 -4
  29. package/dist/mod.js.map +1 -1
  30. package/dist/node/mod.d.ts +31 -4
  31. package/dist/node/mod.d.ts.map +1 -1
  32. package/dist/node/mod.js +32 -5
  33. package/dist/node/mod.js.map +1 -1
  34. package/package.json +23 -23
  35. package/src/NoopTracer.ts +6 -1
  36. package/src/browser/Opfs/utils.ts +36 -20
  37. package/src/effect/Debug.ts +375 -292
  38. package/src/effect/Schema/debug-diff.test.ts +2 -2
  39. package/src/effect/mod.ts +3 -0
  40. package/src/global.ts +4 -0
  41. package/src/guards.ts +15 -0
  42. package/src/mod.ts +197 -5
  43. package/src/node/mod.ts +35 -5
@@ -75,8 +75,8 @@ describe('debug-diff', () => {
75
75
 
76
76
  test('tagged union', () => {
77
77
  const schema = Schema.Union(
78
- Schema.Struct({ _tag: Schema.Literal('a'), a: Schema.String }),
79
- Schema.Struct({ _tag: Schema.Literal('b'), b: Schema.Number }),
78
+ Schema.TaggedStruct('a', { a: Schema.String }),
79
+ Schema.TaggedStruct('b', { b: Schema.Number }),
80
80
  )
81
81
  const a = { _tag: 'a', a: 'hello' } as const
82
82
  const b = { _tag: 'b', b: 1 } as const
package/src/effect/mod.ts CHANGED
@@ -117,7 +117,10 @@ export {
117
117
  // Subscribable,
118
118
  pipe,
119
119
  Queue,
120
+ RcMap,
121
+ RcRef,
120
122
  Record as ReadonlyRecord,
123
+ Redacted,
121
124
  Ref,
122
125
  Request,
123
126
  Runtime,
package/src/global.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  declare global {
2
2
  export type TODO<_Reason extends string = 'unknown'> = any
3
3
  export type UNUSED<_Reason extends string = 'unknown'> = any
4
+
5
+ interface ImportMeta {
6
+ main: boolean
7
+ }
4
8
  }
5
9
 
6
10
  export {}
package/src/guards.ts CHANGED
@@ -1,8 +1,23 @@
1
+ /** Type guard that narrows `T | undefined` to `T`. Useful for filtering arrays. */
1
2
  export const isNotUndefined = <T>(_: T | undefined): _ is T => _ !== undefined
2
3
 
4
+ /** Type guard that narrows `T | null` to `T`. */
3
5
  export const isNotNull = <T>(_: T | null): _ is T => _ !== null
6
+
7
+ /** Type guard that checks if a value is `undefined`. */
4
8
  export const isUndefined = <T>(_: T | undefined): _ is undefined => _ === undefined
5
9
 
10
+ /** Type guard that checks if a value is `null` or `undefined`. */
6
11
  export const isNil = (val: any): val is null | undefined => val === null || val === undefined
7
12
 
13
+ /**
14
+ * Type guard that narrows `T | undefined | null` to `T`.
15
+ * Commonly used to filter out nullish values from arrays.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const values = [1, null, 2, undefined, 3]
20
+ * const nonNil = values.filter(isNotNil) // [1, 2, 3] (type: number[])
21
+ * ```
22
+ */
8
23
  export const isNotNil = <T>(val: T | undefined | null): val is T => val !== null && val !== undefined
package/src/mod.ts CHANGED
@@ -19,37 +19,102 @@ import type { Types } from 'effect'
19
19
 
20
20
  import { objectToString } from './misc.ts'
21
21
 
22
+ /**
23
+ * Recursively expands type aliases for better IDE hover display.
24
+ *
25
+ * Transforms `{ a: string } & { b: number }` into `{ a: string; b: number }`.
26
+ */
22
27
  export type Prettify<T> = T extends infer U ? { [K in keyof U]: Prettify<U[K]> } : never
23
28
 
29
+ /**
30
+ * Type-level equality check. Returns `true` if `A` and `B` are exactly the same type.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * type Test1 = TypeEq<string, string> // true
35
+ * type Test2 = TypeEq<string, number> // false
36
+ * type Test3 = TypeEq<{ a: 1 }, { a: 1 }> // true
37
+ * ```
38
+ */
24
39
  export type TypeEq<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
25
40
 
26
- /** `A` is subtype of `B` */
41
+ /**
42
+ * Type-level subtype check. Returns `true` if `A` extends `B`.
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * type Test1 = IsSubtype<'foo', string> // true
47
+ * type Test2 = IsSubtype<string, 'foo'> // false
48
+ * ```
49
+ */
27
50
  export type IsSubtype<A, B> = A extends B ? true : false
51
+
52
+ /** Compile-time assertion that `T` is `true`. Useful for type tests. */
28
53
  export type AssertTrue<T extends true> = T
29
54
 
55
+ /** Removes `readonly` modifier from all properties of `T`. */
30
56
  export type Writeable<T> = { -readonly [P in keyof T]: T[P] }
57
+
58
+ /** Recursively removes `readonly` modifier from all properties. */
31
59
  export type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> }
32
60
 
61
+ /** Makes all properties of `T` nullable (allows `null`). */
33
62
  export type Nullable<T> = { [K in keyof T]: T[K] | null }
34
63
 
64
+ /** Union of JavaScript primitive types. */
35
65
  export type Primitive = null | undefined | string | number | boolean | symbol | bigint
36
66
 
67
+ /**
68
+ * Creates a union type that allows specific literals while still accepting the base type.
69
+ * Useful for string/number enums with autocomplete support.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * type Status = LiteralUnion<'pending' | 'active', string>
74
+ * // Allows 'pending', 'active', or any other string
75
+ * ```
76
+ */
37
77
  export type LiteralUnion<LiteralType, BaseType extends Primitive> = LiteralType | (BaseType & Record<never, never>)
38
78
 
79
+ /** Extracts the value type for key `K` from object type `T`, or `never` if key doesn't exist. */
39
80
  export type GetValForKey<T, K> = K extends keyof T ? T[K] : never
40
81
 
82
+ /** Accepts either a single value or a readonly array of values. */
41
83
  export type SingleOrReadonlyArray<T> = T | ReadonlyArray<T>
42
84
 
85
+ /** Returns a Promise that resolves after `ms` milliseconds. */
43
86
  export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
44
87
 
88
+ /**
89
+ * Creates a mutable reference object with a `current` property.
90
+ * Similar to React's `useRef` but works outside of React.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const counter = ref(0)
95
+ * counter.current += 1
96
+ * ```
97
+ */
45
98
  export const ref = <T>(val: T): { current: T } => ({ current: val })
46
99
 
100
+ /**
101
+ * Calls a function `n` times with the current index.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * times(3, (i) => console.log(i)) // logs 0, 1, 2
106
+ * ```
107
+ */
47
108
  export const times = (n: number, fn: (index: number) => {}): void => {
48
109
  for (let i = 0; i < n; i++) {
49
110
  fn(i)
50
111
  }
51
112
  }
52
113
 
114
+ /**
115
+ * Wraps a function call in a try/catch that triggers the debugger on error.
116
+ * Useful for debugging exceptions during development.
117
+ */
53
118
  export const debugCatch = <T>(try_: () => T): T => {
54
119
  try {
55
120
  return try_()
@@ -60,6 +125,10 @@ export const debugCatch = <T>(try_: () => T): T => {
60
125
  }
61
126
  }
62
127
 
128
+ /**
129
+ * Recursively removes `undefined` values from an object or array in place.
130
+ * Mutates the input value.
131
+ */
63
132
  export const recRemoveUndefinedValues = (val: any): void => {
64
133
  if (Array.isArray(val)) {
65
134
  val.forEach(recRemoveUndefinedValues)
@@ -79,13 +148,24 @@ export const recRemoveUndefinedValues = (val: any): void => {
79
148
  */
80
149
  export const sluggify = (str: string, separator = '-') => str.replace(/[^a-zA-Z0-9]/g, separator)
81
150
 
151
+ /**
152
+ * Creates a property accessor function for use in pipelines.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * const users = [{ name: 'Alice' }, { name: 'Bob' }]
157
+ * const names = users.map(prop('name')) // ['Alice', 'Bob']
158
+ * ```
159
+ */
82
160
  export const prop =
83
161
  <T extends {}, K extends keyof T>(key: K) =>
84
162
  (obj: T): T[K] =>
85
163
  obj[key]
86
164
 
165
+ /** Capitalizes the first letter of a string. */
87
166
  export const capitalizeFirstLetter = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1)
88
167
 
168
+ /** Type guard that checks if a value is a readonly array. */
89
169
  export const isReadonlyArray = <I, T>(value: ReadonlyArray<I> | T): value is ReadonlyArray<I> => Array.isArray(value)
90
170
 
91
171
  /**
@@ -99,6 +179,14 @@ export function casesHandled(unexpectedCase: never): never {
99
179
  throw new Error(`A case was not handled for value: ${truncate(objectToString(unexpectedCase), 1000)}`)
100
180
  }
101
181
 
182
+ /**
183
+ * Throws if the condition is false. Use for runtime assertions that should never fail.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * assertNever(user !== undefined, 'User must be loaded')
188
+ * ```
189
+ */
102
190
  export const assertNever = (failIfFalse: boolean, msg?: string): void => {
103
191
  if (failIfFalse === false) {
104
192
  // biome-ignore lint/suspicious/noDebugger: debugging
@@ -107,6 +195,14 @@ export const assertNever = (failIfFalse: boolean, msg?: string): void => {
107
195
  }
108
196
  }
109
197
 
198
+ /**
199
+ * Identity function that triggers the debugger. Useful for debugging pipelines.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * data.pipe(transform, debuggerPipe, format) // Pauses debugger here
204
+ * ```
205
+ */
110
206
  export const debuggerPipe = <T>(val: T): T => {
111
207
  // biome-ignore lint/suspicious/noDebugger: debugging
112
208
  debugger
@@ -121,16 +217,40 @@ const truncate = (str: string, length: number): string => {
121
217
  }
122
218
  }
123
219
 
220
+ /**
221
+ * Placeholder for unimplemented code paths. Triggers debugger and throws.
222
+ *
223
+ * @example
224
+ * ```ts
225
+ * const parseFormat = (format: Format) => {
226
+ * switch (format) {
227
+ * case 'json': return parseJson
228
+ * case 'xml': return notYetImplemented('XML parsing')
229
+ * }
230
+ * }
231
+ * ```
232
+ */
124
233
  export const notYetImplemented = (msg?: string): never => {
125
234
  // biome-ignore lint/suspicious/noDebugger: debugging
126
235
  debugger
127
236
  throw new Error(`Not yet implemented: ${msg}`)
128
237
  }
129
238
 
239
+ /** A function that does nothing. Useful as a default callback. */
130
240
  export const noop = () => {}
131
241
 
242
+ /** A function that returns a value of type `T`. */
132
243
  export type Thunk<T> = () => T
133
244
 
245
+ /**
246
+ * If the input is a function, calls it and returns the result. Otherwise returns the value directly.
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * unwrapThunk(5) // 5
251
+ * unwrapThunk(() => 5) // 5
252
+ * ```
253
+ */
134
254
  export const unwrapThunk = <T>(_: T | (() => T)): T => {
135
255
  if (typeof _ === 'function') {
136
256
  return (_ as any)()
@@ -139,6 +259,10 @@ export const unwrapThunk = <T>(_: T | (() => T)): T => {
139
259
  }
140
260
  }
141
261
 
262
+ /**
263
+ * Transforms nullable fields (those that include `null`) into optional fields.
264
+ * Useful for converting database schemas to TypeScript types.
265
+ */
142
266
  export type NullableFieldsToOptional<T> = Types.Simplify<
143
267
  Partial<T> & {
144
268
  [K in keyof T as null extends T[K] ? K : never]?: Exclude<T[K], null>
@@ -147,12 +271,33 @@ export type NullableFieldsToOptional<T> = Types.Simplify<
147
271
  }
148
272
  >
149
273
 
150
- /** `end` is not included */
274
+ /**
275
+ * Creates an array of numbers from `start` (inclusive) to `end` (exclusive).
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * range(0, 5) // [0, 1, 2, 3, 4]
280
+ * range(3, 7) // [3, 4, 5, 6]
281
+ * ```
282
+ */
151
283
  export const range = (start: number, end: number): number[] => {
152
284
  const length = end - start
153
285
  return Array.from({ length }, (_, i) => start + i)
154
286
  }
155
287
 
288
+ /**
289
+ * Rate-limits function calls to at most once per `ms` milliseconds.
290
+ * Trailing calls are preserved—if called during the wait period, the function
291
+ * will be called again after the timeout.
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * const throttledSave = throttle(() => saveData(), 1000)
296
+ * throttledSave() // Executes immediately
297
+ * throttledSave() // Queued, executes after 1 second
298
+ * throttledSave() // Ignored (already queued)
299
+ * ```
300
+ */
156
301
  export const throttle = (fn: () => void, ms: number) => {
157
302
  let shouldWait = false
158
303
  let shouldCallAgain = false
@@ -179,13 +324,26 @@ export const throttle = (fn: () => void, ms: number) => {
179
324
  }
180
325
  }
181
326
 
327
+ /**
328
+ * Generates a W3C Trace Context `traceparent` header from an OpenTelemetry span.
329
+ * @see https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
330
+ */
182
331
  export const getTraceParentHeader = (parentSpan: otel.Span) => {
183
332
  const spanContext = parentSpan.spanContext()
184
- // Format: {version}-{trace_id}-{span_id}-{trace_flags}
185
- // https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
186
333
  return `00-${spanContext.traceId}-${spanContext.spanId}-01`
187
334
  }
188
335
 
336
+ /**
337
+ * Asserts that a tagged union value has a specific tag, narrowing its type.
338
+ * Throws if the tag doesn't match.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * type Result = { _tag: 'ok'; value: number } | { _tag: 'error'; message: string }
343
+ * const result: Result = ...
344
+ * const ok = assertTag(result, 'ok') // Type is { _tag: 'ok'; value: number }
345
+ * ```
346
+ */
189
347
  export const assertTag = <TObj extends { _tag: string }, TTag extends TObj['_tag']>(
190
348
  obj: TObj,
191
349
  tag: TTag,
@@ -197,6 +355,20 @@ export const assertTag = <TObj extends { _tag: string }, TTag extends TObj['_tag
197
355
  return obj as any
198
356
  }
199
357
 
358
+ /**
359
+ * Memoizes a function by JSON-stringifying its arguments as the cache key.
360
+ * Suitable for functions with serializable arguments.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * const expensiveCalc = memoizeByStringifyArgs((a: number, b: number) => {
365
+ * console.log('Computing...')
366
+ * return a + b
367
+ * })
368
+ * expensiveCalc(1, 2) // logs 'Computing...', returns 3
369
+ * expensiveCalc(1, 2) // returns 3 (cached, no log)
370
+ * ```
371
+ */
200
372
  export const memoizeByStringifyArgs = <T extends (...args: any[]) => any>(fn: T): T => {
201
373
  const cache = new Map<string, ReturnType<T>>()
202
374
 
@@ -212,6 +384,18 @@ export const memoizeByStringifyArgs = <T extends (...args: any[]) => any>(fn: T)
212
384
  }) as any
213
385
  }
214
386
 
387
+ /**
388
+ * Memoizes a single-argument function using reference equality for cache lookup.
389
+ * Suitable for functions where arguments are objects that should be compared by reference.
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * const processUser = memoizeByRef((user: User) => expensiveTransform(user))
394
+ * processUser(userA) // Computes
395
+ * processUser(userA) // Returns cached (same reference)
396
+ * processUser(userB) // Computes (different reference)
397
+ * ```
398
+ */
215
399
  export const memoizeByRef = <T extends (arg: any) => any>(fn: T): T => {
216
400
  const cache = new Map<Parameters<T>[0], ReturnType<T>>()
217
401
 
@@ -226,15 +410,23 @@ export const memoizeByRef = <T extends (arg: any) => any>(fn: T): T => {
226
410
  }) as any
227
411
  }
228
412
 
413
+ /** Type guard that checks if a value is a non-empty string. */
229
414
  export const isNonEmptyString = (str: string | undefined | null): str is string => {
230
415
  return typeof str === 'string' && str.length > 0
231
416
  }
232
417
 
418
+ /** Type guard that checks if a value is a Promise (has a `then` method). */
233
419
  export const isPromise = (value: any): value is Promise<unknown> => typeof value?.then === 'function'
234
420
 
421
+ /** Type guard that checks if a value is iterable (has a `Symbol.iterator` method). */
235
422
  export const isIterable = <T>(value: any): value is Iterable<T> => typeof value?.[Symbol.iterator] === 'function'
236
423
 
237
- /** This utility "lies" as a means of compat with libs that don't explicitly type optionals as unioned with `undefined`. */
424
+ /**
425
+ * Type-level utility that removes `undefined` from all property types.
426
+ * Used for compatibility with libraries that don't type optionals as `| undefined`.
427
+ *
428
+ * Note: This is a type-level lie—the runtime value is unchanged.
429
+ */
238
430
  export const omitUndefineds = <T extends Record<keyof any, unknown>>(
239
431
  rec: T,
240
432
  ): {
package/src/node/mod.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import * as http from 'node:http'
2
- import * as PlatformNode from '@effect/platform-node'
3
2
  import { layer as ParcelWatcherLayer } from '@effect/platform-node/NodeFileSystem/ParcelWatcher'
4
3
  import { Effect, Layer } from 'effect'
5
4
  import { OtelTracer, UnknownError } from '../effect/mod.ts'
@@ -53,8 +52,39 @@ export const OtelLiveDummy: Layer.Layer<OtelTracer.OtelTracer> = Layer.suspend((
53
52
  })
54
53
 
55
54
  /**
56
- * Layer that enables recursive file watching by combining the Node filesystem implementation with
57
- * the Parcel-based watch backend. Mirrored from Effect’s platform-node Parcel watcher layer:
58
- * https://github.com/Effect-TS/effect/blob/main/packages/platform-node/src/NodeFileSystem/ParcelWatcher.ts
55
+ * Layer that provides WatchBackend for recursive file watching via @parcel/watcher.
56
+ * This layer alone does NOT provide FileSystem - it only provides the watch backend.
57
+ *
58
+ * IMPORTANT: Layer ordering matters! When composing with NodeFileSystem.layer, use
59
+ * `NodeFileSystemWithWatch` instead, or ensure WatchBackend is available when FileSystem
60
+ * is constructed by using `Layer.provideMerge`:
61
+ *
62
+ * ```ts
63
+ * // ✅ CORRECT: Use the pre-composed layer
64
+ * Effect.provide(NodeFileSystemWithWatch)
65
+ *
66
+ * // ✅ CORRECT: Manual composition with Layer.provideMerge
67
+ * const layer = PlatformNode.NodeFileSystem.layer.pipe(Layer.provideMerge(NodeRecursiveWatchLayer))
68
+ * Effect.provide(layer)
69
+ *
70
+ * // ❌ WRONG: Chained Effect.provide - WatchBackend won't be used!
71
+ * Effect.provide(NodeRecursiveWatchLayer).pipe(Effect.provide(PlatformNode.NodeFileSystem.layer))
72
+ * ```
73
+ *
74
+ * @see https://github.com/Effect-TS/effect/issues/5913
59
75
  */
60
- export const NodeRecursiveWatchLayer = Layer.mergeAll(PlatformNode.NodeFileSystem.layer, ParcelWatcherLayer)
76
+ export const NodeRecursiveWatchLayer = ParcelWatcherLayer
77
+
78
+ /**
79
+ * Pre-composed layer providing FileSystem with recursive file watching via @parcel/watcher.
80
+ * This is the recommended way to get a FileSystem that supports recursive watching.
81
+ *
82
+ * Use this layer when you need to watch files recursively (e.g., watching nested directories).
83
+ * Without recursive watching, Node.js's built-in fs.watch only detects changes in the
84
+ * immediate directory, not in subdirectories.
85
+ */
86
+ export { NodeFileSystem } from '@effect/platform-node'
87
+
88
+ import { NodeFileSystem } from '@effect/platform-node'
89
+
90
+ export const NodeFileSystemWithWatch = NodeFileSystem.layer.pipe(Layer.provideMerge(ParcelWatcherLayer))