@2702rebels/wpidata 1.0.0

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.
Files changed (80) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +5 -0
  3. package/dist/abstractions.cjs +0 -0
  4. package/dist/abstractions.d.cts +246 -0
  5. package/dist/abstractions.d.cts.map +1 -0
  6. package/dist/abstractions.d.mts +246 -0
  7. package/dist/abstractions.d.mts.map +1 -0
  8. package/dist/abstractions.mjs +1 -0
  9. package/dist/formats/json.cjs +32 -0
  10. package/dist/formats/json.d.cts +14 -0
  11. package/dist/formats/json.d.cts.map +1 -0
  12. package/dist/formats/json.d.mts +14 -0
  13. package/dist/formats/json.d.mts.map +1 -0
  14. package/dist/formats/json.mjs +33 -0
  15. package/dist/formats/json.mjs.map +1 -0
  16. package/dist/formats/msgpack.cjs +30 -0
  17. package/dist/formats/msgpack.d.cts +14 -0
  18. package/dist/formats/msgpack.d.cts.map +1 -0
  19. package/dist/formats/msgpack.d.mts +14 -0
  20. package/dist/formats/msgpack.d.mts.map +1 -0
  21. package/dist/formats/msgpack.mjs +31 -0
  22. package/dist/formats/msgpack.mjs.map +1 -0
  23. package/dist/formats/protobuf.cjs +130 -0
  24. package/dist/formats/protobuf.d.cts +68 -0
  25. package/dist/formats/protobuf.d.cts.map +1 -0
  26. package/dist/formats/protobuf.d.mts +68 -0
  27. package/dist/formats/protobuf.d.mts.map +1 -0
  28. package/dist/formats/protobuf.mjs +128 -0
  29. package/dist/formats/protobuf.mjs.map +1 -0
  30. package/dist/formats/struct.cjs +593 -0
  31. package/dist/formats/struct.d.cts +134 -0
  32. package/dist/formats/struct.d.cts.map +1 -0
  33. package/dist/formats/struct.d.mts +134 -0
  34. package/dist/formats/struct.d.mts.map +1 -0
  35. package/dist/formats/struct.mjs +591 -0
  36. package/dist/formats/struct.mjs.map +1 -0
  37. package/dist/sink.cjs +360 -0
  38. package/dist/sink.d.cts +93 -0
  39. package/dist/sink.d.cts.map +1 -0
  40. package/dist/sink.d.mts +93 -0
  41. package/dist/sink.d.mts.map +1 -0
  42. package/dist/sink.mjs +361 -0
  43. package/dist/sink.mjs.map +1 -0
  44. package/dist/types/protobuf.cjs +0 -0
  45. package/dist/types/protobuf.d.cts +302 -0
  46. package/dist/types/protobuf.d.cts.map +1 -0
  47. package/dist/types/protobuf.d.mts +302 -0
  48. package/dist/types/protobuf.d.mts.map +1 -0
  49. package/dist/types/protobuf.mjs +1 -0
  50. package/dist/types/sendable.cjs +0 -0
  51. package/dist/types/sendable.d.cts +225 -0
  52. package/dist/types/sendable.d.cts.map +1 -0
  53. package/dist/types/sendable.d.mts +225 -0
  54. package/dist/types/sendable.d.mts.map +1 -0
  55. package/dist/types/sendable.mjs +1 -0
  56. package/dist/types/struct.cjs +0 -0
  57. package/dist/types/struct.d.cts +304 -0
  58. package/dist/types/struct.d.cts.map +1 -0
  59. package/dist/types/struct.d.mts +304 -0
  60. package/dist/types/struct.d.mts.map +1 -0
  61. package/dist/types/struct.mjs +1 -0
  62. package/dist/utils.cjs +140 -0
  63. package/dist/utils.d.cts +40 -0
  64. package/dist/utils.d.cts.map +1 -0
  65. package/dist/utils.d.mts +40 -0
  66. package/dist/utils.d.mts.map +1 -0
  67. package/dist/utils.mjs +135 -0
  68. package/dist/utils.mjs.map +1 -0
  69. package/package.json +51 -0
  70. package/src/abstractions.ts +308 -0
  71. package/src/formats/json.ts +53 -0
  72. package/src/formats/msgpack.ts +42 -0
  73. package/src/formats/protobuf.ts +213 -0
  74. package/src/formats/struct.test.ts +814 -0
  75. package/src/formats/struct.ts +992 -0
  76. package/src/sink.ts +611 -0
  77. package/src/types/protobuf.ts +334 -0
  78. package/src/types/sendable.ts +244 -0
  79. package/src/types/struct.ts +333 -0
  80. package/src/utils.ts +241 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"struct.mjs","names":["shift","n"],"sources":["../../src/formats/struct.ts"],"sourcesContent":["import { toDataView, toUint8Array } from \"../utils\";\n\nimport type { DataChannel, DataTransformer, DataTypeImpl, StructuredTypeDescriptor } from \"../abstractions\";\n\nconst error = (message: string) => {\n return new Error(message);\n};\n\nexport type StructFieldType =\n | \"ref\"\n | \"bool\"\n | \"char\"\n | \"int8\"\n | \"int16\"\n | \"int32\"\n | \"int64\"\n | \"uint8\"\n | \"uint16\"\n | \"uint32\"\n | \"uint64\"\n | \"float\"\n | \"double\";\n\n/** Describes individual field in {@link StructDescriptor}. */\nexport type StructFieldDescriptor = {\n /** Field identifier */\n identifier: string;\n /** Field value type */\n type: StructFieldType;\n /** Reference for complex (nested) type */\n typeRef?: StructDescriptor | string;\n /** Offset to the packed field data in bytes */\n offset: number;\n /** Size of the packed field data in bytes */\n size: number;\n /** Fixed array size */\n arraySize?: number;\n /** Bit width of the field data (bit-field only) */\n bitWidth?: number;\n /** Bit shift for the field data (bit-field only) */\n bitShift?: number;\n /** Enum specification */\n enum?: Map<number, string>;\n};\n\n/** Describes named struct type. */\nexport type StructDescriptor = {\n /** Struct type name */\n name: string;\n /** Struct fields in packed order */\n fields: ReadonlyArray<StructFieldDescriptor>;\n /** Total packed size in bytes */\n size: number;\n /** Missing dependencies */\n unresolved?: Set<string>;\n};\n\nconst utf8decoder = new TextDecoder(\"utf-8\", {\n // throw TypeError on invalid data instead of silent substitution\n fatal: true,\n});\n\nconst utf8encoder = new TextEncoder();\n\nexport type UnpackStructOptions = {\n /**\n * Indicates that integer numeric values with enum specification should be converted\n * to the corresponding enum string value if possible.\n */\n useEnum?: boolean;\n};\n\n/**\n * Unpacks serialized struct data into JSON object.\n *\n * Implementation of WPILiB packed struct serialization protocol\n * https://github.com/wpilibsuite/allwpilib/blob/main/wpiutil/doc/struct.adoc\n *\n * @param name struct type name\n * @param data serialized binary data\n * @param repository repository of available descriptors\n * @param options additional options\n */\nexport function unpack(\n name: string,\n data: DataView<ArrayBufferLike> | Uint8Array<ArrayBufferLike>,\n repository: StructRepository,\n options?: UnpackStructOptions\n) {\n const descriptor = repository.descriptors.get(name);\n if (descriptor == null) {\n throw error(`Failed to unpack struct data: missing '${name}' type definition`);\n }\n\n // descriptor exists but is not mapped yet due to unresolved dependencies\n if (descriptor.size === 0) {\n throw error(`Failed to unpack struct data: '${name}' type definition has unresolved dependencies`);\n }\n\n const result: Record<string, unknown> = {};\n unpackStruct(result, descriptor, toDataView(data), 0, options?.useEnum ? transformEnums : (_, v) => v);\n return result;\n}\n\n/**\n * Packs JSON object into serialized struct data.\n *\n * @param name struct type name\n * @param value JSON object to pack\n * @param repository repository of available descriptors\n * @returns ArrayBuffer containing serialized data\n */\nexport function pack(name: string, value: Record<string, unknown>, repository: StructRepository) {\n const descriptor = repository.descriptors.get(name);\n if (descriptor == null) {\n throw error(`Failed to pack struct data: missing '${name}' type definition`);\n }\n\n // descriptor exists but is not mapped yet due to unresolved dependencies\n if (descriptor.size === 0) {\n throw error(`Failed to pack struct data: '${name}' type definition has unresolved dependencies`);\n }\n\n const buffer = new ArrayBuffer(descriptor.size);\n const view = new DataView(buffer);\n\n packStruct(value, descriptor, view, 0, transformValue);\n return new Uint8Array(buffer);\n}\n\n/**\n * Transforms field values according to the field descriptor.\n *\n * Handles conversion of enum strings into their numeric representation.\n */\nfunction transformValue(field: StructFieldDescriptor, value: unknown) {\n if (field.enum != null && typeof value === \"string\") {\n for (const [key, v] of field.enum) {\n if (v === value) {\n return key;\n }\n }\n }\n\n return value;\n}\n\n/** Transforms numeric values to enum names for fields that support enums. */\nfunction transformEnums(field: StructFieldDescriptor, value: unknown) {\n if (field.enum != null && typeof value === \"number\") {\n const enumName = field.enum.get(value);\n if (enumName != null) {\n return enumName;\n }\n }\n\n return value;\n}\n\n/**\n * Unpacks data per descriptor specification and populates `sink` placeholder instance.\n *\n * @param sink target object to populate with parsed data\n * @param descriptor struct type descriptor\n * @param view source buffer view\n * @param byteOffset offset in bytes within `view`\n * @param transformer primitive field value transformer\n */\nfunction unpackStruct(\n sink: Record<string, unknown>,\n descriptor: StructDescriptor,\n view: DataView,\n byteOffset: number,\n transformer: (field: StructFieldDescriptor, value: unknown) => unknown\n) {\n for (const field of descriptor.fields) {\n if (field.type === \"ref\") {\n // nested structure\n if (field.typeRef == null || typeof field.typeRef !== \"object\") {\n throw error(`Failed to unpack struct data: field '${field.identifier}' references unresolved type`);\n }\n\n const result: Record<string, unknown> = {};\n unpackStruct(result, field.typeRef, view, byteOffset + field.offset, transformer);\n sink[field.identifier] = result;\n } else if (field.arraySize != null) {\n if (field.type === \"char\") {\n // array of chars is UTF-8 encoded string\n sink[field.identifier] = transformer(\n field,\n decodeStringValue(view, byteOffset + field.offset, field.arraySize)\n );\n } else {\n // array of booleans or numeric values\n const result: Array<unknown> = [];\n for (let i = 0; i < field.arraySize; ++i) {\n result.push(\n transformer(field, decodePrimitiveValue(field, view, byteOffset + field.offset + i * field.size))\n );\n }\n }\n } else if (field.bitWidth != null) {\n sink[field.identifier] = transformer(field, decodeBitFieldValue(field, view, byteOffset + field.offset));\n } else {\n sink[field.identifier] = transformer(field, decodePrimitiveValue(field, view, byteOffset + field.offset));\n }\n }\n}\n\n/**\n * Packs data per descriptor specification.\n *\n * @param source source object to pack\n * @param descriptor struct type descriptor\n * @param view target buffer view\n * @param byteOffset offset in bytes within `view`\n * @param transformer primitive field value transformer\n */\nfunction packStruct(\n source: Record<string, unknown>,\n descriptor: StructDescriptor,\n view: DataView,\n byteOffset: number,\n transformer: (field: StructFieldDescriptor, value: unknown) => unknown\n) {\n for (const field of descriptor.fields) {\n const value = source[field.identifier];\n\n if (field.type === \"ref\") {\n // nested structure\n if (field.typeRef == null || typeof field.typeRef !== \"object\") {\n throw error(`Failed to pack struct data: field '${field.identifier}' references unresolved type`);\n }\n\n packStruct((value ?? {}) as Record<string, unknown>, field.typeRef, view, byteOffset + field.offset, transformer);\n } else if (field.arraySize != null) {\n if (field.type === \"char\") {\n // array of chars is UTF-8 encoded string\n encodeStringValue(\n view,\n byteOffset + field.offset,\n field.arraySize,\n (transformer(field, value) ?? \"\") as string\n );\n } else {\n // array of booleans or numeric values\n for (let i = 0; i < field.arraySize; ++i) {\n encodePrimitiveValue(field, view, byteOffset + field.offset + i * field.size, transformer(field, value));\n }\n }\n } else if (field.bitWidth != null) {\n encodeBitFieldValue(field, view, byteOffset + field.offset, transformer(field, value));\n } else {\n encodePrimitiveValue(field, view, byteOffset + field.offset, transformer(field, value));\n }\n }\n}\n\n/**\n * Decodes a string field value.\n *\n * Assumes UTF-8 encoding, handles zero-termination, continuation bytes.\n */\nfunction decodeStringValue(view: DataView, byteOffset: number, byteLength: number) {\n // the array can can be zero terminated, we need to find last non-zero byte\n let length = byteLength;\n for (; length > 0; --length) {\n if (view.getUint8(byteOffset + length - 1) !== 0) {\n break;\n }\n }\n\n if (length === 0) {\n return \"\";\n }\n\n // UTF-8 continuation bytes (deal with garbage)\n if ((view.getUint8(byteOffset + length - 1) & 0x80) !== 0) {\n let start = length;\n for (; start > 0; --start) {\n if ((view.getUint8(byteOffset + start - 1) & 0x40) != 0) {\n break;\n }\n }\n\n if (start == 0) {\n return \"\";\n }\n\n start--;\n const b = view.getUint8(byteOffset + start);\n if ((b & 0xe0) === 0xc0) {\n if (start !== length - 2) {\n length = start;\n }\n } else if ((b & 0xf0) === 0xe0) {\n if (start !== length - 3) {\n length = start;\n }\n } else if ((b & 0xf8) === 0xf0) {\n if (start !== length - 4) {\n length = start;\n }\n }\n }\n\n // create restricted view\n return utf8decoder.decode(new DataView(view.buffer, view.byteOffset + byteOffset, length));\n}\n\n/**\n * Encodes a string field value.\n *\n * The implementation relies on the behavior of `Uint8Array` that is initialized to all zeros\n * and automatically clamps the size of the encoded data to the length of the array.\n */\nfunction encodeStringValue(view: DataView, byteOffset: number, byteLength: number, value: string) {\n utf8encoder.encodeInto(value, new Uint8Array(view.buffer, view.byteOffset + byteOffset, byteLength));\n}\n\n/**\n * Decodes a primitive field value.\n *\n * † Javascript limits integer types to 53-bit representation.\n * Decoding 64-bit integers may result in loss of precision as values\n * that do not fit within the safe integer limit will be represented by\n * floating-point numbers with double precision.\n *\n * ‡ Decoding a character value only makes sense for reading fields that\n * consist of exactly one character, where UTF-8 is essentially ASCII.\n * In practice UTF-8 encoded strings use multiple bytes to represent\n * non-ASCII characters and must be handled in a special way when array\n * of chars is decoded. @see {decodeStringValue}.\n */\nfunction decodePrimitiveValue(field: StructFieldDescriptor, view: DataView, byteOffset: number) {\n switch (field.type) {\n case \"bool\":\n return view.getUint8(byteOffset) !== 0;\n case \"char\":\n return String.fromCharCode(view.getUint8(byteOffset)); // ‡\n case \"int8\":\n return view.getInt8(byteOffset);\n case \"int16\":\n return view.getInt16(byteOffset, true);\n case \"int32\":\n return view.getInt32(byteOffset, true);\n case \"int64\":\n return Number(view.getBigInt64(byteOffset, true)); // †\n case \"uint8\":\n return view.getUint8(byteOffset);\n case \"uint16\":\n return view.getUint16(byteOffset, true);\n case \"uint32\":\n return view.getUint32(byteOffset, true);\n case \"uint64\":\n return Number(view.getBigUint64(byteOffset, true)); // †\n case \"float\":\n return view.getFloat32(byteOffset, true);\n case \"double\":\n return view.getFloat64(byteOffset, true);\n }\n}\n\n/**\n * Encodes a primitive field value.\n */\nfunction encodePrimitiveValue(field: StructFieldDescriptor, view: DataView, byteOffset: number, value: unknown) {\n // convert to numeric representation\n const v =\n value == null ? 0 : typeof value === \"string\" ? (value.length === 0 ? 0 : value.charCodeAt(0)) : Number(value);\n\n switch (field.type) {\n case \"bool\":\n view.setUint8(byteOffset, v);\n break;\n case \"char\":\n view.setUint8(byteOffset, v);\n break;\n case \"int8\":\n view.setInt8(byteOffset, v);\n break;\n case \"int16\":\n view.setInt16(byteOffset, v, true);\n break;\n case \"int32\":\n view.setInt32(byteOffset, v, true);\n break;\n case \"int64\":\n view.setBigInt64(byteOffset, BigInt(v), true);\n break;\n case \"uint8\":\n view.setUint8(byteOffset, v);\n break;\n case \"uint16\":\n view.setUint16(byteOffset, v, true);\n break;\n case \"uint32\":\n view.setUint32(byteOffset, v, true);\n break;\n case \"uint64\":\n view.setBigUint64(byteOffset, BigInt(v), true);\n break;\n case \"float\":\n view.setFloat32(byteOffset, v, true);\n break;\n case \"double\":\n view.setFloat64(byteOffset, v, true);\n break;\n }\n}\n\n/**\n * Decodes a bit-field integer value.\n */\nfunction decodeBitFieldValue(field: StructFieldDescriptor, view: DataView, byteOffset: number) {\n const width = field.bitWidth!;\n const shift = field.bitShift!;\n\n if (field.size === 8) {\n if (width <= 32) {\n // we can fit in 32-bit integer, use hi/lo 32-bit blocks to reconstruct the value\n // 64-bit is stored in LE, so high bits are in the block at a higher address\n const h32 = view.getUint32(byteOffset + 4, true);\n const l32 = view.getUint32(byteOffset, true);\n\n // example: width = 13, shift = 22\n // ........ ........ ........ .....xxx | xxxxxxxx xx...... ........ ........\n // +-------------- h32 --------------+ +-------------- l32 --------------+\n // unsigned\n // l32 >>> shift = 00000000 00000000 000000xx xxxxxxxx\n // h32 << (32 - shift) = ........ ........ ...xxx00 00000000\n // | = ........ ........ 000xxxxx xxxxxxxx\n // & mask = 00000000 00000000 000xxxxx xxxxxxxx\n const v = (shift >= 32 ? h32 >>> (shift - 32) : (l32 >>> shift) | (h32 << (32 - shift))) & bitmask(width);\n return field.type === \"int64\" ? (v << (32 - width)) >> (32 - width) : v;\n } else {\n // we have to resort to unsigned case only due to Javascript limitations\n // that prevent us from performing bit manipulations on 64-bit integers\n const data = view.getBigUint64(byteOffset, true);\n return Number((data >> BigInt(shift)) & (2n ** BigInt(width) - 1n));\n }\n } else {\n // read the block containing the field\n const data =\n field.size === 4\n ? view.getUint32(byteOffset, true)\n : field.size === 2\n ? view.getUint16(byteOffset, true)\n : view.getUint8(byteOffset);\n\n // for unsigned (and boolean) we can just shift and mask\n // note the use of `>>>` unsigned shift operator here\n switch (field.type) {\n case \"bool\":\n case \"uint8\":\n case \"uint16\":\n case \"uint32\": {\n const v = (data >>> shift) & bitmask(width);\n return field.type === \"bool\" ? v !== 0 : v;\n }\n }\n\n // signed integer situation, first shift left to clear high bits and set the sign bit,\n // then shift right, which also clears low bits so masking is not necessary\n // note the use of `<<` and `>>` shift operators here\n // these are overloaded for 32-bit integers\n return (data << (32 - shift - width)) >> (32 - width);\n }\n}\n\n/**\n * Encodes a bit-field integer value.\n */\nfunction encodeBitFieldValue(field: StructFieldDescriptor, view: DataView, byteOffset: number, value: unknown) {\n const width = field.bitWidth!;\n const shift = field.bitShift!;\n\n // convert to numeric representation\n const n = value == null ? 0 : Number(value);\n const overlay = (n: number, v: number, mask: number, shift: number) => (v & ~(mask << shift)) | ((n & mask) << shift);\n\n if (field.size === 8) {\n if (width <= 32) {\n // we can fit in 32-bit integer, use hi/lo 32-bit blocks to overlay the value\n const mask = bitmask(width);\n if (shift >= 32) {\n view.setUint32(byteOffset + 4, overlay(n, view.getUint32(byteOffset + 4, true), mask, shift - 32), true);\n } else if (shift + width <= 32) {\n view.setUint32(byteOffset, overlay(n, view.getUint32(byteOffset, true), mask, shift), true);\n } else {\n // 64-bit is stored in LE, so high bits are in the block at a higher address\n const h32 = view.getUint32(byteOffset + 4, true);\n const l32 = view.getUint32(byteOffset, true);\n const nm = n & mask;\n view.setUint32(byteOffset + 4, (h32 & ~bitmask(shift + width - 32)) | (nm >>> (32 - shift)), true);\n view.setUint32(byteOffset, overlay(nm, l32, bitmask(32 - shift), shift), true);\n }\n } else {\n const data = view.getBigUint64(byteOffset, true);\n const mask = 2n ** BigInt(width) - 1n;\n view.setBigUint64(byteOffset, (data & ~(mask << BigInt(shift))) | ((BigInt(n) & mask) << BigInt(shift)), true);\n }\n } else {\n const mask = bitmask(width);\n switch (field.size) {\n case 4:\n view.setUint32(byteOffset, overlay(n, view.getUint32(byteOffset, true), mask, shift), true);\n break;\n case 2:\n view.setUint16(byteOffset, overlay(n, view.getUint16(byteOffset, true), mask, shift), true);\n break;\n case 1:\n view.setUint8(byteOffset, overlay(n, view.getUint8(byteOffset), mask, shift));\n break;\n }\n }\n}\n\n/** Constructs bitmask of the specified `width` (max 32 bits). */\nconst bitmask = (width: number) => -1 >>> (32 - width);\n\n/** Bytes size of the value type. */\nconst fieldTypeByteSize = {\n bool: 1,\n char: 1,\n int8: 1,\n int16: 2,\n int32: 4,\n int64: 8,\n uint8: 1,\n uint16: 2,\n uint32: 4,\n uint64: 8,\n float: 4,\n double: 8,\n} as const;\n\nconst getFieldType = (type: string): StructFieldType => {\n switch (type) {\n case \"bool\":\n case \"char\":\n case \"int8\":\n case \"int16\":\n case \"int32\":\n case \"int64\":\n case \"uint8\":\n case \"uint16\":\n case \"uint32\":\n case \"uint64\":\n case \"float\":\n case \"double\":\n return type;\n case \"float32\":\n return \"float\";\n case \"float64\":\n return \"double\";\n default:\n return \"ref\";\n }\n};\n\n/** Repository of struct descriptors. */\nexport class StructRepository {\n private readonly unresolved: Array<StructDescriptor> = [];\n\n /** Descriptors in the repository. */\n public readonly descriptors = new Map<string, StructDescriptor>();\n\n /**\n * Computes field bte offsets and returns total packed size in bytes.\n *\n * This method assumes that all fields have resolved external dependencies\n * and will compute byte offsets and bit shifts for bit-field packed fields.\n */\n private static computeFieldOffsets(fields: ReadonlyArray<StructFieldDescriptor>) {\n let offset = 0; // offset in bytes\n let bitBlock = 0; // size in bytes of the current bit-field block\n let bitAvail = 0; // available bits in the current bit-field\n\n for (const field of fields) {\n if (field.bitWidth != null) {\n // current bit-field must be of the same size (except for booleans)\n // and should have sufficient bits remaining\n if ((bitBlock !== field.size && field.type !== \"bool\") || field.bitWidth > bitAvail) {\n // terminate current bit-field\n if (bitBlock > 0) {\n offset += bitBlock;\n }\n\n // start new bit-field block\n bitBlock = field.size;\n bitAvail = bitBlock << 3;\n }\n\n // booleans are \"spliced\" onto current integer block size\n if (field.type === \"bool\") {\n field.size = bitBlock;\n }\n\n field.offset = offset;\n field.bitShift = (bitBlock << 3) - bitAvail;\n bitAvail -= field.bitWidth;\n } else {\n // terminate current bit-field\n if (bitBlock > 0) {\n offset += bitBlock;\n\n // reset bit-field block\n bitBlock = 0;\n bitAvail = 0;\n }\n\n field.offset = offset;\n offset += field.size * (field.arraySize ?? 1);\n }\n }\n\n // account for the terminal bit-field\n return offset + bitBlock;\n }\n\n /**\n * Attempts to finalize any unresolved descriptors with the recently resolved one.\n */\n private resolve(descriptor: StructDescriptor) {\n const resolved: Array<StructDescriptor> = [];\n for (let i = this.unresolved.length - 1; i >= 0; --i) {\n const d = this.unresolved[i]!;\n if (d.unresolved?.has(descriptor.name)) {\n d.unresolved?.delete(descriptor.name);\n d.fields.forEach((_) => {\n if (_.typeRef === descriptor.name) {\n _.typeRef = descriptor;\n _.size = descriptor.size;\n }\n });\n\n // no more unresolved references, we can finalize this descriptor\n if (d.unresolved.size === 0) {\n d.unresolved = undefined;\n d.size = StructRepository.computeFieldOffsets(d.fields);\n this.unresolved.splice(i, 1);\n resolved.push(d);\n }\n }\n }\n\n // resolve recursively\n for (const d of resolved) {\n this.resolve(d);\n }\n }\n\n /**\n * Determines whether type can be transformed, indicating that\n * the parsed type descriptor is present.\n *\n * @param name struct type name\n */\n public canTransform(name: string) {\n const d = this.descriptors.get(name);\n return d != null && d.size > 0;\n }\n\n /**\n * Gets the struct type serialized size in bytes.\n *\n * @param name struct type name\n */\n public getSize(name: string) {\n const size = this.descriptors.get(name)?.size;\n if (size == null || size == 0) {\n throw new Error(`Descriptor for type '${name}' does not exist or is not fully defined`);\n }\n\n return size;\n }\n\n /**\n * Unpacks serialized struct data into JSON object.\n *\n * @param name struct type name\n * @param data serialized binary data\n * @param options additional options\n */\n public unpack(\n name: string,\n data: DataView<ArrayBufferLike> | Uint8Array<ArrayBufferLike>,\n options?: UnpackStructOptions\n ) {\n return unpack(name, data, this, options);\n }\n\n /**\n * Packs JSON object into serialized struct data.\n *\n * @param name struct type name\n * @param value JSON object to pack\n */\n public pack(name: string, value: Record<string, unknown>) {\n return pack(name, value, this);\n }\n\n /**\n * Parses struct schema and adds the resulting descriptor to the repository.\n *\n * The descriptor may not be fully processed if it references other structs that\n * we have not seen yet. Such pending descriptors will be processed automatically,\n * once corresponding structs have been added. This code checks for circular\n * dependencies and will fail when one is detected.\n *\n * @param name struct type name\n * @param data struct schema in UTF-8 encoded binary representation\n * @returns parsed descriptor or `null` if the operation failed\n */\n public add(name: string, data: DataView<ArrayBufferLike> | Uint8Array<ArrayBufferLike>): StructDescriptor | null {\n let decoded: string | undefined;\n try {\n decoded = utf8decoder.decode(data);\n } catch (exception) {\n throw error(\n exception instanceof TypeError\n ? `Failed to parse schema: ${exception.message}`\n : `Failed to parse schema: unknown error`\n );\n }\n\n const fields: Array<StructFieldDescriptor> = [];\n const tokens = decoded\n .split(\";\")\n .map((_) => _.trim())\n .filter((_) => _.length > 0);\n\n const unresolved = new Set<string>();\n for (const token of tokens) {\n // regular expression to parse individual declaration specification\n // returns the following named capture groups:\n // - `enum` -- entire body of non-empty enum specification\n // - `type` -- type name\n // - `id` -- identifier name\n // - `array` -- if present length of the array\n // - `bits` -- if present bit-field width\n //\n // if present `size` and `bits` are mutually exclusive;\n // no attempt is made to allow `enum` specification only for integer data types\n const re =\n /^(?:(?:enum)?\\s*(?:{\\s*}|{(?<enum>(?:\\s*\\w\\s*=\\s*-?\\d+\\s*)(?:,\\s*\\w\\s*=\\s*-?\\d+\\s*)*),?\\s*}))?\\s*(?<type>\\w+)\\s+(?<id>\\w+)\\s*(?:(?:\\[\\s*(?<array>\\d+)\\s*\\])|:\\s*(?<bits>[1-9]\\d?))?$/i;\n\n const m = re.exec(token);\n if (m == null || m.groups == null) {\n throw error(`Failed to parse schema: invalid declaration '${token}'`);\n }\n\n const id = m.groups[\"id\"]!;\n const typeRaw = m.groups[\"type\"]!;\n\n // check for duplicates\n if (fields.some((_) => _.identifier === id)) {\n throw error(`Failed to parse schema: duplicate '${id}' field declaration`);\n }\n\n const field: StructFieldDescriptor = {\n identifier: id,\n type: getFieldType(typeRaw),\n offset: -1,\n size: 0,\n };\n\n if (field.type === \"ref\") {\n field.typeRef = this.descriptors.get(typeRaw);\n if (field.typeRef == null) {\n field.typeRef = typeRaw;\n unresolved.add(typeRaw);\n } else if (field.typeRef.size === 0) {\n throw error(\n `Failed to parse schema: circular dependency detected between '${name}' and '${field.typeRef.name}'`\n );\n } else {\n field.size = field.typeRef.size;\n }\n } else {\n field.size = fieldTypeByteSize[field.type];\n }\n\n // parse and validate bit-field specification\n const bitWidthRaw = m.groups[\"bits\"];\n if (bitWidthRaw != null) {\n field.bitWidth = parseInt(bitWidthRaw, 10);\n if (Number.isNaN(field.bitWidth)) {\n throw error(`Failed to parse schema: non-numeric bit-field width in '${id}' field declaration`);\n }\n\n switch (field.type) {\n case \"bool\":\n if (field.bitWidth !== 1) {\n throw error(`Failed to parse schema: invalid boolean bit-field width '${id}' field declaration`);\n }\n break;\n case \"int8\":\n case \"int16\":\n case \"int32\":\n case \"int64\":\n case \"uint8\":\n case \"uint16\":\n case \"uint32\":\n case \"uint64\":\n if (field.bitWidth < 1 || field.bitWidth > fieldTypeByteSize[field.type] << 3) {\n throw error(`Failed to parse schema: invalid integer bit-field width '${id}' field declaration`);\n }\n break;\n default:\n throw error(`Failed to parse schema: bit-field in non-integer/boolean '${id}' field declaration`);\n }\n }\n\n // parse and validate array size specification\n const arraySizeRaw = m.groups[\"array\"];\n if (arraySizeRaw != null) {\n field.arraySize = parseInt(arraySizeRaw, 10);\n if (Number.isNaN(field.arraySize) || field.arraySize <= 0) {\n throw error(`Failed to parse schema: invalid array size in '${id}' field declaration`);\n }\n }\n\n const enumBodyRaw = m.groups[\"enum\"];\n if (enumBodyRaw) {\n // enum is only allowed for integer declarations\n switch (field.type) {\n case \"int8\":\n case \"int16\":\n case \"int32\":\n case \"int64\":\n case \"uint8\":\n case \"uint16\":\n case \"uint32\":\n case \"uint64\":\n break;\n default:\n throw error(`Failed to parse schema: enum declaration in non-integer '${id}' field declaration`);\n }\n\n // parse enum values\n field.enum = new Map();\n for (const tuple of enumBodyRaw.split(\",\")) {\n const [enumName, valueRaw] = tuple.trim().split(\"=\", 2);\n const enumValue = parseInt(valueRaw!.trim(), 10);\n if (Number.isNaN(enumValue)) {\n throw error(\n `Failed to parse schema: enum declaration contains non-integer value '${valueRaw}' in '${id}' field declaration`\n );\n }\n field.enum.set(enumValue, enumName!);\n }\n }\n\n fields.push(field);\n }\n\n // if this descriptor has external dependencies that we have not observed yet,\n // we cannot map its fields and its packed size is set to zero for now\n const descriptor = {\n name,\n fields,\n size: unresolved.size > 0 ? 0 : StructRepository.computeFieldOffsets(fields),\n unresolved: unresolved.size > 0 ? unresolved : undefined,\n };\n\n this.descriptors.set(name, descriptor);\n\n // if this descriptor is final, see if it resolves any unresolved ones\n if (descriptor.size > 0) {\n this.resolve(descriptor);\n } else {\n this.unresolved.push(descriptor);\n }\n\n return descriptor;\n }\n}\n\n/** Implements {@link DataTransformer} interface for the `struct` serialization protocol. */\nexport class StructDataTransformer implements DataTransformer {\n private readonly repo = new StructRepository();\n\n public inspect(\n source: string,\n name: string,\n type: string,\n metadata?: string | Record<string, unknown>\n ): DataChannel | string | undefined {\n if (name.startsWith(\"/.schema/struct:\")) {\n if (type !== \"structschema\") {\n throw new Error(`Unexpected type '${type}' for struct schema entry`);\n }\n\n return name.substring(16);\n }\n\n if (type.startsWith(\"struct:\")) {\n // strip `[]` array suffix from the type name\n const isArrayType = type.endsWith(\"[]\");\n return {\n source,\n id: name,\n dataType: \"json\",\n publishedDataType: type,\n transformer: this,\n structuredType: {\n name: isArrayType ? type.slice(7, -2) : type.slice(7),\n format: \"struct\",\n isArray: isArrayType,\n },\n metadata,\n };\n }\n\n return undefined;\n }\n\n public schema(typeName: string, value: unknown): void {\n this.repo.add(typeName, toUint8Array(value));\n }\n\n public deserialize(value: unknown, type?: StructuredTypeDescriptor): DataTypeImpl | undefined {\n if (type == null) {\n throw new Error(\n `Transformation requires type to be specified. This situation should not be possible if the transformer is wired correctly.`\n );\n }\n\n if (this.repo.canTransform(type.name)) {\n const buffer = toUint8Array(value);\n\n if (type.isArray) {\n // special case for a top-level array: buffer contains consecutive fixed-size items\n const result: Array<Record<string, unknown>> = [];\n const itemSize = this.repo.getSize(type.name);\n let byteOffset = 0;\n\n while (byteOffset + itemSize <= buffer.byteLength) {\n result.push(\n this.repo.unpack(type.name, new DataView(buffer.buffer, buffer.byteOffset + byteOffset, itemSize), {\n useEnum: true,\n })\n );\n byteOffset += itemSize;\n }\n\n return result;\n }\n\n return this.repo.unpack(type.name, buffer, { useEnum: true });\n }\n\n return undefined;\n }\n\n public serialize(value: unknown, type?: StructuredTypeDescriptor): Uint8Array {\n if (type == null) {\n throw new Error(\n `Transformation requires type to be specified. This situation should not be possible if the transformer is wired correctly.`\n );\n }\n\n if (value == null || typeof value !== \"object\") {\n throw new Error(\"Only JSON objects can be serialized\");\n }\n\n if (this.repo.canTransform(type.name)) {\n if (Array.isArray(value)) {\n // special case for a top-level array: buffer contains consecutive fixed-size items\n const itemSize = this.repo.getSize(type.name);\n const result = new Uint8Array(itemSize * value.length);\n\n for (let i = 0; i < value.length; ++i) {\n const part = this.repo.pack(type.name, value[i] as Record<string, unknown>);\n result.set(part, i * itemSize);\n }\n\n return result;\n }\n\n return this.repo.pack(type.name, value as Record<string, unknown>);\n }\n\n throw new Error(`Struct serialization is not supported for '${type.name}'`);\n }\n\n public canTransform(type: string): boolean {\n return this.repo.canTransform(type);\n }\n}\n"],"mappings":"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"}
package/dist/sink.cjs ADDED
@@ -0,0 +1,360 @@
1
+ const require_utils = require('./utils.cjs');
2
+ const require_formats_json = require('./formats/json.cjs');
3
+ const require_formats_msgpack = require('./formats/msgpack.cjs');
4
+ const require_formats_protobuf = require('./formats/protobuf.cjs');
5
+ const require_formats_struct = require('./formats/struct.cjs');
6
+
7
+ //#region src/sink.ts
8
+ /** Converts raw data type name to {@link DataType}. */
9
+ function getDataType(v) {
10
+ switch (v.toLocaleLowerCase()) {
11
+ case "boolean": return "boolean";
12
+ case "string": return "string";
13
+ case "double":
14
+ case "int":
15
+ case "float": return "number";
16
+ case "boolean[]": return "booleanArray";
17
+ case "string[]": return "stringArray";
18
+ case "double[]":
19
+ case "int[]":
20
+ case "float[]": return "numberArray";
21
+ case "json":
22
+ case "msgpack": return "json";
23
+ default: return "binary";
24
+ }
25
+ }
26
+ /** Gets the raw data type of the value to publish. */
27
+ function getRawType(v) {
28
+ if (Array.isArray(v)) {
29
+ if (v.length > 0) {
30
+ const itemType = getRawType(v[0]);
31
+ return itemType ? `${itemType}[]` : void 0;
32
+ }
33
+ } else switch (typeof v) {
34
+ case "boolean": return "boolean";
35
+ case "string": return "string";
36
+ case "number": return "double";
37
+ case "object": return "json";
38
+ }
39
+ }
40
+ /** Enqueues data into the composite channel at the specified path. */
41
+ function enqueueDataWithPath(channel, timestamp, data, path) {
42
+ channel.records ??= [];
43
+ const [record, recordIndex] = require_utils.getTimestampedRecord(channel.records, timestamp);
44
+ let index = recordIndex;
45
+ if (record != null) if (record.timestamp === timestamp) require_utils.setValueByPath(record.value, path, data);
46
+ else index = require_utils.addTimestampedRecord(channel.records, timestamp, require_utils.setValueByPath(structuredClone(record.value), path, data));
47
+ else index = require_utils.addTimestampedRecord(channel.records, timestamp, require_utils.setValueByPath({}, path, data));
48
+ for (let i = index + 1; i < channel.records.length; ++i) require_utils.setValueByPath(channel.records[i].value, path, data, true);
49
+ }
50
+ /** Enqueues data into the channel. */
51
+ function enqueueData(channel, timestamp, data, dlq) {
52
+ if (dlq) {
53
+ channel.dlq ??= [];
54
+ require_utils.addTimestampedRecord(channel.dlq, timestamp, require_utils.toUint8Array(data));
55
+ } else if (channel.composite != null) {
56
+ const path = channel.composite.channels.get(channel.id);
57
+ if (path == null) throw new Error(`Invariant violation: ${channel.id} must be a sub-channel of ${channel.composite.id}`);
58
+ enqueueDataWithPath(channel.composite, timestamp, data, path);
59
+ } else {
60
+ channel.records ??= [];
61
+ require_utils.addTimestampedRecord(channel.records, timestamp, data);
62
+ }
63
+ }
64
+ /** Converts channel into a sub-channel of a composite channel. */
65
+ function convertToSubchannel(channel, composite) {
66
+ const path = channel.id.substring(composite.id.length + 1).split(/\//).map((_) => _.trim()).filter((_) => _.length > 0);
67
+ channel.composite = composite;
68
+ composite.channels.set(channel.id, path);
69
+ }
70
+ /** Compares two arrays for value equality. */
71
+ function arraysEquals(a, b) {
72
+ if (a === b) return true;
73
+ if (a == null || b == null) return false;
74
+ if (a.length !== b.length) return false;
75
+ for (let i = 0; i < a.length; ++i) if (a[i] !== b[i]) return false;
76
+ return true;
77
+ }
78
+ /** Default console logger for error messages. */
79
+ function logError(message, error) {
80
+ console.error(`💣 [DataSink] ${message}`, error);
81
+ }
82
+ /** Default {@link DataTransformer} used by {@link DataSink} that can be reused across instances. */
83
+ const DefaultDataSinkTransformers = [new require_formats_json.JsonDataTransformer(), new require_formats_msgpack.MsgpackDataTransformer()];
84
+ /** A data sink in the data pipeline with support for protocol transformers, data retention, etc. */
85
+ var DataSink = class DataSink {
86
+ channels;
87
+ composites;
88
+ schemas;
89
+ transformers;
90
+ retention;
91
+ disableCompositeChannels;
92
+ onDataChannelAdded;
93
+ onDataChannelRemoved;
94
+ logger;
95
+ timestamp;
96
+ structTransformer = new require_formats_struct.StructDataTransformer();
97
+ protobufTransformer = new require_formats_protobuf.ProtobufDataTransformer();
98
+ constructor(options) {
99
+ this.channels = /* @__PURE__ */ new Map();
100
+ this.composites = /* @__PURE__ */ new Map();
101
+ this.schemas = /* @__PURE__ */ new Map();
102
+ this.retention = options?.retention;
103
+ this.onDataChannelAdded = options?.onDataChannelAdded;
104
+ this.onDataChannelRemoved = options?.onDataChannelRemoved;
105
+ this.disableCompositeChannels = options?.disableCompositeChannels ?? false;
106
+ this.logger = options?.logger ?? logError;
107
+ this.timestamp = 0;
108
+ this.transformers = [
109
+ ...DefaultDataSinkTransformers,
110
+ this.structTransformer,
111
+ this.protobufTransformer
112
+ ];
113
+ if (options?.transformers) this.transformers.push(...options.transformers);
114
+ }
115
+ /** Constructs channel identifier. */
116
+ static createId(source, name) {
117
+ return `${source}:${name}`;
118
+ }
119
+ /** Returns parent composite data channel if one exists. */
120
+ getCompositeParent(id) {
121
+ for (const composite of this.composites.values()) if (id.startsWith(`${composite.id}/`)) return composite;
122
+ }
123
+ /** Registers new data channel. */
124
+ registerChannel(channel, publish) {
125
+ const id = DataSink.createId(channel.source, channel.id);
126
+ this.channels.set(id, channel);
127
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : void 0;
128
+ let silent = false;
129
+ if (!this.disableCompositeChannels) {
130
+ const composite = this.getCompositeParent(channel.id);
131
+ if (composite != null) {
132
+ convertToSubchannel(channel, composite);
133
+ silent = true;
134
+ }
135
+ }
136
+ if (!silent && this.onDataChannelAdded != null) this.onDataChannelAdded(channel);
137
+ }
138
+ /** Gets sub-channel by its path. */
139
+ getSubChannel(channel, path) {
140
+ for (const [id, p] of channel.channels) if (arraysEquals(path, p)) return this.channels.get(id);
141
+ }
142
+ /** Creates channel publisher. */
143
+ createChannelPublisher(channel, publish) {
144
+ return (value, path, options) => {
145
+ if (path != null && channel.dataType === "composite") {
146
+ const subchannel = this.getSubChannel(channel, path);
147
+ if (subchannel) {
148
+ if (subchannel.transformer != null) value = subchannel.transformer.serialize(value, channel.structuredType);
149
+ publish(subchannel.id, subchannel.publishedDataType, value);
150
+ } else {
151
+ const topic = `${channel.id}/${path.join("/")}`;
152
+ let dataType = getRawType(value);
153
+ if (options?.structuredType) switch (options.structuredType.format) {
154
+ case "composite": return;
155
+ case "struct":
156
+ value = this.structTransformer.serialize(value, options.structuredType);
157
+ dataType = `struct:${options.structuredType.name}`;
158
+ break;
159
+ case "protobuf":
160
+ value = this.protobufTransformer.serialize(value, options.structuredType);
161
+ dataType = `proto:${options.structuredType.name}`;
162
+ break;
163
+ }
164
+ if (dataType) publish(topic, dataType, value);
165
+ }
166
+ } else {
167
+ if (channel.transformer != null) value = channel.transformer.serialize(value, channel.structuredType);
168
+ publish(channel.id, channel.publishedDataType, value);
169
+ }
170
+ };
171
+ }
172
+ /** Returns most recent timestamp in microseconds observed by this instance. */
173
+ get recentTimestamp() {
174
+ return this.timestamp;
175
+ }
176
+ /**
177
+ * Gets a channel descriptor.
178
+ *
179
+ * @param source source, e.g. `nt` or `wpilog`
180
+ * @param name channel name
181
+ */
182
+ get(source, name) {
183
+ const channel = this.channels.get(DataSink.createId(source, name));
184
+ return channel && channel.composite == null ? channel : void 0;
185
+ }
186
+ /**
187
+ * Adds a channel descriptor.
188
+ *
189
+ * @param source source, e.g. `nt` or `wpilog`
190
+ * @param name channel name
191
+ * @param type channel type
192
+ * @param properties channel properties
193
+ * @param publish channel publisher
194
+ */
195
+ add(source, name, type, properties, publish) {
196
+ let metadata;
197
+ if (properties != null) {
198
+ if (typeof properties === "string") try {
199
+ metadata = JSON.parse(properties);
200
+ } catch {
201
+ metadata = properties;
202
+ }
203
+ else if (typeof properties === "object") {
204
+ const { persistent, retained, cached, ...other } = properties;
205
+ if (Object.keys(other).length > 0) metadata = other;
206
+ }
207
+ }
208
+ const dataType = getDataType(type);
209
+ if (!this.disableCompositeChannels && name.endsWith("/.type")) {
210
+ const composite = this.getCompositeParent(name);
211
+ if (composite) {
212
+ const channel = {
213
+ source,
214
+ id: name,
215
+ dataType,
216
+ publishedDataType: type,
217
+ metadata
218
+ };
219
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : void 0;
220
+ const id = DataSink.createId(channel.source, channel.id);
221
+ this.channels.set(id, channel);
222
+ convertToSubchannel(channel, composite);
223
+ } else {
224
+ const channel = {
225
+ source,
226
+ id: name.slice(0, -6),
227
+ dataType: "composite",
228
+ publishedDataType: "",
229
+ metadata,
230
+ channels: /* @__PURE__ */ new Map()
231
+ };
232
+ channel.publish = publish ? this.createChannelPublisher(channel, publish) : void 0;
233
+ const prefix = `${channel.id}/`;
234
+ this.channels.forEach((value) => {
235
+ if (value.id.startsWith(prefix)) {
236
+ convertToSubchannel(value, channel);
237
+ if (this.onDataChannelRemoved != null) this.onDataChannelRemoved(value);
238
+ }
239
+ });
240
+ const id = DataSink.createId(channel.source, channel.id);
241
+ this.composites.set(id, channel);
242
+ this.channels.set(id, channel);
243
+ if (this.onDataChannelAdded != null) this.onDataChannelAdded(channel);
244
+ }
245
+ } else {
246
+ for (const transformer of this.transformers) try {
247
+ const result = transformer.inspect(source, name, type, metadata);
248
+ if (result != null) {
249
+ if (typeof result === "string") this.schemas.set(DataSink.createId(source, name), [transformer, result]);
250
+ else this.registerChannel(result, publish);
251
+ return;
252
+ }
253
+ } catch (exception) {
254
+ this.logger(`Transformer '${typeof transformer}' inspection failed`, exception);
255
+ }
256
+ this.registerChannel({
257
+ source,
258
+ id: name,
259
+ dataType,
260
+ publishedDataType: type,
261
+ metadata
262
+ }, publish);
263
+ }
264
+ }
265
+ /**
266
+ * Enqueues a timestamped value for a named data channel.
267
+ *
268
+ * @param source source, e.g. `nt` or `wpilog`
269
+ * @param name channel name
270
+ * @param timestamp timestamp in microseconds
271
+ * @param value raw value
272
+ */
273
+ enqueue(source, name, timestamp, value) {
274
+ this.timestamp = Math.max(this.timestamp, timestamp);
275
+ const id = DataSink.createId(source, name);
276
+ const schema = this.schemas.get(id);
277
+ if (schema) {
278
+ const [transformer, typeName] = schema;
279
+ try {
280
+ transformer.schema(typeName, value);
281
+ } catch (exception) {
282
+ this.logger(`Transformer '${typeof transformer}' failed to ingest schema data`, exception);
283
+ return false;
284
+ }
285
+ this.channels.forEach((channel$1) => {
286
+ if (channel$1.transformer === transformer && channel$1.dlq != null && channel$1.dlq.length > 0 && channel$1.structuredType != null && channel$1.transformer.canTransform(channel$1.structuredType.name)) {
287
+ channel$1.records ??= [];
288
+ for (const record of channel$1.dlq) try {
289
+ const v$1 = transformer.deserialize(record.value, channel$1.structuredType);
290
+ if (v$1 != null) enqueueData(channel$1, record.timestamp, v$1, false);
291
+ } catch (exception) {
292
+ this.logger(`Transformer '${typeof transformer}' failed to transform data in channel '${id}'`, exception);
293
+ }
294
+ channel$1.dlq = void 0;
295
+ }
296
+ });
297
+ return true;
298
+ }
299
+ if (!this.disableCompositeChannels && name.endsWith("/.type")) {
300
+ const channel$1 = this.composites.get(id.slice(0, -6));
301
+ if (channel$1 != null) {
302
+ if (typeof value === "string") {
303
+ channel$1.structuredType = {
304
+ name: value,
305
+ format: "composite"
306
+ };
307
+ enqueueDataWithPath(channel$1, timestamp, value, [".type"]);
308
+ }
309
+ return true;
310
+ }
311
+ }
312
+ const channel = this.channels.get(id);
313
+ if (channel == null) return false;
314
+ let v = value;
315
+ try {
316
+ if (channel.transformer != null) {
317
+ v = channel.transformer.deserialize(v, channel.structuredType);
318
+ if (v == null) {
319
+ enqueueData(channel, timestamp, value, true);
320
+ return true;
321
+ }
322
+ }
323
+ } catch (exception) {
324
+ this.logger(`Transformer '${typeof channel.transformer}' failed to transform data in channel '${id}'`, exception);
325
+ return false;
326
+ }
327
+ enqueueData(channel, timestamp, v, false);
328
+ return true;
329
+ }
330
+ /**
331
+ * Prunes old records based on the retention policy if configured.
332
+ *
333
+ * The `currentTimestamp` represents time in the robot clock, not
334
+ * wall clock, typically you want to supply the most recent timestamp
335
+ * reported by live connection protocol, such as NetworkTables.
336
+ * Defaults to {@link recentTimestamp} field.
337
+ *
338
+ * @param currentTimestamp timestamp in microseconds representing current time
339
+ */
340
+ enforceRetention(currentTimestamp) {
341
+ if (this.retention == null) return;
342
+ const timestamp = currentTimestamp ?? this.timestamp;
343
+ const maxSize = this.retention.maxSize;
344
+ const cutoff = this.retention.maxTimeSeconds != null ? Math.max(0, timestamp - this.retention.maxTimeSeconds * 1e6) : void 0;
345
+ if (maxSize != null && maxSize > 0 || cutoff != null) this.channels.forEach((channel) => {
346
+ if (channel.records != null) require_utils.pruneTimestampedRecords(channel.records, maxSize, cutoff);
347
+ if (channel.dlq != null) require_utils.pruneTimestampedRecords(channel.dlq, maxSize, cutoff);
348
+ });
349
+ }
350
+ /** Purges this sink and all its records. */
351
+ purge() {
352
+ this.channels.clear();
353
+ this.composites.clear();
354
+ this.schemas.clear();
355
+ this.timestamp = 0;
356
+ }
357
+ };
358
+
359
+ //#endregion
360
+ exports.DataSink = DataSink;
@@ -0,0 +1,93 @@
1
+ import { DataChannel, DataTransformer } from "./abstractions.cjs";
2
+
3
+ //#region src/sink.d.ts
4
+ type NativeChannelPublisher = (topic: string, type: string, value: unknown) => void;
5
+ type DataRetentionPolicy = {
6
+ /** Maximum number of records to retain */
7
+ maxSize?: number;
8
+ /** Time window in seconds to retain */
9
+ maxTimeSeconds?: number;
10
+ };
11
+ /** A data sink in the data pipeline with support for protocol transformers, data retention, etc. */
12
+ declare class DataSink {
13
+ private readonly channels;
14
+ private readonly composites;
15
+ private readonly schemas;
16
+ private readonly transformers;
17
+ private readonly retention?;
18
+ private readonly disableCompositeChannels;
19
+ private readonly onDataChannelAdded?;
20
+ private readonly onDataChannelRemoved?;
21
+ private readonly logger;
22
+ private timestamp;
23
+ private readonly structTransformer;
24
+ private readonly protobufTransformer;
25
+ constructor(options: {
26
+ /** Additional data transformers. Default transformers for structured and binary types are always used. */
27
+ transformers?: Array<DataTransformer>;
28
+ /** Data retention policy. Default is unlimited retention. */
29
+ retention?: DataRetentionPolicy;
30
+ /** Callback invoked when new data channel is registered. */
31
+ onDataChannelAdded?: (channel: DataChannel) => void;
32
+ /** Callback invoked when existing data channel is removed. */
33
+ onDataChannelRemoved?: (channel: DataChannel) => void;
34
+ /** Disables support for composite (legacy) channels. */
35
+ disableCompositeChannels?: boolean;
36
+ /** Logger callback. Default is logging to console. */
37
+ logger?: (message: string, error: unknown) => void;
38
+ });
39
+ /** Constructs channel identifier. */
40
+ private static createId;
41
+ /** Returns parent composite data channel if one exists. */
42
+ private getCompositeParent;
43
+ /** Registers new data channel. */
44
+ private registerChannel;
45
+ /** Gets sub-channel by its path. */
46
+ private getSubChannel;
47
+ /** Creates channel publisher. */
48
+ private createChannelPublisher;
49
+ /** Returns most recent timestamp in microseconds observed by this instance. */
50
+ get recentTimestamp(): number;
51
+ /**
52
+ * Gets a channel descriptor.
53
+ *
54
+ * @param source source, e.g. `nt` or `wpilog`
55
+ * @param name channel name
56
+ */
57
+ get(source: string, name: string): DataChannel | undefined;
58
+ /**
59
+ * Adds a channel descriptor.
60
+ *
61
+ * @param source source, e.g. `nt` or `wpilog`
62
+ * @param name channel name
63
+ * @param type channel type
64
+ * @param properties channel properties
65
+ * @param publish channel publisher
66
+ */
67
+ add(source: string, name: string, type: string, properties?: Record<string, unknown> | string, publish?: NativeChannelPublisher): void;
68
+ /**
69
+ * Enqueues a timestamped value for a named data channel.
70
+ *
71
+ * @param source source, e.g. `nt` or `wpilog`
72
+ * @param name channel name
73
+ * @param timestamp timestamp in microseconds
74
+ * @param value raw value
75
+ */
76
+ enqueue(source: string, name: string, timestamp: number, value: unknown): boolean;
77
+ /**
78
+ * Prunes old records based on the retention policy if configured.
79
+ *
80
+ * The `currentTimestamp` represents time in the robot clock, not
81
+ * wall clock, typically you want to supply the most recent timestamp
82
+ * reported by live connection protocol, such as NetworkTables.
83
+ * Defaults to {@link recentTimestamp} field.
84
+ *
85
+ * @param currentTimestamp timestamp in microseconds representing current time
86
+ */
87
+ enforceRetention(currentTimestamp?: number): void;
88
+ /** Purges this sink and all its records. */
89
+ purge(): void;
90
+ }
91
+ //#endregion
92
+ export { DataRetentionPolicy, DataSink, NativeChannelPublisher };
93
+ //# sourceMappingURL=sink.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink.d.cts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;KAqKY,sBAAA;KAEA,mBAAA;EAFA;EAEA,OAAA,CAAA,EAAA,MAAA;EAYC;EAkBY,cAAA,CAAA,EAAA,MAAA;CAAN;;AAIgB,cAtBtB,QAAA,CAsBsB;EAEE,iBAAA,QAAA;EA4II,iBAAA,UAAA;EAkBxB,iBAAA,OAAA;EACH,iBAAA,YAAA;EAAsB,iBAAA,SAAA;;;;;;;;;;mBArKjB,MAAM;;gBAET;;mCAEmB;;qCAEE;;;;;;;;;;;;;;;;;;;;;;;;qCA4II;;;;;;;;;;+DAkBxB,4CACH"}
@@ -0,0 +1,93 @@
1
+ import { DataChannel, DataTransformer } from "./abstractions.mjs";
2
+
3
+ //#region src/sink.d.ts
4
+ type NativeChannelPublisher = (topic: string, type: string, value: unknown) => void;
5
+ type DataRetentionPolicy = {
6
+ /** Maximum number of records to retain */
7
+ maxSize?: number;
8
+ /** Time window in seconds to retain */
9
+ maxTimeSeconds?: number;
10
+ };
11
+ /** A data sink in the data pipeline with support for protocol transformers, data retention, etc. */
12
+ declare class DataSink {
13
+ private readonly channels;
14
+ private readonly composites;
15
+ private readonly schemas;
16
+ private readonly transformers;
17
+ private readonly retention?;
18
+ private readonly disableCompositeChannels;
19
+ private readonly onDataChannelAdded?;
20
+ private readonly onDataChannelRemoved?;
21
+ private readonly logger;
22
+ private timestamp;
23
+ private readonly structTransformer;
24
+ private readonly protobufTransformer;
25
+ constructor(options: {
26
+ /** Additional data transformers. Default transformers for structured and binary types are always used. */
27
+ transformers?: Array<DataTransformer>;
28
+ /** Data retention policy. Default is unlimited retention. */
29
+ retention?: DataRetentionPolicy;
30
+ /** Callback invoked when new data channel is registered. */
31
+ onDataChannelAdded?: (channel: DataChannel) => void;
32
+ /** Callback invoked when existing data channel is removed. */
33
+ onDataChannelRemoved?: (channel: DataChannel) => void;
34
+ /** Disables support for composite (legacy) channels. */
35
+ disableCompositeChannels?: boolean;
36
+ /** Logger callback. Default is logging to console. */
37
+ logger?: (message: string, error: unknown) => void;
38
+ });
39
+ /** Constructs channel identifier. */
40
+ private static createId;
41
+ /** Returns parent composite data channel if one exists. */
42
+ private getCompositeParent;
43
+ /** Registers new data channel. */
44
+ private registerChannel;
45
+ /** Gets sub-channel by its path. */
46
+ private getSubChannel;
47
+ /** Creates channel publisher. */
48
+ private createChannelPublisher;
49
+ /** Returns most recent timestamp in microseconds observed by this instance. */
50
+ get recentTimestamp(): number;
51
+ /**
52
+ * Gets a channel descriptor.
53
+ *
54
+ * @param source source, e.g. `nt` or `wpilog`
55
+ * @param name channel name
56
+ */
57
+ get(source: string, name: string): DataChannel | undefined;
58
+ /**
59
+ * Adds a channel descriptor.
60
+ *
61
+ * @param source source, e.g. `nt` or `wpilog`
62
+ * @param name channel name
63
+ * @param type channel type
64
+ * @param properties channel properties
65
+ * @param publish channel publisher
66
+ */
67
+ add(source: string, name: string, type: string, properties?: Record<string, unknown> | string, publish?: NativeChannelPublisher): void;
68
+ /**
69
+ * Enqueues a timestamped value for a named data channel.
70
+ *
71
+ * @param source source, e.g. `nt` or `wpilog`
72
+ * @param name channel name
73
+ * @param timestamp timestamp in microseconds
74
+ * @param value raw value
75
+ */
76
+ enqueue(source: string, name: string, timestamp: number, value: unknown): boolean;
77
+ /**
78
+ * Prunes old records based on the retention policy if configured.
79
+ *
80
+ * The `currentTimestamp` represents time in the robot clock, not
81
+ * wall clock, typically you want to supply the most recent timestamp
82
+ * reported by live connection protocol, such as NetworkTables.
83
+ * Defaults to {@link recentTimestamp} field.
84
+ *
85
+ * @param currentTimestamp timestamp in microseconds representing current time
86
+ */
87
+ enforceRetention(currentTimestamp?: number): void;
88
+ /** Purges this sink and all its records. */
89
+ purge(): void;
90
+ }
91
+ //#endregion
92
+ export { DataRetentionPolicy, DataSink, NativeChannelPublisher };
93
+ //# sourceMappingURL=sink.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sink.d.mts","names":[],"sources":["../src/sink.ts"],"sourcesContent":[],"mappings":";;;KAqKY,sBAAA;KAEA,mBAAA;EAFA;EAEA,OAAA,CAAA,EAAA,MAAA;EAYC;EAkBY,cAAA,CAAA,EAAA,MAAA;CAAN;;AAIgB,cAtBtB,QAAA,CAsBsB;EAEE,iBAAA,QAAA;EA4II,iBAAA,UAAA;EAkBxB,iBAAA,OAAA;EACH,iBAAA,YAAA;EAAsB,iBAAA,SAAA;;;;;;;;;;mBArKjB,MAAM;;gBAET;;mCAEmB;;qCAEE;;;;;;;;;;;;;;;;;;;;;;;;qCA4II;;;;;;;;;;+DAkBxB,4CACH"}