Skip to content

Commit 6680ab9

Browse files
committed
Move transforms to utils package
1 parent c29af66 commit 6680ab9

3 files changed

Lines changed: 94 additions & 121 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@solid-primitives/utils": minor
3+
---
4+
5+
Add string transform utilities: `json`, `ndjson`, `lines`, `number`, `safe`, `pipe`.
6+
7+
These are `(string) => T` transform functions useful as the `transform` option for SSE, WebSocket, and similar streaming primitives.

packages/sse/src/transform.ts

Lines changed: 1 addition & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1 @@
1-
/**
2-
* Built-in transform functions for common SSE data formats.
3-
* Pass one of these as the `transform` option to `createSSE`:
4-
*
5-
* ```ts
6-
* const { data } = createSSE<Event[]>(url, { transform: ndjson });
7-
* ```
8-
*/
9-
10-
/**
11-
* Parse SSE message data as a single JSON value.
12-
*
13-
* Equivalent to `JSON.parse` but named for use alongside the other
14-
* transformers in this module.
15-
*
16-
* ```ts
17-
* const { data } = createSSE<{ status: string }>(url, { transform: json });
18-
* ```
19-
*/
20-
export const json = <T>(raw: string): T => JSON.parse(raw) as T;
21-
22-
/**
23-
* Parse SSE message data as newline-delimited JSON (NDJSON / JSON Lines).
24-
*
25-
* Each non-empty line in the event's `data` field is parsed as a separate
26-
* JSON value. Returns an array of the parsed values.
27-
*
28-
* Use this when the server batches multiple JSON objects into a single SSE
29-
* event, one object per line:
30-
*
31-
* ```
32-
* data: {"id":1,"type":"tick"}
33-
* data: {"id":2,"type":"tick"}
34-
*
35-
* ```
36-
*
37-
* ```ts
38-
* const { data } = createSSE<TickEvent[]>(url, { transform: ndjson });
39-
* // data() === [{ id: 1, type: "tick" }, { id: 2, type: "tick" }]
40-
* ```
41-
*/
42-
export const ndjson = <T>(raw: string): T[] =>
43-
raw
44-
.split("\n")
45-
.filter(line => line !== "")
46-
.map(line => JSON.parse(line) as T);
47-
48-
/**
49-
* Split SSE message data into individual lines, returning a `string[]`.
50-
* Empty lines are filtered out.
51-
*
52-
* Use this for multi-line text events that are not JSON.
53-
*
54-
* ```ts
55-
* const { data } = createSSE<string[]>(url, { transform: lines });
56-
* // data() === ["line one", "line two"]
57-
* ```
58-
*/
59-
export const lines = (raw: string): string[] => raw.split("\n").filter(line => line !== "");
60-
61-
/**
62-
* Parse SSE message data as a number using `Number()` semantics.
63-
*
64-
* Use this for streams that emit plain numeric values: counters, progress
65-
* percentages, sensor readings, prices, etc.
66-
*
67-
* ```ts
68-
* const { data } = createSSE<number>(url, { transform: number });
69-
* // data() === 42
70-
* ```
71-
*
72-
* Note: follows `Number()` coercion — `""` → `0`, non-numeric strings → `NaN`.
73-
*/
74-
export const number = (raw: string): number => Number(raw);
75-
76-
/**
77-
* Wrap any transform in a `try/catch` so that a malformed event does not
78-
* throw; instead it returns `fallback` (default `undefined`).
79-
*
80-
* ```ts
81-
* // Returns undefined on bad input instead of throwing
82-
* const { data } = createSSE<MyEvent>(url, { transform: safe(json) });
83-
*
84-
* // With an explicit fallback value
85-
* const { data } = createSSE<number>(url, { transform: safe(number, 0) });
86-
* ```
87-
*/
88-
export function safe<T>(transform: (raw: string) => T): (raw: string) => T | undefined;
89-
export function safe<T>(transform: (raw: string) => T, fallback: T): (raw: string) => T;
90-
export function safe<T>(
91-
transform: (raw: string) => T,
92-
fallback?: T,
93-
): (raw: string) => T | undefined {
94-
return (raw: string): T | undefined => {
95-
try {
96-
return transform(raw);
97-
} catch {
98-
return fallback;
99-
}
100-
};
101-
}
102-
103-
/**
104-
* Compose two transforms into one: the output of `a` is passed as the input
105-
* of `b`.
106-
*
107-
* ```ts
108-
* // Parse NDJSON then keep only "tick" events
109-
* const { data } = createSSE<TickEvent[]>(url, {
110-
* transform: pipe(ndjson<RawEvent>, rows => rows.filter(r => r.type === "tick")),
111-
* });
112-
*
113-
* // Safe JSON followed by a post-processing step
114-
* const { data } = createSSE<string>(url, {
115-
* transform: pipe(safe(json<{ label: string }>), ev => ev?.label ?? ""),
116-
* });
117-
* ```
118-
*/
119-
export function pipe<A, B>(a: (raw: string) => A, b: (a: A) => B): (raw: string) => B {
120-
return (raw: string): B => b(a(raw));
121-
}
1+
export { json, ndjson, lines, number, safe, pipe } from "@solid-primitives/utils";

packages/utils/src/index.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,3 +311,89 @@ export function handleDiffArray<T>(
311311
if (!prev.includes(currEl)) handleAdded(currEl);
312312
}
313313
}
314+
315+
// ─── String transforms ────────────────────────────────────────────────────────
316+
317+
/**
318+
* Parse a string as a single JSON value.
319+
*
320+
* ```ts
321+
* const { data } = createSSE<{ status: string }>(url, { transform: json });
322+
* ```
323+
*/
324+
export const json = <T>(raw: string): T => JSON.parse(raw) as T;
325+
326+
/**
327+
* Parse a string as newline-delimited JSON (NDJSON / JSON Lines).
328+
*
329+
* Each non-empty line is parsed as a separate JSON value and returned as an array.
330+
*
331+
* ```ts
332+
* const { data } = createSSE<TickEvent[]>(url, { transform: ndjson });
333+
* // data() === [{ id: 1, type: "tick" }, { id: 2, type: "tick" }]
334+
* ```
335+
*/
336+
export const ndjson = <T>(raw: string): T[] =>
337+
raw
338+
.split("\n")
339+
.filter(line => line !== "")
340+
.map(line => JSON.parse(line) as T);
341+
342+
/**
343+
* Split a string into individual lines, returning a `string[]`. Empty lines are filtered out.
344+
*
345+
* ```ts
346+
* const { data } = createSSE<string[]>(url, { transform: lines });
347+
* // data() === ["line one", "line two"]
348+
* ```
349+
*/
350+
export const lines = (raw: string): string[] => raw.split("\n").filter(line => line !== "");
351+
352+
/**
353+
* Parse a string as a number using `Number()` semantics.
354+
*
355+
* Note: `""` → `0`, non-numeric strings → `NaN`.
356+
*
357+
* ```ts
358+
* const { data } = createSSE<number>(url, { transform: number });
359+
* // data() === 42
360+
* ```
361+
*/
362+
export const number = (raw: string): number => Number(raw);
363+
364+
/**
365+
* Wrap any `(string) => T` transform in a `try/catch`. Returns `fallback`
366+
* (default `undefined`) instead of throwing on malformed input.
367+
*
368+
* ```ts
369+
* const { data } = createSSE<MyEvent>(url, { transform: safe(json) });
370+
* const { data } = createSSE<number>(url, { transform: safe(number, 0) });
371+
* ```
372+
*/
373+
export function safe<T>(transform: (raw: string) => T): (raw: string) => T | undefined;
374+
export function safe<T>(transform: (raw: string) => T, fallback: T): (raw: string) => T;
375+
export function safe<T>(
376+
transform: (raw: string) => T,
377+
fallback?: T,
378+
): (raw: string) => T | undefined {
379+
return (raw: string): T | undefined => {
380+
try {
381+
return transform(raw);
382+
} catch {
383+
return fallback;
384+
}
385+
};
386+
}
387+
388+
/**
389+
* Compose two transforms into one: the output of `a` is passed as the input of `b`.
390+
*
391+
* ```ts
392+
* const { data } = createSSE<RawEvent[]>(url, {
393+
* transform: pipe(ndjson<RawEvent>, rows => rows.filter(r => r.type === "tick")),
394+
* });
395+
* ```
396+
*/
397+
export function pipe<A, B>(a: (raw: string) => A, b: (a: A) => B): (raw: string) => B {
398+
return (raw: string): B => b(a(raw));
399+
}

0 commit comments

Comments
 (0)