@cloudpss/ubjson 0.5.10 → 0.5.12
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/benchmark-small.js +81 -0
- package/dist/common/decoder.d.ts +8 -1
- package/dist/common/decoder.js +29 -15
- package/dist/common/decoder.js.map +1 -1
- package/dist/common/encoder.d.ts +1 -3
- package/dist/common/encoder.js +33 -11
- package/dist/common/encoder.js.map +1 -1
- package/dist/decoder.d.ts +1 -1
- package/dist/decoder.js.map +1 -1
- package/dist/encoder.d.ts +9 -1
- package/dist/encoder.js +41 -20
- package/dist/encoder.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -5
- package/dist/index.js.map +1 -1
- package/dist/rxjs/decoder.js +5 -2
- package/dist/rxjs/decoder.js.map +1 -1
- package/dist/rxjs/encoder.js +5 -4
- package/dist/rxjs/encoder.js.map +1 -1
- package/dist/stream/encoder.d.ts +3 -0
- package/dist/stream/encoder.js +8 -2
- package/dist/stream/encoder.js.map +1 -1
- package/dist/stream-helper/encoder.d.ts +10 -2
- package/dist/stream-helper/encoder.js +50 -24
- package/dist/stream-helper/encoder.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +1 -4
- package/dist/utils.js.map +1 -1
- package/package.json +6 -15
- package/src/common/decoder.ts +36 -19
- package/src/common/encoder.ts +32 -10
- package/src/decoder.ts +1 -1
- package/src/encoder.ts +44 -21
- package/src/index.ts +7 -6
- package/src/rxjs/decoder.ts +6 -2
- package/src/rxjs/encoder.ts +5 -4
- package/src/stream/encoder.ts +10 -2
- package/src/stream-helper/encoder.ts +48 -26
- package/src/utils.ts +1 -4
- package/tests/decode.js +29 -9
- package/tests/e2e.js +3 -0
- package/tests/encode.js +173 -2
- package/tests/huge-string.js +3 -0
- package/tests/rxjs/decode.js +10 -8
- package/tests/rxjs/encode.js +6 -2
- package/tests/stream/decode.js +25 -8
- package/tests/stream/encode.js +13 -2
- package/tests/string-encoding.js +2 -2
package/src/rxjs/encoder.ts
CHANGED
|
@@ -5,12 +5,11 @@ import { StreamEncoderHelper } from '../stream-helper/encoder.js';
|
|
|
5
5
|
export function encode(): OperatorFunction<unknown, Uint8Array> {
|
|
6
6
|
return (observable) => {
|
|
7
7
|
return new Observable<Uint8Array>((subscriber) => {
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const helper = new StreamEncoderHelper((chunk: Uint8Array): void => subscriber.next(chunk));
|
|
9
|
+
const sub = observable.subscribe({
|
|
10
10
|
next(value) {
|
|
11
11
|
try {
|
|
12
|
-
|
|
13
|
-
helper.encode();
|
|
12
|
+
helper.encode(value);
|
|
14
13
|
} catch (ex) {
|
|
15
14
|
subscriber.error(ex as Error);
|
|
16
15
|
}
|
|
@@ -22,6 +21,8 @@ export function encode(): OperatorFunction<unknown, Uint8Array> {
|
|
|
22
21
|
subscriber.complete();
|
|
23
22
|
},
|
|
24
23
|
});
|
|
24
|
+
sub.add(() => helper.destroy());
|
|
25
|
+
return sub;
|
|
25
26
|
});
|
|
26
27
|
};
|
|
27
28
|
}
|
package/src/stream/encoder.ts
CHANGED
|
@@ -8,16 +8,24 @@ export class StreamEncoder extends Transform {
|
|
|
8
8
|
readableObjectMode: false,
|
|
9
9
|
writableObjectMode: true,
|
|
10
10
|
});
|
|
11
|
+
this.helper = new StreamEncoderHelper((binary) => this.push(binary));
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
private readonly helper;
|
|
15
|
+
|
|
13
16
|
/** @inheritdoc */
|
|
14
17
|
override _transform(obj: unknown, _encoding: BufferEncoding, callback: TransformCallback): void {
|
|
15
|
-
const helper = new StreamEncoderHelper(obj, (binary) => this.push(binary));
|
|
16
18
|
try {
|
|
17
|
-
helper.encode();
|
|
19
|
+
this.helper.encode(obj);
|
|
18
20
|
callback();
|
|
19
21
|
} catch (ex) {
|
|
20
22
|
callback(ex as Error);
|
|
21
23
|
}
|
|
22
24
|
}
|
|
25
|
+
|
|
26
|
+
/** @inheritdoc */
|
|
27
|
+
override _destroy(error: Error | null, callback: (error?: Error | null | undefined) => void): void {
|
|
28
|
+
this.helper.destroy();
|
|
29
|
+
super._destroy(error, callback);
|
|
30
|
+
}
|
|
23
31
|
}
|
|
@@ -3,14 +3,31 @@ import { EncoderBase } from '../common/encoder.js';
|
|
|
3
3
|
const BLOCK_SIZE = 1024 * 8; // 8 KiB
|
|
4
4
|
const MAX_SIZE = 1024 * 1024 * 256; // 256 MiB
|
|
5
5
|
|
|
6
|
+
/** 保存一个内存池以减少重复分配 */
|
|
7
|
+
let POOL: Uint8Array | null = null;
|
|
8
|
+
|
|
6
9
|
/** 流式编码 UBJSON */
|
|
7
10
|
export class StreamEncoderHelper extends EncoderBase {
|
|
8
|
-
constructor(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
constructor(protected readonly onChunk: (chunk: Uint8Array) => void) {
|
|
12
|
+
super();
|
|
13
|
+
if (POOL != null) {
|
|
14
|
+
this.pool = POOL;
|
|
15
|
+
POOL = null;
|
|
16
|
+
} else {
|
|
17
|
+
this.pool = new Uint8Array(BLOCK_SIZE);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 销毁实例,释放内存池
|
|
22
|
+
*/
|
|
23
|
+
destroy(): void {
|
|
24
|
+
POOL ??= this.pool;
|
|
25
|
+
const self = this as unknown as { pool: Uint8Array | null; buffer: Uint8Array | null };
|
|
26
|
+
self.pool = null;
|
|
27
|
+
self.buffer = null;
|
|
13
28
|
}
|
|
29
|
+
/** 通过内存池减少分配 */
|
|
30
|
+
private readonly pool;
|
|
14
31
|
/**
|
|
15
32
|
* 确保 buffer 还有 capacity 的空闲空间
|
|
16
33
|
*/
|
|
@@ -19,41 +36,46 @@ export class StreamEncoderHelper extends EncoderBase {
|
|
|
19
36
|
// 超过最大尺寸限制
|
|
20
37
|
throw new Error('Buffer has exceed max size');
|
|
21
38
|
}
|
|
22
|
-
if (this.buffer == null) {
|
|
23
|
-
this.buffer = new Uint8Array(capacity);
|
|
24
|
-
this.view = new DataView(this.buffer.buffer);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
39
|
if (capacity < 0) {
|
|
28
40
|
// 结束流
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if (this.buffer === this.pool) {
|
|
42
|
+
this.onChunk(this.buffer.slice(0, this.length));
|
|
43
|
+
} else {
|
|
44
|
+
this.onChunk(this.buffer.subarray(0, this.length));
|
|
45
|
+
}
|
|
33
46
|
return;
|
|
34
47
|
}
|
|
35
48
|
// 无需扩容
|
|
36
49
|
if (this.buffer.byteLength >= this.length + capacity) return;
|
|
37
50
|
|
|
38
51
|
// 提交目前的数据
|
|
39
|
-
|
|
52
|
+
if (this.buffer === this.pool) {
|
|
53
|
+
this.onChunk(this.buffer.slice(0, this.length));
|
|
54
|
+
} else {
|
|
55
|
+
this.onChunk(this.buffer.subarray(0, this.length));
|
|
56
|
+
}
|
|
40
57
|
|
|
41
58
|
// 重新分配缓冲区
|
|
42
59
|
if (capacity < BLOCK_SIZE) capacity = BLOCK_SIZE;
|
|
43
|
-
this.
|
|
60
|
+
this.allocUnsafe(capacity);
|
|
61
|
+
}
|
|
62
|
+
/** 分配 buffer */
|
|
63
|
+
private allocUnsafe(size: number): void {
|
|
64
|
+
if (size === this.pool.byteLength) {
|
|
65
|
+
// 从 pool 中获取
|
|
66
|
+
this.buffer = this.pool;
|
|
67
|
+
this.view = new DataView(this.buffer.buffer);
|
|
68
|
+
this.length = 0;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.buffer = new Uint8Array(size);
|
|
44
72
|
this.view = new DataView(this.buffer.buffer);
|
|
45
73
|
this.length = 0;
|
|
46
74
|
}
|
|
47
75
|
/** 获取写入结果 */
|
|
48
|
-
encode(): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.ensureCapacity(-1);
|
|
53
|
-
} else {
|
|
54
|
-
this.ensureCapacity(BLOCK_SIZE);
|
|
55
|
-
this.writeValue();
|
|
56
|
-
this.ensureCapacity(-1);
|
|
57
|
-
}
|
|
76
|
+
encode(value: unknown): void {
|
|
77
|
+
this.allocUnsafe(BLOCK_SIZE);
|
|
78
|
+
this.writeValue(value);
|
|
79
|
+
this.ensureCapacity(-1);
|
|
58
80
|
}
|
|
59
81
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
/** 支持的数据转为 Uint8Array */
|
|
2
|
-
export function toUint8Array(data: BinaryData
|
|
2
|
+
export function toUint8Array(data: BinaryData): Uint8Array {
|
|
3
3
|
if (data == null || typeof data != 'object' || typeof data.byteLength != 'number') {
|
|
4
4
|
throw new TypeError('Invalid data');
|
|
5
5
|
}
|
|
6
6
|
if (!ArrayBuffer.isView(data)) {
|
|
7
7
|
return new Uint8Array(data);
|
|
8
8
|
}
|
|
9
|
-
if (exact && (data.byteOffset !== 0 || data.buffer.byteLength !== data.byteLength)) {
|
|
10
|
-
return new Uint8Array(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
11
|
-
}
|
|
12
9
|
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
13
10
|
}
|
|
14
11
|
|
package/tests/decode.js
CHANGED
|
@@ -54,8 +54,9 @@ test('decode int32', () => {
|
|
|
54
54
|
expect(decode(toBuffer('l', 0x12, 0x34, 0x56, 0x78))).toBe(0x1234_5678);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test('decode int64
|
|
58
|
-
expect(
|
|
57
|
+
test('decode int64', () => {
|
|
58
|
+
expect(decode(toBuffer('L', 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x1234_5678_9abc_def0n);
|
|
59
|
+
expect(decode(toBuffer('L', 0x00, 0x04, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toBe(0x04_5678_9abc_def0);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
test('decode float32', () => {
|
|
@@ -156,9 +157,10 @@ test('decode huge string', () => {
|
|
|
156
157
|
expect(decode(toBuffer('S', 'l', 0x00, 0x00, 0x00, 6, 'u', 'b', 'j', 's', 'o', 'n'))).toBe('ubjson');
|
|
157
158
|
});
|
|
158
159
|
|
|
159
|
-
test('decode huge string [
|
|
160
|
-
expect(
|
|
161
|
-
|
|
160
|
+
test('decode huge string [int64 length]', () => {
|
|
161
|
+
expect(decode(toBuffer('S', 'L', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toBe('x');
|
|
162
|
+
expect(() => decode(toBuffer('S', 'L', 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toThrow(
|
|
163
|
+
/Invalid length/,
|
|
162
164
|
);
|
|
163
165
|
});
|
|
164
166
|
|
|
@@ -200,6 +202,24 @@ test('decode N-D array (strongly typed, optimized)', () => {
|
|
|
200
202
|
).toEqual([new Int8Array([1, 2, 3]), new Int8Array([4, 5, 6])]);
|
|
201
203
|
});
|
|
202
204
|
|
|
205
|
+
test('decode N-D array (strongly typed, optimized, no alloc buffer)', () => {
|
|
206
|
+
const buf = toBuffer('[', '$', '[', '#', 'i', 2, '$', 'i', '#', 'i', 3, 1, 2, 3, '$', 'U', '#', 'U', 3, 4, 5, 6);
|
|
207
|
+
|
|
208
|
+
const decoded1 = /** @type {[Int8Array, Uint8Array]} */ (decode(buf));
|
|
209
|
+
const decoded2 = /** @type {[Int8Array, Uint8Array]} */ (decode(buf, { noAllocBuffer: true }));
|
|
210
|
+
// @ts-expect-error Falsy value
|
|
211
|
+
const decoded3 = /** @type {[Int8Array, Uint8Array]} */ (decode(buf, { noAllocBuffer: 0 }));
|
|
212
|
+
expect(decoded1).toEqual([new Int8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]);
|
|
213
|
+
expect(decoded2).toEqual([new Int8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]);
|
|
214
|
+
expect(decoded3).toEqual([new Int8Array([1, 2, 3]), new Uint8Array([4, 5, 6])]);
|
|
215
|
+
expect(decoded1[0].buffer).not.toBe(buf.buffer);
|
|
216
|
+
expect(decoded1[1].buffer).not.toBe(buf.buffer);
|
|
217
|
+
expect(decoded2[0].buffer).not.toBe(buf.buffer);
|
|
218
|
+
expect(decoded2[1].buffer).toBe(buf.buffer);
|
|
219
|
+
expect(decoded3[0].buffer).not.toBe(buf.buffer);
|
|
220
|
+
expect(decoded3[1].buffer).not.toBe(buf.buffer);
|
|
221
|
+
});
|
|
222
|
+
|
|
203
223
|
test('decode array of objects (optimized)', () => {
|
|
204
224
|
expect(
|
|
205
225
|
decode(
|
|
@@ -389,13 +409,13 @@ test('decode array (int32, strongly typed, optimized) [use typed array]', () =>
|
|
|
389
409
|
});
|
|
390
410
|
|
|
391
411
|
test('decode array (int64, strongly typed, optimized) [use typed array]', () => {
|
|
392
|
-
expect(()
|
|
393
|
-
|
|
394
|
-
)
|
|
412
|
+
expect(decode(toBuffer('[', '$', 'L', '#', 'i', 1, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98))).toEqual(
|
|
413
|
+
new BigInt64Array([0x1234_5678_fedc_ba98n]),
|
|
414
|
+
);
|
|
395
415
|
});
|
|
396
416
|
|
|
397
417
|
test('decode array (int64, strongly typed, optimized, empty) [use typed array]', () => {
|
|
398
|
-
expect(
|
|
418
|
+
expect(decode(toBuffer('[', '$', 'L', '#', 'i', 0))).toEqual(new BigInt64Array([]));
|
|
399
419
|
});
|
|
400
420
|
|
|
401
421
|
test('decode array (float32, strongly typed, optimized) [use typed array]', () => {
|
package/tests/e2e.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Tests from https://bitbucket.org/shelacek/ubjson
|
|
3
3
|
*/
|
|
4
4
|
import { encode, decode } from '../dist/index.js';
|
|
5
|
+
import { resetEncoder } from '../dist/encoder.js';
|
|
5
6
|
|
|
6
7
|
test('encode/decode complex object', () => {
|
|
7
8
|
const expected = {
|
|
@@ -301,6 +302,7 @@ test('encode/decode complex object (no encodeInto)', () => {
|
|
|
301
302
|
const encodeInto = TextEncoder.prototype.encodeInto;
|
|
302
303
|
// @ts-expect-error 移除 encodeInto 以测试兼容性
|
|
303
304
|
TextEncoder.prototype.encodeInto = undefined;
|
|
305
|
+
resetEncoder();
|
|
304
306
|
const expected = {
|
|
305
307
|
hello: 'world',
|
|
306
308
|
from: ['UBJSON'],
|
|
@@ -372,6 +374,7 @@ test('encode/decode complex object (no encodeInto)', () => {
|
|
|
372
374
|
const actual = decode(encoded);
|
|
373
375
|
expect(actual).toEqual(expected);
|
|
374
376
|
TextEncoder.prototype.encodeInto = encodeInto;
|
|
377
|
+
resetEncoder();
|
|
375
378
|
});
|
|
376
379
|
|
|
377
380
|
test('encode/decode large array', () => {
|