From 62196ffda4a18923968a04cd24437a80dd8e5204 Mon Sep 17 00:00:00 2001 From: bas080 Date: Tue, 14 Apr 2026 19:24:15 +0300 Subject: [PATCH 1/2] Add JSDoc to relevant function for docs --- parse.mjs | 80 ++++++++++++++++++++++++++++++++------------------ references.mjs | 63 +++++++++++++++++++++++++++++++++++---- stringify.mjs | 60 +++++++++++++++++++++++++++---------- 3 files changed, 153 insertions(+), 50 deletions(-) diff --git a/parse.mjs b/parse.mjs index 912aaa1..3cd3353 100644 --- a/parse.mjs +++ b/parse.mjs @@ -6,13 +6,14 @@ function flattenSchema (schema) { for (const item of schema) { if (typeof item === 'string') { - // leaf function obj[item] = true } else if (Array.isArray(item)) { const [name, children] = item + if (!Array.isArray(children)) { throw new Error(`Expected children array for namespace "${name}"`) } + obj[name] = flattenSchema(children) } else { throw new Error('Schema items must be strings or [name, children] arrays') @@ -38,7 +39,6 @@ const spy = (type, fn) => (...args) => { return value } -// Recursively resolve awaited values in a parsed tree const evaluate = spy('eval', (value, awaits = []) => { if (value === undefinedSentinel) return undefined @@ -47,49 +47,41 @@ const evaluate = spy('eval', (value, awaits = []) => { if (operator === 'await') { const [index] = rest - // No need to check index. It is closely tied to the program. - // if (typeof index !== 'number' || index < 0 || index >= awaitsResolved.length) { - // throw new Error(`Invalid await index: ${index}`); - // } return awaits[index] } if (operator === 'then') { const [v, onResolve, onReject] = rest - return evaluate(v, awaits).then(evaluate(onResolve, awaits), evaluate(onReject, awaits)) + return evaluate(v, awaits).then( + evaluate(onResolve, awaits), + evaluate(onReject, awaits) + ) } if (operator === 'call') { - // Step 1: evaluate the function itself let [fn, args] = rest fn = evaluate(fn, awaits) - // Step 2: evaluate each argument AFTER fn is ready for (let i = 0; i < args.length; i++) { args[i] = evaluate(args[i], awaits) } - // Step 3: call the function return fn(...args) } if (operator === 'quote') { const [quoted] = rest - return quoted // return as-is without evaluating + return quoted } - // fallback: evaluate each element - // re-uses the array again. - - for (let index = 0; index < value.length; index++) { - const item = value[index] - value[index] = evaluate(item, awaits) + for (let i = 0; i < value.length; i++) { + value[i] = evaluate(value[i], awaits) } + return value } if (isPlainObject(value)) { - // We muatate the object itself. No need to make a new one. for (const key of Object.keys(value)) { value[key] = evaluate(value[key], awaits) } @@ -99,23 +91,56 @@ const evaluate = spy('eval', (value, awaits = []) => { return value }) +/** + * Default deserializer for leaf nodes. + * + * @param {string} text + * @returns {any} + * @public + */ const defaultLeafDeserializer = (text) => JSON.parse(text) -export default (schemaArg, env, deserialize = defaultLeafDeserializer) => { +/** + * Creates a program parser for a given schema and environment. + * + * The parser: + * - resolves references into runtime functions + * - deserializes leaf nodes + * - collects and executes async awaits + * - evaluates AST-like JSON programs + * + * @param {Array} schemaArg + * @param {Object} env - runtime environment for refs + * @param {(text: string) => any} [deserialize=defaultLeafDeserializer] + * @returns {(program: string) => any|Promise} + * @public + */ +export default function Parse (schemaArg, env, deserialize = defaultLeafDeserializer) { const schema = flattenSchema(schemaArg) + /** + * Parses and executes a serialized program. + * + * @param {string} program - JSON encoded program + * @returns {any|Promise} + * @public + */ return function parse (program) { debug('program', program) const awaits = [] - // Creates the list of awaits that will resolve in order - // and also deserializes the leaves. + /** + * JSON reviver used during parsing. + * Converts encoded operators into runtime structures. + * + * @param {string} key + * @param {any} value + * @returns {any} + */ const reviver = spy('revive', (key, value) => { if (value === null) return value - if (!Array.isArray(value)) { - return value - } + if (!Array.isArray(value)) return value const [operator, ...rest] = value @@ -126,7 +151,6 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => { if (operator === 'await') { const [program] = rest - return ['await', awaits.push(program) - 1] } @@ -151,19 +175,17 @@ export default (schemaArg, env, deserialize = defaultLeafDeserializer) => { }) const parsed = JSON.parse(program, reviver) - debug('parsed', parsed) if (awaits.length) { debug('awaits', awaits) return (async function () { - for (let index = 0; index < awaits.length; index++) { - awaits[index] = await evaluate(awaits[index], awaits) + for (let i = 0; i < awaits.length; i++) { + awaits[i] = await evaluate(awaits[i], awaits) } debug('awaits(awaited)', awaits) - return evaluate(parsed, awaits) })() } diff --git a/references.mjs b/references.mjs index e6fd673..859a881 100644 --- a/references.mjs +++ b/references.mjs @@ -1,6 +1,19 @@ import { awaitSymbol, call, ref, then, referenceSymbol } from './symbol.mjs' +/** + * Creates a callable reference node bound to a path. + * Used to build a lazy/instrumented execution structure. + * + * @param {Array} path - Path representing the function location in schema. + * @returns {Function} Reference function with attached control methods (.then, .catch, toJSON). + */ function instrument (path) { + /** + * Creates a callable reference invocation. + * + * @param {...any} args - Arguments passed to the call. + * @returns {Function} New instrumented reference node. + */ function reference (...args) { const called = instrument(path) @@ -13,6 +26,13 @@ function instrument (path) { return called } + /** + * Internal helper for building promise-like continuation nodes. + * + * @param {Function|null} resolve + * @param {Function|null} reject + * @returns {Function} Instrumented continuation node. + */ function dotThen (resolve, reject) { const node = instrument(path) @@ -26,18 +46,32 @@ function instrument (path) { return node } + /** + * Registers rejection handler (promise-style). + * + * @param {Function} reject + * @returns {Function} + */ reference.catch = (reject) => { return dotThen(null, reject) } + /** + * Handles async chaining or awaiting logic. + * + * If resolve/reject contain a reference marker, it behaves like a .then chain. + * Otherwise it behaves like an await wrapper. + * + * @param {Function} resolve + * @param {Function} reject + * @returns {Function|any} + */ reference.then = (resolve, reject) => { - // That is how we know if it is an await or a .then call. if (resolve?.[referenceSymbol] || reject?.[referenceSymbol]) { return dotThen(resolve, reject) } const awaited = instrument(path) - // Prevent infinite recur delete awaited.then awaited.toJSON = () => ({ @@ -48,6 +82,11 @@ function instrument (path) { return resolve(awaited) } + /** + * JSON representation of the reference path node. + * + * @returns {{ref: symbol, path: Array}} + */ reference.toJSON = () => ({ [ref]: ref, path @@ -58,17 +97,29 @@ function instrument (path) { return reference } -export default function module (schema, parentPath = []) { +/** + * Builds a nested API structure from a schema definition. + * + * Schema supports: + * - string => leaf function node + * - [name, children] => namespace with nested schema + * + * @param {Array} schema + * @param {Array} [parentPath=[]] + * @returns {Object} Nested instrumented API object + * + * @throws {Error} If schema format is invalid + * @public + */ +export default function References (schema, parentPath = []) { return schema.reduce((acc, item) => { if (typeof item === 'string') { - // leaf function acc[item] = instrument([...parentPath, item]) } else if (Array.isArray(item)) { const [name, children] = item if (Array.isArray(children)) { - // recurse: children can be strings or [name, children] arrays - acc[name] = module(children, [...parentPath, name]) + acc[name] = References(children, [...parentPath, name]) } else { throw new Error(`Expected children array for namespace "${name}"`) } diff --git a/stringify.mjs b/stringify.mjs index d801bb2..4fd38fe 100644 --- a/stringify.mjs +++ b/stringify.mjs @@ -10,6 +10,7 @@ import { const debug = Debug.extend('stringify') const keywords = ['ref', 'call', 'quote', 'await', 'leaf'] + const isKeyword = (v) => keywords.includes(v) const isPlainObject = (value) => { @@ -18,26 +19,26 @@ const isPlainObject = (value) => { return proto === Object.prototype || proto === null } -// Recursively transform a program tree, encoding SendScript operators and leaf values function transformValue (value, leafSerializer) { debug(value) - if (value === null) { - return null - } + if (value === null) return null - // Normalize SendScript wrapper functions (ref, call, await) + // unwrap function wrappers (instrumented nodes) if (typeof value === 'function' && typeof value.toJSON === 'function') { return transformValue(value.toJSON(), leafSerializer) } - // Encode SendScript operators if (value && value[ref]) { return ['ref', ...value.path] } if (value && value[call]) { - return ['call', transformValue(value.ref, leafSerializer), transformValue(value.args, leafSerializer)] + return [ + 'call', + transformValue(value.ref, leafSerializer), + transformValue(value.args, leafSerializer) + ] } if (value && value[awaitSymbol]) { @@ -45,22 +46,27 @@ function transformValue (value, leafSerializer) { } if (value && value[then]) { - return ['then', transformValue(value.ref, leafSerializer), transformValue(value.resolve || null, leafSerializer), transformValue(value.reject || null, leafSerializer)] + return [ + 'then', + transformValue(value.ref, leafSerializer), + transformValue(value.resolve || null, leafSerializer), + transformValue(value.reject || null, leafSerializer) + ] } - // Handle arrays: quote keyword operators, transform other arrays recursively if (Array.isArray(value)) { const [operator, ...rest] = value if (isKeyword(operator)) { - // Quote reserved keyword strings to preserve them as data - return [['quote', operator], ...rest.map((item) => transformValue(item, leafSerializer))] + return [ + ['quote', operator], + ...rest.map((item) => transformValue(item, leafSerializer)) + ] } return value.map((item) => transformValue(item, leafSerializer)) } - // Recurse into plain objects if (isPlainObject(value)) { const result = {} @@ -71,21 +77,45 @@ function transformValue (value, leafSerializer) { return result } - // Encode non-JSON leaf values (Date, RegExp, BigInt, etc.) return ['leaf', leafSerializer(value)] } +/** + * Default strict serializer for leaf values. + * + * Rejects non-JSON-safe values. + * + * @param {any} x + * @returns {string} + * @public + */ function strictStringify (x) { const typeOf = typeof x if (typeOf === 'object' || typeOf === 'function' || x === undefined) { - throw new SendScriptSerializationError(`Cannot and should not attempt to serialize ${x}`) + throw new SendScriptSerializationError( + `Cannot and should not attempt to serialize ${x}` + ) } return JSON.stringify(x) } -export default function stringify (leafSerializer = strictStringify) { +/** + * Creates a stringify function for SendScript AST structures. + * + * @param {(value: any) => string} [leafSerializer=strictStringify] + * @returns {(program: any) => string} + * @public + */ +export default function Stringify (leafSerializer = strictStringify) { + /** + * Serializes a program into a JSON string representation. + * + * @param {any} program + * @returns {string} + * @public + */ function stringify (program) { return JSON.stringify(transformValue(program, leafSerializer)) } From 60666cc465d3d919c66f494e7a868dd4ae4f6e9c Mon Sep 17 00:00:00 2001 From: bas080 Date: Tue, 14 Apr 2026 19:25:10 +0300 Subject: [PATCH 2/2] Add generated from JSDoc reference section --- CONTRIBUTING.md | 4 +++- README.mz | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edab012..42ec247 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,7 +40,9 @@ Generate the README from the mz file. ```bash bash markatzea ./README.mz | tee ./README.md -npx markdown-toc -i README.md +npx documentation readme references.mjs parse.mjs stringify.mjs -s Reference --github --a public --markdown-toc false + +npx markdown-toc --maxdepth 3 -i README.md git add *.md ./example ``` diff --git a/README.mz b/README.mz index 1bed550..147a3bd 100644 --- a/README.mz +++ b/README.mz @@ -79,6 +79,10 @@ piqued your interest. SendScript leaves it up to you to choose HTTP, web-sockets or any other method of communication between servers and clients that best fits your needs. +## Reference + + + ## Socket example For this example we'll use [socket.io][socket.io].