datool 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -169,10 +169,14 @@ Tails a local file and optionally honors `history`.
169
169
 
170
170
  ```ts
171
171
  sources.file({
172
+ defaultHistory: 5,
172
173
  path: "./app.log",
173
174
  })
174
175
  ```
175
176
 
177
+ `defaultHistory` lets the file source emit existing lines on startup when the URL
178
+ does not include a `history` query param.
179
+
176
180
  ### `sources.command(...)`
177
181
 
178
182
  Spawns a local process and streams stdout lines.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datool",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "Local-only config-driven log viewer with SSE streaming and a generic table UI.",
6
6
  "bin": {
@@ -25,6 +25,7 @@ import type {
25
25
  DatoolClientConfig,
26
26
  DatoolClientStream,
27
27
  DatoolColumn,
28
+ DatoolSseEndEvent,
28
29
  DatoolRowEvent,
29
30
  DatoolSseErrorEvent,
30
31
  } from "../shared/types"
@@ -45,6 +46,7 @@ import {
45
46
  writeDatoolUrlState,
46
47
  } from "@/lib/datool-url-state"
47
48
  import { LOG_VIEWER_ICONS } from "@/lib/datool-icons"
49
+ import { upsertViewerRow } from "./stream-state"
48
50
 
49
51
  type ViewerRow = Record<string, unknown> & {
50
52
  __datoolRowId: string
@@ -242,6 +244,10 @@ function parseErrorEvent(event: MessageEvent<string>) {
242
244
  return JSON.parse(event.data) as DatoolSseErrorEvent
243
245
  }
244
246
 
247
+ function parseEndEvent(event: MessageEvent<string>) {
248
+ return JSON.parse(event.data) as DatoolSseEndEvent
249
+ }
250
+
245
251
  function stringifyRowActionValue(value: unknown) {
246
252
  if (value === null || value === undefined) {
247
253
  return ""
@@ -773,13 +779,12 @@ export default function App() {
773
779
  const handleRow = (event: MessageEvent<string>) => {
774
780
  const payload = parseRowEvent(event)
775
781
 
776
- setRows((currentRows) => [
777
- ...currentRows,
778
- {
782
+ setRows((currentRows) =>
783
+ upsertViewerRow(currentRows, {
779
784
  ...payload.row,
780
785
  __datoolRowId: payload.id,
781
- },
782
- ])
786
+ })
787
+ )
783
788
  }
784
789
 
785
790
  const handleRuntimeError = (event: MessageEvent<string>) => {
@@ -788,6 +793,13 @@ export default function App() {
788
793
  setErrorMessage(payload.message)
789
794
  }
790
795
 
796
+ const handleEnd = (event: MessageEvent<string>) => {
797
+ parseEndEvent(event)
798
+ eventSource.close()
799
+ eventSourceRef.current = null
800
+ setIsConnected(false)
801
+ }
802
+
791
803
  eventSource.onopen = () => {
792
804
  setIsConnected(true)
793
805
  }
@@ -797,14 +809,19 @@ export default function App() {
797
809
  }
798
810
 
799
811
  eventSource.addEventListener("row", handleRow as EventListener)
800
- eventSource.addEventListener("error", handleRuntimeError as EventListener)
812
+ eventSource.addEventListener(
813
+ "runtime-error",
814
+ handleRuntimeError as EventListener
815
+ )
816
+ eventSource.addEventListener("end", handleEnd as EventListener)
801
817
 
802
818
  return () => {
803
819
  eventSource.removeEventListener("row", handleRow as EventListener)
804
820
  eventSource.removeEventListener(
805
- "error",
821
+ "runtime-error",
806
822
  handleRuntimeError as EventListener
807
823
  )
824
+ eventSource.removeEventListener("end", handleEnd as EventListener)
808
825
  eventSource.close()
809
826
  setIsConnected(false)
810
827
  }
@@ -0,0 +1,20 @@
1
+ export type StreamViewerRow = Record<string, unknown> & {
2
+ __datoolRowId: string
3
+ }
4
+
5
+ export function upsertViewerRow<T extends StreamViewerRow>(
6
+ currentRows: T[],
7
+ nextRow: T
8
+ ) {
9
+ const existingIndex = currentRows.findIndex(
10
+ (row) => row.__datoolRowId === nextRow.__datoolRowId
11
+ )
12
+
13
+ if (existingIndex < 0) {
14
+ return [...currentRows, nextRow]
15
+ }
16
+
17
+ return currentRows.map((row, index) =>
18
+ index === existingIndex ? nextRow : row
19
+ )
20
+ }
@@ -10,6 +10,7 @@ import type {
10
10
  DatoolActionResolveResult,
11
11
  DatoolActionResponse,
12
12
  DatoolConfig,
13
+ DatoolSseEndEvent,
13
14
  DatoolSseErrorEvent,
14
15
  } from "../shared/types"
15
16
 
@@ -74,6 +75,12 @@ function toErrorPayload(error: unknown): DatoolSseErrorEvent {
74
75
  }
75
76
  }
76
77
 
78
+ function toEndPayload(reason: DatoolSseEndEvent["reason"]): DatoolSseEndEvent {
79
+ return {
80
+ reason,
81
+ }
82
+ }
83
+
77
84
  function encodeSseEvent(event: string, data: unknown) {
78
85
  return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`
79
86
  }
@@ -279,15 +286,23 @@ function createSseResponse(
279
286
  abortController.signal,
280
287
  {
281
288
  async onError(error) {
282
- send("error", toErrorPayload(error))
289
+ send("runtime-error", toErrorPayload(error))
283
290
  },
284
291
  async onRow(payload) {
285
292
  send("row", payload)
286
293
  },
287
294
  }
288
295
  )
296
+ .then(() => {
297
+ if (!abortController.signal.aborted) {
298
+ send("end", toEndPayload("completed"))
299
+ }
300
+ })
289
301
  .catch((error) => {
290
- send("error", toErrorPayload(error))
302
+ if (!abortController.signal.aborted) {
303
+ send("runtime-error", toErrorPayload(error))
304
+ send("end", toEndPayload("error"))
305
+ }
291
306
  })
292
307
  .finally(() => {
293
308
  clearInterval(heartbeat)
@@ -6,6 +6,7 @@ import type { DatoolSource } from "../../shared/types"
6
6
  type Resolver<T> = T | ((context: { query: URLSearchParams }) => T)
7
7
 
8
8
  export type FileSourceOptions = {
9
+ defaultHistory?: number
9
10
  historyParam?: string
10
11
  path: Resolver<string>
11
12
  pollIntervalMs?: number
@@ -21,20 +22,28 @@ function resolveValue<T>(value: Resolver<T>, query: URLSearchParams) {
21
22
  return value
22
23
  }
23
24
 
24
- function toHistoryLineCount(query: URLSearchParams, historyParam: string) {
25
+ function normalizeHistoryLineCount(value: number) {
26
+ if (!Number.isFinite(value) || value <= 0) {
27
+ return 0
28
+ }
29
+
30
+ return Math.floor(value)
31
+ }
32
+
33
+ function resolveHistoryLineCount(
34
+ query: URLSearchParams,
35
+ historyParam: string,
36
+ defaultHistory: number
37
+ ) {
25
38
  const rawValue = query.get(historyParam)
26
39
 
27
40
  if (!rawValue) {
28
- return 0
41
+ return normalizeHistoryLineCount(defaultHistory)
29
42
  }
30
43
 
31
44
  const parsedValue = Number.parseInt(rawValue, 10)
32
45
 
33
- if (!Number.isFinite(parsedValue) || parsedValue <= 0) {
34
- return 0
35
- }
36
-
37
- return parsedValue
46
+ return normalizeHistoryLineCount(parsedValue)
38
47
  }
39
48
 
40
49
  function normalizeLines(content: string) {
@@ -76,10 +85,15 @@ async function readAppendedText(filePath: string, start: number, end: number) {
76
85
  export function fileSource(options: FileSourceOptions): DatoolSource {
77
86
  return {
78
87
  async open(context) {
88
+ const defaultHistory = options.defaultHistory ?? 0
79
89
  const historyParam = options.historyParam ?? "history"
80
90
  const pollIntervalMs = options.pollIntervalMs ?? 250
81
91
  const filePath = resolveValue(options.path, context.query)
82
- const historyLineCount = toHistoryLineCount(context.query, historyParam)
92
+ const historyLineCount = resolveHistoryLineCount(
93
+ context.query,
94
+ historyParam,
95
+ defaultHistory
96
+ )
83
97
  let stat = await fs.stat(filePath)
84
98
  let position = stat.size
85
99
  let remainder = ""
@@ -179,6 +179,10 @@ export type DatoolSseErrorEvent = {
179
179
  message: string
180
180
  }
181
181
 
182
+ export type DatoolSseEndEvent = {
183
+ reason: "completed" | "error"
184
+ }
185
+
182
186
  export type DatoolActionRequest = {
183
187
  rows: Record<string, unknown>[]
184
188
  }