@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/qs.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* TYTX Query String Encoding/Decoding.
|
|
4
|
+
*
|
|
5
|
+
* QS format is a flat key=value structure separated by &:
|
|
6
|
+
* alfa=33::L&date=2025-12-14::D::QS → {"alfa": 33, "date": Date(2025, 12, 14)}
|
|
7
|
+
* alfa&beta&gamma::QS → ["alfa", "beta", "gamma"]
|
|
8
|
+
*
|
|
9
|
+
* Rules:
|
|
10
|
+
* - All items with = → object
|
|
11
|
+
* - All items without = → array
|
|
12
|
+
* - Mixed → error (use ::JS embedded for complex structures)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fromTytx } from './decode.js';
|
|
16
|
+
import { rawEncode } from './utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encode a JavaScript object or array to TYTX QS string.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object|Array} value - Object with scalar values or array of strings
|
|
22
|
+
* @returns {string} QS string with typed values marked (without ::QS suffix)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* toQs({"alfa": 33, "date": new Date(Date.UTC(2025, 11, 14))})
|
|
26
|
+
* // 'alfa=33::L&date=2025-12-14::D'
|
|
27
|
+
*
|
|
28
|
+
* toQs(["alfa", "beta", "gamma"])
|
|
29
|
+
* // 'alfa&beta&gamma'
|
|
30
|
+
*/
|
|
31
|
+
function toQs(value) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
return value.map(item => String(item)).join('&');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (value !== null && typeof value === 'object') {
|
|
37
|
+
const parts = [];
|
|
38
|
+
for (const [k, v] of Object.entries(value)) {
|
|
39
|
+
const [encoded, result] = rawEncode(v, true); // force_suffix=true
|
|
40
|
+
if (encoded) {
|
|
41
|
+
parts.push(`${k}=${result}`);
|
|
42
|
+
} else {
|
|
43
|
+
parts.push(`${k}=${v}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return parts.join('&');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw new TypeError(`toQs expects object or array, got ${typeof value}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Decode a TYTX QS string to JavaScript object or array.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} data - QS string (without ::QS suffix)
|
|
56
|
+
* @returns {Object|Array} Object if all items have =, array if none have =
|
|
57
|
+
* @throws {Error} If mixed (some with =, some without)
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* fromQs('alfa=33::L&date=2025-12-14::D')
|
|
61
|
+
* // {"alfa": 33, "date": Date(2025, 11, 14)}
|
|
62
|
+
*
|
|
63
|
+
* fromQs('alfa&beta&gamma')
|
|
64
|
+
* // ["alfa", "beta", "gamma"]
|
|
65
|
+
*/
|
|
66
|
+
function fromQs(data) {
|
|
67
|
+
|
|
68
|
+
if (!data) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parts = data.split('&');
|
|
73
|
+
const hasEq = parts.map(p => p.includes('='));
|
|
74
|
+
|
|
75
|
+
const allWithEq = hasEq.every(Boolean);
|
|
76
|
+
const noneWithEq = !hasEq.some(Boolean);
|
|
77
|
+
|
|
78
|
+
if (!allWithEq && !noneWithEq) {
|
|
79
|
+
throw new Error("QS format error: mixed items with and without '='");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (noneWithEq) {
|
|
83
|
+
// Array mode: decode each item
|
|
84
|
+
return parts.map(p => fromTytx(p));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Object mode: split key=value and decode values
|
|
88
|
+
const result = {};
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
const eqIndex = part.indexOf('=');
|
|
91
|
+
const key = part.slice(0, eqIndex);
|
|
92
|
+
const value = part.slice(eqIndex + 1);
|
|
93
|
+
result[key] = fromTytx(value);
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { toQs, fromQs };
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// Copyright 2025 Softwell S.r.l. - Licensed under Apache License 2.0
|
|
2
|
+
/**
|
|
3
|
+
* Type Registry for TYTX Base.
|
|
4
|
+
*
|
|
5
|
+
* Maps JavaScript types to/from TYTX suffixes.
|
|
6
|
+
* Only scalar types are supported in base version.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { fromQs } from './qs.js';
|
|
10
|
+
import { BigJS, DecimalJS } from './platform/dependencies.js';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// DECIMAL LIBRARY DETECTION
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
// Import all decimal libraries at startup
|
|
17
|
+
// Current active class and library name
|
|
18
|
+
let DecimalClass = DecimalJS || BigJS || Number;
|
|
19
|
+
let decimalLibrary = DecimalJS ? 'decimal.js' : BigJS ? 'big.js' : 'number';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Set the decimal library to use.
|
|
23
|
+
* @param {'decimal.js'|'big.js'|'number'} name
|
|
24
|
+
*/
|
|
25
|
+
function setDecimalLibrary(name) {
|
|
26
|
+
if (name === 'decimal.js' && DecimalJS) {
|
|
27
|
+
DecimalClass = DecimalJS;
|
|
28
|
+
decimalLibrary = 'decimal.js';
|
|
29
|
+
} else if (name === 'big.js' && BigJS) {
|
|
30
|
+
DecimalClass = BigJS;
|
|
31
|
+
decimalLibrary = 'big.js';
|
|
32
|
+
} else {
|
|
33
|
+
DecimalClass = Number;
|
|
34
|
+
decimalLibrary = 'number';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get current decimal library name.
|
|
40
|
+
* @returns {'decimal.js'|'big.js'|'number'}
|
|
41
|
+
*/
|
|
42
|
+
function getDecimalLibrary() {
|
|
43
|
+
return decimalLibrary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create a Decimal value using the current library.
|
|
48
|
+
* @param {string|number} value
|
|
49
|
+
* @returns {Decimal|Big|number}
|
|
50
|
+
*/
|
|
51
|
+
function createDecimal(value) {
|
|
52
|
+
return new DecimalClass(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a value is a Decimal instance.
|
|
57
|
+
* @param {any} value
|
|
58
|
+
* @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function isDecimal(value) {
|
|
61
|
+
if (decimalLibrary === 'number') {
|
|
62
|
+
return false; // Cannot distinguish from regular Number
|
|
63
|
+
}
|
|
64
|
+
return value instanceof DecimalClass;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =============================================================================
|
|
68
|
+
// DATE TYPE DETECTION
|
|
69
|
+
// =============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Determine the TYTX type for a Date object based on its content.
|
|
73
|
+
* @param {Date} d
|
|
74
|
+
* @returns {'D'|'H'|'DHZ'}
|
|
75
|
+
*/
|
|
76
|
+
function getDateType(d) {
|
|
77
|
+
const isEpochDate = d.getUTCFullYear() === 1970 &&
|
|
78
|
+
d.getUTCMonth() === 0 &&
|
|
79
|
+
d.getUTCDate() === 1;
|
|
80
|
+
const isMidnight = d.getUTCHours() === 0 &&
|
|
81
|
+
d.getUTCMinutes() === 0 &&
|
|
82
|
+
d.getUTCSeconds() === 0 &&
|
|
83
|
+
d.getUTCMilliseconds() === 0;
|
|
84
|
+
|
|
85
|
+
if (isEpochDate && !isMidnight) return 'H'; // time only
|
|
86
|
+
if (isMidnight && !isEpochDate) return 'D'; // date only
|
|
87
|
+
return 'DHZ'; // full datetime
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// =============================================================================
|
|
91
|
+
// SERIALIZERS (JavaScript type -> string)
|
|
92
|
+
// =============================================================================
|
|
93
|
+
|
|
94
|
+
function _serializeDecimal(v) {
|
|
95
|
+
return v.toString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function _serializeDate(v) {
|
|
99
|
+
// Format: YYYY-MM-DD
|
|
100
|
+
const year = v.getUTCFullYear();
|
|
101
|
+
const month = String(v.getUTCMonth() + 1).padStart(2, '0');
|
|
102
|
+
const day = String(v.getUTCDate()).padStart(2, '0');
|
|
103
|
+
return `${year}-${month}-${day}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _serializeDatetime(v) {
|
|
107
|
+
// Format: YYYY-MM-DDTHH:MM:SS.mmmZ (millisecond precision)
|
|
108
|
+
return v.toISOString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _serializeTime(v) {
|
|
112
|
+
// Format: HH:MM:SS.mmm
|
|
113
|
+
const hours = String(v.getUTCHours()).padStart(2, '0');
|
|
114
|
+
const minutes = String(v.getUTCMinutes()).padStart(2, '0');
|
|
115
|
+
const seconds = String(v.getUTCSeconds()).padStart(2, '0');
|
|
116
|
+
const millis = String(v.getUTCMilliseconds()).padStart(3, '0');
|
|
117
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _serializeBool(v) {
|
|
121
|
+
return v ? 'true' : 'false';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _serializeInt(v) {
|
|
125
|
+
return v.toString();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _serializeFloat(v) {
|
|
129
|
+
return v.toString();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _serializeRaw(v) {
|
|
133
|
+
// Standard base64 (RFC 4648, padded), built from a binary string in
|
|
134
|
+
// chunks so a large view does not overflow the argument list. Text
|
|
135
|
+
// transports only: msgpack carries bytes as its native bin type.
|
|
136
|
+
let binary = '';
|
|
137
|
+
for (let i = 0; i < v.length; i += 0x8000) {
|
|
138
|
+
binary += String.fromCharCode.apply(null, v.subarray(i, i + 0x8000));
|
|
139
|
+
}
|
|
140
|
+
return btoa(binary);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// TYPE REGISTRY
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
// Type detection and serialization
|
|
148
|
+
// For JS we need functions to detect types since we can't use type() like Python
|
|
149
|
+
|
|
150
|
+
// Custom types registered at runtime: [cls, suffix, serializer, jsonNative].
|
|
151
|
+
let CUSTOM_TYPES = [];
|
|
152
|
+
|
|
153
|
+
// Suffix -> subtype dictionary. TYTX stores it and never reads it: the type
|
|
154
|
+
// that owns the suffix decides its content (for "X": symbolic name -> class).
|
|
155
|
+
const SUBTYPE_DICTS = new Map();
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the registered custom-type entry for a value, or null.
|
|
159
|
+
*
|
|
160
|
+
* The prototype chain is walked from the value up, mirroring Python's MRO
|
|
161
|
+
* lookup: the exact class wins, otherwise the nearest registered ancestor,
|
|
162
|
+
* so an unregistered subclass travels under that ancestor's suffix.
|
|
163
|
+
*
|
|
164
|
+
* @param {any} value
|
|
165
|
+
* @returns {[string, function, boolean]|null} [suffix, serializer, jsonNative] or null
|
|
166
|
+
*/
|
|
167
|
+
function getCustomTypeEntry(value) {
|
|
168
|
+
if (value === null || typeof value !== 'object') {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
for (let proto = Object.getPrototypeOf(value); proto !== null; proto = Object.getPrototypeOf(proto)) {
|
|
172
|
+
for (const [cls, suffix, serializer, jsonNative] of CUSTOM_TYPES) {
|
|
173
|
+
if (cls.prototype === proto) {
|
|
174
|
+
return [suffix, serializer, jsonNative];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get type entry for a value.
|
|
183
|
+
* @param {any} value
|
|
184
|
+
* @returns {[string, function, boolean]|null} [suffix, serializer, jsonNative] or null
|
|
185
|
+
*/
|
|
186
|
+
function getTypeEntry(value) {
|
|
187
|
+
if (value === null) {
|
|
188
|
+
return ['NN', () => '', true];
|
|
189
|
+
}
|
|
190
|
+
if (isDecimal(value)) {
|
|
191
|
+
return ['N', _serializeDecimal, false];
|
|
192
|
+
}
|
|
193
|
+
if (value instanceof Uint8Array) {
|
|
194
|
+
// Node's Buffer is a Uint8Array too, so it travels as RAW as well.
|
|
195
|
+
return ['RAW', _serializeRaw, false];
|
|
196
|
+
}
|
|
197
|
+
if (value instanceof Date) {
|
|
198
|
+
const dateType = getDateType(value);
|
|
199
|
+
if (dateType === 'D') {
|
|
200
|
+
return ['D', _serializeDate, false];
|
|
201
|
+
} else if (dateType === 'H') {
|
|
202
|
+
return ['H', _serializeTime, false];
|
|
203
|
+
} else {
|
|
204
|
+
return ['DHZ', _serializeDatetime, false];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (typeof value === 'boolean') {
|
|
208
|
+
return ['B', _serializeBool, true];
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === 'number') {
|
|
211
|
+
if (Number.isInteger(value)) {
|
|
212
|
+
return ['L', _serializeInt, true];
|
|
213
|
+
} else {
|
|
214
|
+
return ['R', _serializeFloat, true];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return getCustomTypeEntry(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// DESERIALIZERS (string -> JavaScript type)
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
function _deserializeDecimal(s) {
|
|
225
|
+
return createDecimal(s);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function _deserializeDate(s) {
|
|
229
|
+
// Input: YYYY-MM-DD
|
|
230
|
+
const [year, month, day] = s.split('-').map(Number);
|
|
231
|
+
return new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function _deserializeDatetime(s) {
|
|
235
|
+
return new Date(s);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function _deserializeTime(s) {
|
|
239
|
+
// Input: HH:MM:SS.mmm
|
|
240
|
+
const [h, m, rest] = s.split(':');
|
|
241
|
+
const [sec, ms] = rest.split('.');
|
|
242
|
+
return new Date(Date.UTC(1970, 0, 1, Number(h), Number(m), Number(sec), Number(ms || 0)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function _deserializeBool(s) {
|
|
246
|
+
return s.toLowerCase() === 'true';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function _deserializeInt(s) {
|
|
250
|
+
return parseInt(s, 10);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _deserializeFloat(s) {
|
|
254
|
+
return parseFloat(s);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function _deserializeStr(s) {
|
|
258
|
+
return s;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function _deserializeNone(s) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Standard base64 with padding, whole string: atob alone accepts missing
|
|
266
|
+
// padding and whitespace, which Python's strict decoder refuses.
|
|
267
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
268
|
+
|
|
269
|
+
function _deserializeRaw(s) {
|
|
270
|
+
if (!BASE64_PATTERN.test(s)) {
|
|
271
|
+
throw new Error(`RAW payload is not standard padded base64: '${s}'`);
|
|
272
|
+
}
|
|
273
|
+
const binary = atob(s);
|
|
274
|
+
const out = new Uint8Array(binary.length);
|
|
275
|
+
for (let i = 0; i < binary.length; i++) {
|
|
276
|
+
out[i] = binary.charCodeAt(i);
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function _deserializeQs(s) {
|
|
282
|
+
return fromQs(s);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// A type code is one or more uppercase ASCII letters ("N", "QS", "DHZ"). No
|
|
286
|
+
// length limit. The grammar rules out ":" so a code can never be confused with
|
|
287
|
+
// the "::" suffix separator or with the ":" that splits the msgpack ext-4
|
|
288
|
+
// payload.
|
|
289
|
+
const SUFFIX_PATTERN = /^[A-Z]+$/;
|
|
290
|
+
|
|
291
|
+
// Suffix -> [type, deserializer] - includes all for decoding
|
|
292
|
+
// Accepts both DH (deprecated) and DHZ (canonical) for datetime
|
|
293
|
+
const SUFFIX_TO_TYPE = {
|
|
294
|
+
'N': [Object, _deserializeDecimal], // Object as placeholder for Decimal type
|
|
295
|
+
'D': [Date, _deserializeDate],
|
|
296
|
+
'DH': [Date, _deserializeDatetime], // deprecated, still accepted
|
|
297
|
+
'DHZ': [Date, _deserializeDatetime], // canonical
|
|
298
|
+
'H': [Date, _deserializeTime],
|
|
299
|
+
'L': [Number, _deserializeInt],
|
|
300
|
+
'R': [Number, _deserializeFloat],
|
|
301
|
+
'T': [String, _deserializeStr],
|
|
302
|
+
'B': [Boolean, _deserializeBool],
|
|
303
|
+
'QS': [Object, _deserializeQs],
|
|
304
|
+
'NN': [null, _deserializeNone],
|
|
305
|
+
'RAW': [Uint8Array, _deserializeRaw],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// =============================================================================
|
|
309
|
+
// CUSTOM TYPE REGISTRATION
|
|
310
|
+
// =============================================================================
|
|
311
|
+
|
|
312
|
+
/** Return the constructor registered for a suffix, without running its decoder. */
|
|
313
|
+
function getRegisteredType(suffix) {
|
|
314
|
+
return Object.hasOwn(SUFFIX_TO_TYPE, suffix) ? SUFFIX_TO_TYPE[suffix][0] : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Register a custom type for TYTX serialization.
|
|
319
|
+
*
|
|
320
|
+
* Lets external code extend TYTX with its own types. The encode side matches
|
|
321
|
+
* the exact constructor first; an unregistered subclass travels under the
|
|
322
|
+
* suffix of its nearest registered ancestor, written by the serializer (for
|
|
323
|
+
* registerClass, the subclass's own toTytx). The concrete class of a subclass
|
|
324
|
+
* is the type's own business, carried through its subtype dictionary
|
|
325
|
+
* (setSubtypeDict). The decode side maps the suffix back via SUFFIX_TO_TYPE.
|
|
326
|
+
* Re-registering the same class replaces its hooks; reusing a suffix owned by
|
|
327
|
+
* a different type throws.
|
|
328
|
+
*
|
|
329
|
+
* @param {Function} cls - the class/constructor to register
|
|
330
|
+
* @param {string} suffix - the TYTX suffix: uppercase ASCII letters (e.g. "X")
|
|
331
|
+
* @param {function(any): string} serializer - instance -> string
|
|
332
|
+
* @param {function(string): any} deserializer - string -> instance
|
|
333
|
+
* @param {boolean} [jsonNative=false] - if true, skip suffix when JSON-native
|
|
334
|
+
* @throws {Error} if the suffix does not match SUFFIX_PATTERN, or is already
|
|
335
|
+
* registered for a different type
|
|
336
|
+
*/
|
|
337
|
+
function registerType(cls, suffix, serializer, deserializer, jsonNative = false) {
|
|
338
|
+
if (typeof suffix !== 'string' || !SUFFIX_PATTERN.test(suffix)) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`TYTX suffix '${suffix}' is invalid: expected uppercase ASCII letters only`);
|
|
341
|
+
}
|
|
342
|
+
const existing = SUFFIX_TO_TYPE[suffix];
|
|
343
|
+
if (existing !== undefined && existing[0] !== cls) {
|
|
344
|
+
const owner = existing[0] === null ? 'null' : existing[0].name;
|
|
345
|
+
throw new Error(`TYTX suffix '${suffix}' is already registered for ${owner}`);
|
|
346
|
+
}
|
|
347
|
+
// Replace semantics: drop any previous entry for the same class so the
|
|
348
|
+
// encode loop cannot keep serving stale hooks.
|
|
349
|
+
CUSTOM_TYPES = CUSTOM_TYPES.filter(([c]) => c !== cls);
|
|
350
|
+
CUSTOM_TYPES.push([cls, suffix, serializer, jsonNative]);
|
|
351
|
+
SUFFIX_TO_TYPE[suffix] = [cls, deserializer];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Register a class that declares its own TYTX hooks.
|
|
356
|
+
*
|
|
357
|
+
* Reads from the class:
|
|
358
|
+
* static tytxSuffix: the TYTX suffix (e.g. "X")
|
|
359
|
+
* toTytx(): instance -> string
|
|
360
|
+
* static fromTytx(s): string -> instance (must be static: decode starts
|
|
361
|
+
* from the suffix and rebuilds the instance from scratch)
|
|
362
|
+
* static tytxJsonNative: optional boolean, default false
|
|
363
|
+
*
|
|
364
|
+
* @param {Function} cls
|
|
365
|
+
* @returns {Function} the class, so it can be used as a decorator
|
|
366
|
+
*/
|
|
367
|
+
function registerClass(cls) {
|
|
368
|
+
if (!cls.tytxSuffix) {
|
|
369
|
+
throw new Error(`registerClass: ${cls.name} is missing a static tytxSuffix`);
|
|
370
|
+
}
|
|
371
|
+
if (typeof cls.prototype?.toTytx !== 'function') {
|
|
372
|
+
throw new Error(`registerClass: ${cls.name} is missing a toTytx method`);
|
|
373
|
+
}
|
|
374
|
+
if (typeof cls.fromTytx !== 'function') {
|
|
375
|
+
throw new Error(`registerClass: ${cls.name} is missing a static fromTytx method`);
|
|
376
|
+
}
|
|
377
|
+
registerType(
|
|
378
|
+
cls,
|
|
379
|
+
cls.tytxSuffix,
|
|
380
|
+
obj => obj.toTytx(),
|
|
381
|
+
s => cls.fromTytx(s),
|
|
382
|
+
cls.tytxJsonNative || false,
|
|
383
|
+
);
|
|
384
|
+
return cls;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Store the subtype dictionary of a suffix, replacing the previous one.
|
|
389
|
+
*
|
|
390
|
+
* Nothing is checked: the suffix need not be registered and the content is up
|
|
391
|
+
* to the type that owns the suffix. To extend it, read it with getSubtypeDict,
|
|
392
|
+
* add the entries and set it again.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} suffix
|
|
395
|
+
* @param {Object} subtypes
|
|
396
|
+
*/
|
|
397
|
+
function setSubtypeDict(suffix, subtypes) {
|
|
398
|
+
SUBTYPE_DICTS.set(suffix, subtypes);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Return the subtype dictionary stored for a suffix, or {} if none was set.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} suffix
|
|
405
|
+
* @returns {Object}
|
|
406
|
+
*/
|
|
407
|
+
function getSubtypeDict(suffix) {
|
|
408
|
+
return SUBTYPE_DICTS.has(suffix) ? SUBTYPE_DICTS.get(suffix) : {};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Remove all custom type registrations and subtype dictionaries (test helper).
|
|
413
|
+
*/
|
|
414
|
+
function _resetCustomTypes() {
|
|
415
|
+
for (const [, suffix] of CUSTOM_TYPES) {
|
|
416
|
+
delete SUFFIX_TO_TYPE[suffix];
|
|
417
|
+
}
|
|
418
|
+
CUSTOM_TYPES = [];
|
|
419
|
+
SUBTYPE_DICTS.clear();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export {
|
|
423
|
+
// Decimal utilities
|
|
424
|
+
decimalLibrary,
|
|
425
|
+
createDecimal,
|
|
426
|
+
isDecimal,
|
|
427
|
+
setDecimalLibrary,
|
|
428
|
+
getDecimalLibrary,
|
|
429
|
+
// Date type detection
|
|
430
|
+
getDateType,
|
|
431
|
+
// Type registry
|
|
432
|
+
getTypeEntry,
|
|
433
|
+
getRegisteredType,
|
|
434
|
+
getCustomTypeEntry,
|
|
435
|
+
SUFFIX_TO_TYPE,
|
|
436
|
+
SUFFIX_PATTERN,
|
|
437
|
+
// Custom type registration
|
|
438
|
+
registerType,
|
|
439
|
+
registerClass,
|
|
440
|
+
setSubtypeDict,
|
|
441
|
+
getSubtypeDict,
|
|
442
|
+
_resetCustomTypes,
|
|
443
|
+
// Serializers (exported for testing)
|
|
444
|
+
_serializeDecimal,
|
|
445
|
+
_serializeDate,
|
|
446
|
+
_serializeDatetime,
|
|
447
|
+
_serializeTime,
|
|
448
|
+
_serializeBool,
|
|
449
|
+
_serializeInt,
|
|
450
|
+
_serializeFloat,
|
|
451
|
+
_serializeRaw,
|
|
452
|
+
// Deserializers (exported for testing)
|
|
453
|
+
_deserializeDecimal,
|
|
454
|
+
_deserializeDate,
|
|
455
|
+
_deserializeDatetime,
|
|
456
|
+
_deserializeTime,
|
|
457
|
+
_deserializeBool,
|
|
458
|
+
_deserializeInt,
|
|
459
|
+
_deserializeFloat,
|
|
460
|
+
_deserializeStr,
|
|
461
|
+
_deserializeNone,
|
|
462
|
+
_deserializeRaw,
|
|
463
|
+
};
|