@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.
Files changed (48) hide show
  1. package/benchmark-small.js +81 -0
  2. package/dist/common/decoder.d.ts +8 -1
  3. package/dist/common/decoder.js +29 -15
  4. package/dist/common/decoder.js.map +1 -1
  5. package/dist/common/encoder.d.ts +1 -3
  6. package/dist/common/encoder.js +33 -11
  7. package/dist/common/encoder.js.map +1 -1
  8. package/dist/decoder.d.ts +1 -1
  9. package/dist/decoder.js.map +1 -1
  10. package/dist/encoder.d.ts +9 -1
  11. package/dist/encoder.js +41 -20
  12. package/dist/encoder.js.map +1 -1
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.js +4 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/rxjs/decoder.js +5 -2
  17. package/dist/rxjs/decoder.js.map +1 -1
  18. package/dist/rxjs/encoder.js +5 -4
  19. package/dist/rxjs/encoder.js.map +1 -1
  20. package/dist/stream/encoder.d.ts +3 -0
  21. package/dist/stream/encoder.js +8 -2
  22. package/dist/stream/encoder.js.map +1 -1
  23. package/dist/stream-helper/encoder.d.ts +10 -2
  24. package/dist/stream-helper/encoder.js +50 -24
  25. package/dist/stream-helper/encoder.js.map +1 -1
  26. package/dist/utils.d.ts +1 -1
  27. package/dist/utils.js +1 -4
  28. package/dist/utils.js.map +1 -1
  29. package/package.json +6 -15
  30. package/src/common/decoder.ts +36 -19
  31. package/src/common/encoder.ts +32 -10
  32. package/src/decoder.ts +1 -1
  33. package/src/encoder.ts +44 -21
  34. package/src/index.ts +7 -6
  35. package/src/rxjs/decoder.ts +6 -2
  36. package/src/rxjs/encoder.ts +5 -4
  37. package/src/stream/encoder.ts +10 -2
  38. package/src/stream-helper/encoder.ts +48 -26
  39. package/src/utils.ts +1 -4
  40. package/tests/decode.js +29 -9
  41. package/tests/e2e.js +3 -0
  42. package/tests/encode.js +173 -2
  43. package/tests/huge-string.js +3 -0
  44. package/tests/rxjs/decode.js +10 -8
  45. package/tests/rxjs/encode.js +6 -2
  46. package/tests/stream/decode.js +25 -8
  47. package/tests/stream/encode.js +13 -2
  48. package/tests/string-encoding.js +2 -2
@@ -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 onChunk = (chunk: Uint8Array): void => subscriber.next(chunk);
9
- return observable.subscribe({
8
+ const helper = new StreamEncoderHelper((chunk: Uint8Array): void => subscriber.next(chunk));
9
+ const sub = observable.subscribe({
10
10
  next(value) {
11
11
  try {
12
- const helper = new StreamEncoderHelper(value, onChunk);
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
  }
@@ -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
- value: unknown,
10
- protected readonly onChunk: (chunk: Uint8Array) => void,
11
- ) {
12
- super(value);
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
- this.onChunk(this.buffer.subarray(0, this.length));
30
- this.buffer = new Uint8Array(0);
31
- this.view = new DataView(this.buffer.buffer);
32
- this.length = 0;
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
- this.onChunk(this.buffer.subarray(0, this.length));
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.buffer = new Uint8Array(capacity);
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
- if (typeof this.value != 'object' || this.value == null || ArrayBuffer.isView(this.value)) {
50
- this.ensureCapacity(20);
51
- this.writeValue();
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, exact: boolean): Uint8Array {
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 [error]', () => {
58
- expect(() => decode(toBuffer('L', 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0))).toThrow();
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 [error]', () => {
160
- expect(() => decode(toBuffer('S', 'L', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 'x'))).toThrow(
161
- /Unsupported type int64/,
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
- decode(toBuffer('[', '$', 'L', '#', 'i', 1, 0x12, 0x34, 0x56, 0x78, 0xfe, 0xdc, 0xba, 0x98)),
394
- ).toThrow();
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(() => decode(toBuffer('[', '$', 'L', '#', 'i', 0))).toThrow();
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', () => {