PageSourceSearch

https://smartcone.ai/src/shell/shared/ndjsonUtils.js

js smartcone.ai collected 2026-10-05 15:07:08 UTC 6,174 bytes, 148 lines download raw bytes

1/**
2 * ndjsonUtils.js – Shared NDJSON wire-protocol utilities.
3 *
4 * This module is the single source of truth for NDJSON frame building,
5 * parsing, token handling, and serialization logic that is used by the
6 * WebSocket/HTTP transport layer.  Both shellWss.js (shell send path) and
7 * wssClient.js (generic send/receive path) import from here so that no
8 * NDJSON logic is duplicated between the two.
9 *
10 * ── Responsibilities ──────────────────────────────────────────────────────
11 *
12 *  splitNdjsonLines  – Convert a raw NDJSON document string into an array of
13 *                       non-empty JSON lines ready to be sent as individual
14 *                       WebSocket messages.
15 *
16 *  buildAuthFrame    – Produce the JSON-serialised authentication frame that
17 *                       must precede any authenticated NDJSON payload.
18 *
19 *  parseAckLines     – Parse the acknowledgement lines returned by zimserver
20 *                       after a save/upload and return a structured result.
21 *
22 *  recordsFromData   – Normalise a raw WS/HTTP response (string, array, or
23 *                       object) into an array of flat record objects suitable
24 *                       for publishing to the stream registry.
25 *
26 * ── Usage ─────────────────────────────────────────────────────────────────
27 *
28 *   import {
29 *     splitNdjsonLines,
30 *     buildAuthFrame,
31 *     parseAckLines,
32 *     recordsFromData,
33 *   } from "../shared/ndjsonUtils.js";
34 */
35
36import { jsonWithTypeFirst } from "../common/ndjson.js";
37import { flattenRecord }     from "../../streams/schema.js";
38
39// ── Splitting ─────────────────────────────────────────────────────────────
40
41/**
42 * Split a raw NDJSON document string into an array of non-empty lines.
43 *
44 * Each returned line is a complete JSON object string that can be sent as an
45 * individual WebSocket message.  Blank lines are filtered out so the caller
46 * never accidentally sends an empty frame.
47 *
48 * @param {string} text  Raw NDJSON document (lines separated by "\n").
49 * @returns {string[]}   Array of non-empty JSON line strings.
50 */
51export function splitNdjsonLines(text) {
52  return String(text || "")
53    .split("\n")
54    .filter(l => l.trim());
55}
56
57// ── Authentication ────────────────────────────────────────────────────────
58
59/**
60 * Build a JSON-serialised authentication frame for the zimserver wire
61 * protocol.  The frame must be sent as the very first WebSocket message
62 * so the server can validate the session before accepting any payload data.
63 *
64 * Returns an empty string when `token` is falsy so callers can safely
65 * skip injecting the frame without an extra guard.
66 *
67 * @param {string|null|undefined} token  Bearer access token.
68 * @returns {string}  Serialised auth frame, or "" when no token is provided.
69 */
70export function buildAuthFrame(token) {
71  const t = (token || "").trim();
72  if (!t) return "";
73  return jsonWithTypeFirst({ type: "auth", action: "authenticate", token: t });
74}
75
76// ── Acknowledgement parsing ───────────────────────────────────────────────
77
78/**
79 * Parse zimserver acknowledgement lines and return a structured result.
80 *
81 * The server typically responds with one or more NDJSON lines such as:
82 *   {"type":"ack","action":"save","appType":"mindmap","status":"ok","saved":12}
83 *   {"type":"error","message":"..."}
84 *
85 * @param {string[]} ackLines  Lines returned by sendShellNdjson() or similar.
86 * @returns {{ status: "ok"|"error"|"unknown", saved: number, messages: string[] }}
87 */
88export function parseAckLines(ackLines) {
89  let status = "unknown";
90  let saved  = 0;
91  const messages = [];
92
93  for (const line of ackLines) {
94    try {
95      const obj = JSON.parse(line);
96      if (obj.type === "ack") {
97        status = obj.status === "ok" ? "ok" : "error";
98        if (typeof obj.saved === "number") saved = obj.saved;
99        if (obj.message) messages.push(obj.message);
100      } else if (obj.type === "error") {
101        status = "error";
102        if (obj.message) messages.push(obj.message);
103      }
104    } catch { /* skip unparseable lines */ }
105  }
106
107  return { status, saved, messages };
108}
109
110// ── Record normalisation ──────────────────────────────────────────────────
111
112/**
113 * Convert raw data from a WS/HTTP response into flat record objects suitable
114 * for publishing to the stream registry.  Handles single JSON objects, arrays,
115 * and NDJSON strings (newline-delimited JSON).
116 *
117 * @param {*} data
118 * @returns {object[]}
119 */
120export function recordsFromData(data) {
121  const tryJson = (text) => { try { return JSON.parse(text); } catch { return null; } };
122
123  if (typeof data === "string") {
124    // Try as single JSON first (covers plain object or array responses)
125    const single = tryJson(data);
126    if (single !== null) {
127      if (Array.isArray(single)) {
128        return single.map((r) => flattenRecord(r && typeof r === "object" ? r : { value: r }));
129      }
130      if (typeof single === "object") return [flattenRecord(single)];
131    }
132    // Fall back to NDJSON: parse each non-empty line independently
133    const records = [];
134    for (const line of data.split("\n")) {
135      const t = line.trim();
136      if (!t) continue;
137      const parsed = tryJson(t);
138      if (parsed && typeof parsed === "object") records.push(flattenRecord(parsed));
139    }
140    return records;
141  }
142
143  if (Array.isArray(data)) {
144    return data.map((r) => flattenRecord(r && typeof r === "object" ? r : { value: r }));
145  }
146  if (data && typeof data === "object") return [flattenRecord(data)];
147  return [];
148}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.