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.