@genrojs/tytx 0.16.1
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +334 -0
- package/js/README.md +79 -0
- package/js/src/decode.js +128 -0
- package/js/src/encode.js +162 -0
- package/js/src/http.js +101 -0
- package/js/src/index.d.ts +38 -0
- package/js/src/index.js +63 -0
- package/js/src/msgpack.js +185 -0
- package/js/src/platform/dependencies.js +12 -0
- package/js/src/qs.js +98 -0
- package/js/src/registry.js +463 -0
- package/js/src/utils.js +164 -0
- package/js/src/xml.js +228 -0
- package/package.json +52 -0
package/js/src/decode.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* TYTX Decoding - TYTX format to JavaScript objects.
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple transports: json, xml, msgpack.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fromXml } from './xml.js';
|
|
9
|
+
import { fromMsgpack } from './msgpack.js';
|
|
10
|
+
import { walk, rawDecode } from './utils.js';
|
|
11
|
+
|
|
12
|
+
const TYTX_MARKER = '::JS';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Filter for string values.
|
|
16
|
+
* @param {any} v
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function isString(v) {
|
|
20
|
+
return typeof v === 'string';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Decode a TYTX JSON string to JavaScript objects (internal).
|
|
25
|
+
*
|
|
26
|
+
* @param {string} data - JSON string with ::JS suffix (struct) or ::T suffix (scalar)
|
|
27
|
+
* @returns {any} JavaScript object with typed values hydrated
|
|
28
|
+
*/
|
|
29
|
+
function _fromJson(data) {
|
|
30
|
+
// Try rawDecode first (scalar with type suffix)
|
|
31
|
+
const [decoded, value] = rawDecode(data);
|
|
32
|
+
if (decoded) {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let jsonData = data;
|
|
37
|
+
if (jsonData.endsWith('::JS')) {
|
|
38
|
+
jsonData = jsonData.slice(0, -4);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(jsonData);
|
|
44
|
+
} catch {
|
|
45
|
+
return data;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return walk(parsed, _decodeItem, isString);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Decode a single item with TYTX suffix.
|
|
53
|
+
* @param {string} s
|
|
54
|
+
* @returns {any}
|
|
55
|
+
*/
|
|
56
|
+
function _decodeItem(s) {
|
|
57
|
+
if (!s.includes('::')) {
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
return rawDecode(s)[1];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decode a TYTX XML string to JavaScript objects (internal).
|
|
65
|
+
* @param {string} data
|
|
66
|
+
* @returns {any}
|
|
67
|
+
*/
|
|
68
|
+
function _fromXml(data) {
|
|
69
|
+
const result = fromXml(data);
|
|
70
|
+
// If result is a string with TYTX suffix, hydrate it via JSON decoder
|
|
71
|
+
if (typeof result === 'string') {
|
|
72
|
+
return fromTytx(result);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decode TYTX MessagePack bytes to JavaScript objects (internal).
|
|
79
|
+
* @param {Uint8Array} data
|
|
80
|
+
* @returns {any}
|
|
81
|
+
*/
|
|
82
|
+
function _fromMsgpack(data) {
|
|
83
|
+
return fromMsgpack(data);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Decode TYTX format to JavaScript objects.
|
|
88
|
+
*
|
|
89
|
+
* @param {string|Uint8Array|null} data - Encoded data (string for json/xml, Uint8Array for msgpack), or null
|
|
90
|
+
* @param {string|null} transport - Input format: "json", "xml", "msgpack", or null
|
|
91
|
+
* @returns {any} JavaScript object with typed values hydrated, or null if data is null
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* fromTytx('{"price": "100.50::N"}::JS')
|
|
95
|
+
* // {"price": Decimal("100.50")}
|
|
96
|
+
*
|
|
97
|
+
* fromTytx('<root>100::N</root>', "xml")
|
|
98
|
+
* // {"root": {"attrs": {}, "value": Decimal("100")}}
|
|
99
|
+
*/
|
|
100
|
+
function fromTytx(data, transport = null) {
|
|
101
|
+
if (data === null) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (transport === null || transport === 'json') {
|
|
106
|
+
let jsonData = data;
|
|
107
|
+
if (transport === 'json' && data.startsWith('"') && data.endsWith('"')) {
|
|
108
|
+
jsonData = data.slice(1, -1); // Remove surrounding quotes (TYTX-wrapped)
|
|
109
|
+
}
|
|
110
|
+
return _fromJson(jsonData);
|
|
111
|
+
} else if (transport === 'xml') {
|
|
112
|
+
return _fromXml(data);
|
|
113
|
+
} else if (transport === 'msgpack') {
|
|
114
|
+
return _fromMsgpack(data);
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`Unknown transport: ${transport}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export {
|
|
121
|
+
fromTytx,
|
|
122
|
+
TYTX_MARKER,
|
|
123
|
+
isString,
|
|
124
|
+
_fromJson,
|
|
125
|
+
_fromXml,
|
|
126
|
+
_fromMsgpack,
|
|
127
|
+
_decodeItem,
|
|
128
|
+
};
|
package/js/src/encode.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* TYTX Encoding - JavaScript objects to TYTX format.
|
|
4
|
+
*
|
|
5
|
+
* Supports multiple transports: json, xml, msgpack.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { toQs } from './qs.js';
|
|
9
|
+
import { toXml } from './xml.js';
|
|
10
|
+
import { toMsgpack } from './msgpack.js';
|
|
11
|
+
import { rawEncode } from './utils.js';
|
|
12
|
+
import { getTypeEntry } from './registry.js';
|
|
13
|
+
import { msgpack } from './platform/dependencies.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pre-process a value recursively, converting typed values to TYTX strings.
|
|
17
|
+
* Returns [processedValue, hasSpecial].
|
|
18
|
+
*
|
|
19
|
+
* @param {any} value
|
|
20
|
+
* @returns {[any, boolean]}
|
|
21
|
+
*/
|
|
22
|
+
function _preprocessValue(value) {
|
|
23
|
+
// Check if value is a typed value that needs encoding
|
|
24
|
+
const entry = getTypeEntry(value);
|
|
25
|
+
if (entry !== null) {
|
|
26
|
+
const [suffix, serializer, jsonNative] = entry;
|
|
27
|
+
if (!jsonNative) {
|
|
28
|
+
return [`${serializer(value)}::${suffix}`, true];
|
|
29
|
+
}
|
|
30
|
+
// jsonNative types (bool, int, float) are kept as-is
|
|
31
|
+
return [value, false];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle arrays
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
let hasSpecial = false;
|
|
37
|
+
const result = value.map(item => {
|
|
38
|
+
const [processed, special] = _preprocessValue(item);
|
|
39
|
+
if (special) hasSpecial = true;
|
|
40
|
+
return processed;
|
|
41
|
+
});
|
|
42
|
+
return [result, hasSpecial];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle objects (but not null)
|
|
46
|
+
if (value !== null && typeof value === 'object') {
|
|
47
|
+
let hasSpecial = false;
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [k, v] of Object.entries(value)) {
|
|
50
|
+
const [processed, special] = _preprocessValue(v);
|
|
51
|
+
if (special) hasSpecial = true;
|
|
52
|
+
result[k] = processed;
|
|
53
|
+
}
|
|
54
|
+
return [result, hasSpecial];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Primitive values (string, number, boolean, null)
|
|
58
|
+
return [value, false];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Encode a JavaScript value to TYTX JSON string (internal).
|
|
63
|
+
*
|
|
64
|
+
* @param {any} value - JavaScript object to encode
|
|
65
|
+
* @param {boolean} forceSuffix - If true, add suffix for all types (int/bool/float)
|
|
66
|
+
* @returns {string} JSON string. For dict/list with typed values: adds ::JS suffix.
|
|
67
|
+
*/
|
|
68
|
+
function _toJson(value, forceSuffix = false) {
|
|
69
|
+
const [encoded, result] = rawEncode(value, forceSuffix);
|
|
70
|
+
if (encoded) {
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [processed, hasSpecial] = _preprocessValue(value);
|
|
75
|
+
const jsonResult = JSON.stringify(processed);
|
|
76
|
+
|
|
77
|
+
if (hasSpecial) {
|
|
78
|
+
return `${jsonResult}::JS`;
|
|
79
|
+
}
|
|
80
|
+
return jsonResult;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Encode a JavaScript value to raw JSON string (no TYTX suffix).
|
|
85
|
+
* @param {any} value
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function _toRawJson(value) {
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Encode a JavaScript value to raw MessagePack bytes (no TYTX processing).
|
|
94
|
+
* @param {any} value
|
|
95
|
+
* @returns {Uint8Array}
|
|
96
|
+
*/
|
|
97
|
+
function _toRawMsgpack(value) {
|
|
98
|
+
if (!msgpack) throw new Error('@msgpack/msgpack is required for MessagePack support');
|
|
99
|
+
return msgpack.encode(value);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Encode a JavaScript value to TYTX format.
|
|
104
|
+
*
|
|
105
|
+
* @param {any} value - JavaScript object to encode
|
|
106
|
+
* @param {string|null} transport - Output format: "json", "xml", "msgpack", or null
|
|
107
|
+
* @param {Object} options - Options object
|
|
108
|
+
* @param {boolean} options.raw - If true, output raw format without TYTX type suffixes
|
|
109
|
+
* @param {boolean} options.qs - If true, output as query string format (flat object or array only)
|
|
110
|
+
* @param {boolean} options._forceSuffix - Internal: force suffix for all types
|
|
111
|
+
* @returns {string|Uint8Array} Encoded data (string for json/xml, Uint8Array for msgpack)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* toTytx({"price": createDecimal("100.50")})
|
|
115
|
+
* // '{"price": "100.50::N"}::JS'
|
|
116
|
+
*
|
|
117
|
+
* toTytx({"price": 100.50}, null, { raw: true })
|
|
118
|
+
* // '{"price": 100.5}'
|
|
119
|
+
*
|
|
120
|
+
* toTytx({"alfa": 33, "date": new Date(Date.UTC(2025, 11, 14))}, null, { qs: true })
|
|
121
|
+
* // 'alfa=33::L&date=2025-12-14::D::QS'
|
|
122
|
+
*
|
|
123
|
+
* toTytx({"root": {"value": createDecimal("100")}}, "xml")
|
|
124
|
+
* // '<?xml version="1.0" ?><root>100::N</root>'
|
|
125
|
+
*/
|
|
126
|
+
function toTytx(value, transport = null, { raw = false, qs = false, _forceSuffix = false } = {}) {
|
|
127
|
+
if (qs) {
|
|
128
|
+
return `${toQs(value)}::QS`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (raw) {
|
|
132
|
+
if (transport === null || transport === 'json') {
|
|
133
|
+
return _toRawJson(value);
|
|
134
|
+
} else if (transport === 'msgpack') {
|
|
135
|
+
return _toRawMsgpack(value);
|
|
136
|
+
} else if (transport === 'xml') {
|
|
137
|
+
throw new Error('raw=true is not supported for XML transport');
|
|
138
|
+
} else {
|
|
139
|
+
throw new Error(`Unknown transport: ${transport}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (transport === null || transport === 'json') {
|
|
144
|
+
const result = _toJson(value, _forceSuffix);
|
|
145
|
+
if (transport === 'json') {
|
|
146
|
+
return `"${result}"`;
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
} else if (transport === 'xml') {
|
|
150
|
+
const result = toXml(value);
|
|
151
|
+
return `<?xml version="1.0" ?><tytx_root>${result}</tytx_root>`;
|
|
152
|
+
} else if (transport === 'msgpack') {
|
|
153
|
+
return toMsgpack(value);
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error(`Unknown transport: ${transport}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export {
|
|
160
|
+
toTytx,
|
|
161
|
+
_toJson,
|
|
162
|
+
};
|
package/js/src/http.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* TYTX HTTP utilities.
|
|
4
|
+
*
|
|
5
|
+
* Client-side functions to make HTTP requests with TYTX serialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { toTytx } from './encode.js';
|
|
9
|
+
import { fromTytx } from './decode.js';
|
|
10
|
+
|
|
11
|
+
const CONTENT_TYPES = {
|
|
12
|
+
json: 'application/json',
|
|
13
|
+
xml: 'application/xml',
|
|
14
|
+
msgpack: 'application/msgpack',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get transport from Content-Type header.
|
|
19
|
+
* @param {string} contentType
|
|
20
|
+
* @returns {'json'|'xml'|'msgpack'|null}
|
|
21
|
+
*/
|
|
22
|
+
function getTransport(contentType) {
|
|
23
|
+
if (!contentType) return null;
|
|
24
|
+
const ct = contentType.toLowerCase();
|
|
25
|
+
if (ct.includes('json')) return 'json';
|
|
26
|
+
if (ct.includes('xml')) return 'xml';
|
|
27
|
+
if (ct.includes('msgpack')) return 'msgpack';
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Fetch with TYTX serialization/deserialization.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} url - URL to fetch
|
|
35
|
+
* @param {Object} options - Fetch options
|
|
36
|
+
* @param {any} [options.body] - Data to send (will be serialized with toTytx)
|
|
37
|
+
* @param {'json'|'xml'|'msgpack'} [options.transport='json'] - Transport format
|
|
38
|
+
* @param {string} [options.method='GET'] - HTTP method
|
|
39
|
+
* @param {Object} [options.headers={}] - Additional headers
|
|
40
|
+
* @returns {Promise<any>} Deserialized response data
|
|
41
|
+
*/
|
|
42
|
+
async function fetchTytx(url, options = {}) {
|
|
43
|
+
const {
|
|
44
|
+
body,
|
|
45
|
+
transport = 'json',
|
|
46
|
+
method = body !== undefined ? 'POST' : 'GET',
|
|
47
|
+
headers = {},
|
|
48
|
+
...fetchOptions
|
|
49
|
+
} = options;
|
|
50
|
+
|
|
51
|
+
const requestHeaders = {
|
|
52
|
+
'X-TYTX-Transport': transport,
|
|
53
|
+
...headers,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Serialize body if provided
|
|
57
|
+
let requestBody;
|
|
58
|
+
if (body !== undefined) {
|
|
59
|
+
requestHeaders['Content-Type'] = CONTENT_TYPES[transport];
|
|
60
|
+
const encoded = toTytx(body, transport);
|
|
61
|
+
if (transport === 'msgpack') {
|
|
62
|
+
requestBody = encoded;
|
|
63
|
+
} else {
|
|
64
|
+
requestBody = encoded;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Make request
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method,
|
|
71
|
+
headers: requestHeaders,
|
|
72
|
+
body: requestBody,
|
|
73
|
+
...fetchOptions,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Determine response transport from Content-Type
|
|
81
|
+
const responseContentType = response.headers.get('Content-Type') || '';
|
|
82
|
+
const responseTransport = getTransport(responseContentType) || transport;
|
|
83
|
+
|
|
84
|
+
// Read and deserialize response
|
|
85
|
+
let responseData;
|
|
86
|
+
if (responseTransport === 'msgpack') {
|
|
87
|
+
const buffer = await response.arrayBuffer();
|
|
88
|
+
responseData = fromTytx(Buffer.from(buffer), responseTransport);
|
|
89
|
+
} else {
|
|
90
|
+
const text = await response.text();
|
|
91
|
+
responseData = fromTytx(text, responseTransport);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return responseData;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
fetchTytx,
|
|
99
|
+
getTransport,
|
|
100
|
+
CONTENT_TYPES,
|
|
101
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Copyright 2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/** Supported wire transports; null selects suffix-encoded typed text. */
|
|
3
|
+
export type Transport = 'json' | 'xml' | 'msgpack' | null;
|
|
4
|
+
export type DecimalLibrary = 'decimal.js' | 'big.js' | 'number';
|
|
5
|
+
export interface EncodeOptions { raw?: boolean; qs?: boolean; _forceSuffix?: boolean; }
|
|
6
|
+
/** Serialize a value. MessagePack returns bytes; other transports return text. */
|
|
7
|
+
export function toTytx(value: unknown, transport?: Transport, options?: EncodeOptions): string | Uint8Array;
|
|
8
|
+
/** Decode typed text, XML or MessagePack. Registered decoders determine the result. */
|
|
9
|
+
export function fromTytx(data: string | Uint8Array | null, transport?: Transport): unknown;
|
|
10
|
+
export interface FetchOptions extends Omit<RequestInit, 'body' | 'headers'> {
|
|
11
|
+
body?: unknown;
|
|
12
|
+
transport?: Exclude<Transport, null>;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
export function fetchTytx(url: string, options?: FetchOptions): Promise<unknown>;
|
|
16
|
+
export function getTransport(contentType: string | null | undefined): Transport;
|
|
17
|
+
export const CONTENT_TYPES: { json: string; xml: string; msgpack: string };
|
|
18
|
+
export function isDecimal(value: unknown): boolean;
|
|
19
|
+
/** Returns an instance of the selected decimal library, or boxed Number. */
|
|
20
|
+
export function createDecimal(value: string | number): { toString(): string; valueOf(): string | number };
|
|
21
|
+
export function setDecimalLibrary(name: DecimalLibrary): void;
|
|
22
|
+
export function getDecimalLibrary(): DecimalLibrary;
|
|
23
|
+
export function registerType<T>(cls: new (...args: any[]) => T, suffix: string,
|
|
24
|
+
serializer: (value: T) => string, deserializer: (value: string) => T,
|
|
25
|
+
jsonNative?: boolean): void;
|
|
26
|
+
export interface RegisteredClass<T extends { toTytx(): string }> {
|
|
27
|
+
new (...args: any[]): T;
|
|
28
|
+
tytxSuffix: string;
|
|
29
|
+
tytxJsonNative?: boolean;
|
|
30
|
+
fromTytx(value: string): T;
|
|
31
|
+
}
|
|
32
|
+
export function registerClass<T extends { toTytx(): string }, C extends RegisteredClass<T>>(cls: C): C;
|
|
33
|
+
export function getRegisteredType(suffix: string): Function | null;
|
|
34
|
+
/** Stores the subtype dictionary of a suffix, replacing the previous one. */
|
|
35
|
+
export function setSubtypeDict(suffix: string, subtypes: Record<string, unknown>): void;
|
|
36
|
+
/** Returns the subtype dictionary of a suffix, or {} if none was set. */
|
|
37
|
+
export function getSubtypeDict(suffix: string): Record<string, unknown>;
|
|
38
|
+
export const __version__: string;
|
package/js/src/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* @ts-self-types="./index.d.ts" */
|
|
2
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
3
|
+
/**
|
|
4
|
+
* TYTX Base - Typed Text Protocol for Scalar Types
|
|
5
|
+
*
|
|
6
|
+
* Minimal implementation supporting:
|
|
7
|
+
* - Scalar types: Decimal, date, datetime, time, bool, int
|
|
8
|
+
* - Encoders/Decoders: JSON, XML, MessagePack
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { toTytx, fromTytx, fetchTytx } from '@genrojs/tytx';
|
|
12
|
+
* import Big from 'big.js';
|
|
13
|
+
*
|
|
14
|
+
* // Encode
|
|
15
|
+
* const data = {"price": new Big("100.50"), "date": new Date(Date.UTC(2025, 0, 15))};
|
|
16
|
+
* const jsonStr = toTytx(data);
|
|
17
|
+
* // '{"price": "100.50::N", "date": "2025-01-15::D"}::JS'
|
|
18
|
+
*
|
|
19
|
+
* // Decode
|
|
20
|
+
* const result = fromTytx(jsonStr);
|
|
21
|
+
* // {"price": Big("100.50"), "date": Date}
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
isDecimal,
|
|
26
|
+
createDecimal,
|
|
27
|
+
setDecimalLibrary,
|
|
28
|
+
getDecimalLibrary,
|
|
29
|
+
registerType,
|
|
30
|
+
registerClass,
|
|
31
|
+
getRegisteredType,
|
|
32
|
+
setSubtypeDict,
|
|
33
|
+
getSubtypeDict,
|
|
34
|
+
} from './registry.js';
|
|
35
|
+
|
|
36
|
+
import { toTytx } from './encode.js';
|
|
37
|
+
import { fromTytx } from './decode.js';
|
|
38
|
+
import { fetchTytx, getTransport, CONTENT_TYPES } from './http.js';
|
|
39
|
+
|
|
40
|
+
const __version__ = '0.16.1';
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
// Core API
|
|
44
|
+
toTytx,
|
|
45
|
+
fromTytx,
|
|
46
|
+
// HTTP utilities
|
|
47
|
+
fetchTytx,
|
|
48
|
+
getTransport,
|
|
49
|
+
CONTENT_TYPES,
|
|
50
|
+
// Decimal utilities
|
|
51
|
+
isDecimal,
|
|
52
|
+
createDecimal,
|
|
53
|
+
setDecimalLibrary,
|
|
54
|
+
getDecimalLibrary,
|
|
55
|
+
// Custom type registration
|
|
56
|
+
registerType,
|
|
57
|
+
registerClass,
|
|
58
|
+
getRegisteredType,
|
|
59
|
+
setSubtypeDict,
|
|
60
|
+
getSubtypeDict,
|
|
61
|
+
// Version
|
|
62
|
+
__version__,
|
|
63
|
+
};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* TYTX MessagePack Encoding/Decoding.
|
|
4
|
+
*
|
|
5
|
+
* Uses MessagePack extension types to carry TYTX type information directly in the protocol:
|
|
6
|
+
* - Ext -1: datetime (native Timestamp, handled by @msgpack/msgpack)
|
|
7
|
+
* - Ext 1: Decimal (payload: UTF-8 string, e.g. "100.50")
|
|
8
|
+
* - Ext 2: date (payload: ISO "YYYY-MM-DD")
|
|
9
|
+
* - Ext 3: time (payload: ISO "HH:MM:SS.mmm")
|
|
10
|
+
* - Ext 4: registered custom type (payload: UTF-8 "SUFFIX:serialized",
|
|
11
|
+
* split at the first ":"; unknown suffixes on the receiving side
|
|
12
|
+
* degrade to the string "serialized::SUFFIX", like the json path)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
getDateType,
|
|
17
|
+
createDecimal,
|
|
18
|
+
isDecimal,
|
|
19
|
+
getCustomTypeEntry,
|
|
20
|
+
SUFFIX_TO_TYPE,
|
|
21
|
+
} from './registry.js';
|
|
22
|
+
import { msgpack } from './platform/dependencies.js';
|
|
23
|
+
|
|
24
|
+
// Check for @msgpack/msgpack availability
|
|
25
|
+
const HAS_MSGPACK = msgpack !== null;
|
|
26
|
+
|
|
27
|
+
function _checkMsgpack() {
|
|
28
|
+
if (!HAS_MSGPACK) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'@msgpack/msgpack is required for MessagePack support. ' +
|
|
31
|
+
'Install with: npm install @msgpack/msgpack'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Build the ExtensionCodec once at module load (if msgpack is available).
|
|
37
|
+
// The encode/decode functions close over isDecimal/createDecimal/getDateType
|
|
38
|
+
// which read the current decimal library state at call time.
|
|
39
|
+
const _extensionCodec = _buildCodec();
|
|
40
|
+
|
|
41
|
+
function _buildCodec() {
|
|
42
|
+
if (!HAS_MSGPACK) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const enc = new TextEncoder();
|
|
47
|
+
const dec = new TextDecoder();
|
|
48
|
+
const codec = new msgpack.ExtensionCodec();
|
|
49
|
+
|
|
50
|
+
// Override built-in Timestamp encoder (-1) to skip date-only and time-only
|
|
51
|
+
// Date objects so they can be handled by ext 2 and ext 3 below.
|
|
52
|
+
// DHZ datetimes fall through to the native Timestamp encoding.
|
|
53
|
+
codec.register({
|
|
54
|
+
type: -1,
|
|
55
|
+
encode: (v) => {
|
|
56
|
+
if (v instanceof Date) {
|
|
57
|
+
const dt = getDateType(v);
|
|
58
|
+
if (dt === 'D' || dt === 'H') {
|
|
59
|
+
return null; // let custom encoders (2, 3) handle these
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return msgpack.encodeTimestampExtension(v);
|
|
63
|
+
},
|
|
64
|
+
decode: (data) => msgpack.decodeTimestampExtension(data),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Ext 1: Decimal
|
|
68
|
+
codec.register({
|
|
69
|
+
type: 1,
|
|
70
|
+
encode: (v) => {
|
|
71
|
+
if (isDecimal(v)) {
|
|
72
|
+
return enc.encode(v.toString());
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
},
|
|
76
|
+
decode: (data) => createDecimal(dec.decode(data)),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Ext 2: date-only (midnight UTC)
|
|
80
|
+
codec.register({
|
|
81
|
+
type: 2,
|
|
82
|
+
encode: (v) => {
|
|
83
|
+
if (v instanceof Date && getDateType(v) === 'D') {
|
|
84
|
+
const y = v.getUTCFullYear();
|
|
85
|
+
const m = String(v.getUTCMonth() + 1).padStart(2, '0');
|
|
86
|
+
const d = String(v.getUTCDate()).padStart(2, '0');
|
|
87
|
+
return enc.encode(`${y}-${m}-${d}`);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
},
|
|
91
|
+
decode: (data) => new Date(dec.decode(data) + 'T00:00:00.000Z'),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Ext 3: time-only (epoch-based Date)
|
|
95
|
+
codec.register({
|
|
96
|
+
type: 3,
|
|
97
|
+
encode: (v) => {
|
|
98
|
+
if (v instanceof Date && getDateType(v) === 'H') {
|
|
99
|
+
const h = String(v.getUTCHours()).padStart(2, '0');
|
|
100
|
+
const m = String(v.getUTCMinutes()).padStart(2, '0');
|
|
101
|
+
const s = String(v.getUTCSeconds()).padStart(2, '0');
|
|
102
|
+
const ms = String(v.getUTCMilliseconds()).padStart(3, '0');
|
|
103
|
+
return enc.encode(`${h}:${m}:${s}.${ms}`);
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
},
|
|
107
|
+
decode: (data) => {
|
|
108
|
+
const str = dec.decode(data);
|
|
109
|
+
// Accept "HH:MM:SS", "HH:MM:SS.mmm", or "HH:MM:SS.ffffff"
|
|
110
|
+
const dotIdx = str.indexOf('.');
|
|
111
|
+
const timePart = dotIdx >= 0 ? str.substring(0, dotIdx) : str;
|
|
112
|
+
const fracStr = dotIdx >= 0 ? str.substring(dotIdx + 1) : '0';
|
|
113
|
+
const [h, m, s] = timePart.split(':');
|
|
114
|
+
// Truncate fractional part to 3 digits (milliseconds)
|
|
115
|
+
const ms = Math.round(+fracStr.substring(0, 3));
|
|
116
|
+
return new Date(Date.UTC(1970, 0, 1, +h, +m, +s, ms));
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Ext 4: registered custom type ("SUFFIX:serialized", first-colon split)
|
|
121
|
+
codec.register({
|
|
122
|
+
type: 4,
|
|
123
|
+
encode: (v) => {
|
|
124
|
+
const entry = getCustomTypeEntry(v);
|
|
125
|
+
if (entry !== null) {
|
|
126
|
+
const [suffix, serializer] = entry;
|
|
127
|
+
return enc.encode(`${suffix}:${serializer(v)}`);
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
decode: (data) => {
|
|
132
|
+
const str = dec.decode(data);
|
|
133
|
+
const idx = str.indexOf(':');
|
|
134
|
+
const suffix = str.slice(0, idx);
|
|
135
|
+
const payload = str.slice(idx + 1);
|
|
136
|
+
const entry = SUFFIX_TO_TYPE[suffix];
|
|
137
|
+
if (entry !== undefined) {
|
|
138
|
+
const [, deserializer] = entry;
|
|
139
|
+
return deserializer(payload);
|
|
140
|
+
}
|
|
141
|
+
// Unknown suffix on the receiving side: degrade to the same
|
|
142
|
+
// pass-through string rawDecode gives on the json path.
|
|
143
|
+
return `${payload}::${suffix}`;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return codec;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Encode a JavaScript value to TYTX MessagePack bytes.
|
|
152
|
+
*
|
|
153
|
+
* @param {any} value - JavaScript object to encode
|
|
154
|
+
* @returns {Uint8Array} MessagePack bytes with TYTX types as extension types
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* toMsgpack({"price": createDecimal("100.50")})
|
|
158
|
+
* // Uint8Array[...] // MessagePack bytes
|
|
159
|
+
*/
|
|
160
|
+
function toMsgpack(value) {
|
|
161
|
+
_checkMsgpack();
|
|
162
|
+
return msgpack.encode(value, { extensionCodec: _extensionCodec });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Decode TYTX MessagePack bytes to JavaScript objects.
|
|
167
|
+
*
|
|
168
|
+
* @param {Uint8Array} data - MessagePack bytes
|
|
169
|
+
* @returns {any} JavaScript object with TYTX extension types hydrated
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* fromMsgpack(packedBytes)
|
|
173
|
+
* // {"price": Decimal("100.50")}
|
|
174
|
+
*/
|
|
175
|
+
function fromMsgpack(data) {
|
|
176
|
+
_checkMsgpack();
|
|
177
|
+
return msgpack.decode(data, { extensionCodec: _extensionCodec });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export {
|
|
181
|
+
toMsgpack,
|
|
182
|
+
fromMsgpack,
|
|
183
|
+
HAS_MSGPACK,
|
|
184
|
+
_checkMsgpack,
|
|
185
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Copyright 2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// One statically analyzable codec graph for Node, Bun and browser bundlers.
|
|
3
|
+
import Decimal from 'decimal.js';
|
|
4
|
+
import Big from 'big.js';
|
|
5
|
+
import * as messagePack from '@msgpack/msgpack';
|
|
6
|
+
import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
|
|
7
|
+
|
|
8
|
+
export const DecimalJS = Decimal;
|
|
9
|
+
export const BigJS = Big;
|
|
10
|
+
export const msgpack = messagePack;
|
|
11
|
+
export const NodeDOMParser = DOMParser;
|
|
12
|
+
export const NodeXMLSerializer = XMLSerializer;
|