Skip to content

Commit 38ad537

Browse files
committed
Added support for custom encoding/decoding context for keeping track of objects
1 parent 908d093 commit 38ad537

File tree

9 files changed

+239
-54
lines changed

9 files changed

+239
-54
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ deepStrictEqual(decode(encoded), object);
5050
- [`decodeArrayStream(stream: AsyncIterable<ArrayLike<number>> | ReadableStream<ArrayLike<number>>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodearraystreamstream-asynciterablearraylikenumber--readablestreamarraylikenumber-options-decodeasyncoptions-asynciterableunknown)
5151
- [`decodeStream(stream: AsyncIterable<ArrayLike<number>> | ReadableStream<ArrayLike<number>>, options?: DecodeAsyncOptions): AsyncIterable<unknown>`](#decodestreamstream-asynciterablearraylikenumber--readablestreamarraylikenumber-options-decodeasyncoptions-asynciterableunknown)
5252
- [Extension Types](#extension-types)
53+
- [Codec context](#codec-context)
5354
- [Handling BigInt with ExtensionCodec](#handling-bigint-with-extensioncodec)
5455
- [The temporal module as timestamp extensions](#the-temporal-module-as-timestamp-extensions)
5556
- [MessagePack Specification](#messagepack-specification)
@@ -115,6 +116,7 @@ maxDepth | number | `100`
115116
initialBufferSize | number | `2048`
116117
sortKeys | boolean | false
117118
forceFloat32 | boolean | false
119+
context | user-defined | -
118120

119121
### `decode(buffer: ArrayLike<number> | ArrayBuffer, options?: DecodeOptions): unknown`
120122

@@ -144,6 +146,7 @@ maxBinLength | number | `4_294_967_295` (UINT32_MAX)
144146
maxArrayLength | number | `4_294_967_295` (UINT32_MAX)
145147
maxMapLength | number | `4_294_967_295` (UINT32_MAX)
146148
maxExtLength | number | `4_294_967_295` (UINT32_MAX)
149+
context | user-defined | -
147150

148151
You can use `max${Type}Length` to limit the length of each type decoded.
149152

@@ -261,6 +264,50 @@ const decoded = decode(encoded, { extensionCodec });
261264

262265
Not that extension types for custom objects must be `[0, 127]`, while `[-1, -128]` is reserved for MessagePack itself.
263266

267+
#### Codec context
268+
269+
When using an extension codec, it may be necessary to keep encoding/decoding state, to keep track of which objects got encoded/re-created. To do this, pass a `context` to the `EncodeOptions` and `DecodeOptions` (and if using typescript, type the `ExtensionCodec` too). Don't forget to pass the `{extensionCodec, context}` along recursive encoding/decoding:
270+
271+
```typescript
272+
import { encode, decode, ExtensionCodec } from "@msgpack/msgpack";
273+
274+
class MyContext {
275+
track(object: any) { /*...*/ }
276+
}
277+
278+
class MyType { /* ... */ }
279+
280+
const extensionCodec = new ExtensionCodec<MyContext>();
281+
282+
// MyType
283+
const MYTYPE_EXT_TYPE = 0 // Any in 0-127
284+
extensionCodec.register({
285+
type: MYTYPE_EXT_TYPE,
286+
encode: (object, context) => {
287+
if (object instanceof MyType) {
288+
context.track(object); // <-- like this
289+
return encode(object.toJSON(), { extensionCodec, context });
290+
} else {
291+
return null;
292+
}
293+
},
294+
decode: (data, extType, context) => {
295+
const decoded = decode(data, { extensionCodec, context });
296+
const my = new MyType(decoded);
297+
context.track(my); // <-- and like this
298+
return my;
299+
},
300+
});
301+
302+
// and later
303+
import { encode, decode } from "@msgpack/msgpack";
304+
305+
const context = new MyContext();
306+
307+
const encoded = = encode({myType: new MyType<any>()}, { extensionCodec, context });
308+
const decoded = decode(encoded, { extensionCodec, context });
309+
```
310+
264311
#### Handling BigInt with ExtensionCodec
265312

266313
This library does not handle BigInt by default, but you can handle it with `ExtensionCodec` like this:

src/Decoder.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { prettyByte } from "./utils/prettyByte";
2-
import { ExtensionCodec } from "./ExtensionCodec";
2+
import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec";
33
import { getInt64, getUint64 } from "./utils/int";
44
import { utf8DecodeJs, TEXT_ENCODING_AVAILABLE, TEXT_DECODER_THRESHOLD, utf8DecodeTD } from "./utils/utf8";
55
import { createDataView, ensureUint8Array } from "./utils/typedArrays";
@@ -60,7 +60,7 @@ const DEFAULT_MAX_LENGTH = 0xffff_ffff; // uint32_max
6060

6161
const sharedCachedKeyDecoder = new CachedKeyDecoder();
6262

63-
export class Decoder {
63+
export class Decoder<ContextType> {
6464
totalPos = 0;
6565
pos = 0;
6666

@@ -70,7 +70,8 @@ export class Decoder {
7070
readonly stack: Array<StackState> = [];
7171

7272
constructor(
73-
readonly extensionCodec = ExtensionCodec.defaultCodec,
73+
readonly extensionCodec: ExtensionCodecType<ContextType> = ExtensionCodec.defaultCodec as any,
74+
readonly context: ContextType,
7475
readonly maxStrLength = DEFAULT_MAX_LENGTH,
7576
readonly maxBinLength = DEFAULT_MAX_LENGTH,
7677
readonly maxArrayLength = DEFAULT_MAX_LENGTH,
@@ -519,7 +520,7 @@ export class Decoder {
519520

520521
const extType = this.view.getInt8(this.pos + headOffset);
521522
const data = this.decodeBinary(size, headOffset + 1 /* extType */);
522-
return this.extensionCodec.decode(data, extType);
523+
return this.extensionCodec.decode(data, extType, this.context);
523524
}
524525

525526
lookU8() {

src/Encoder.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import { utf8EncodeJs, utf8Count, TEXT_ENCODING_AVAILABLE, TEXT_ENCODER_THRESHOLD, utf8EncodeTE } from "./utils/utf8";
2-
import { ExtensionCodec } from "./ExtensionCodec";
2+
import { ExtensionCodec, ExtensionCodecType } from "./ExtensionCodec";
33
import { setInt64, setUint64 } from "./utils/int";
44
import { ensureUint8Array } from "./utils/typedArrays";
55
import { ExtData } from "./ExtData";
66

77
export const DEFAULT_MAX_DEPTH = 100;
88
export const DEFAULT_INITIAL_BUFFER_SIZE = 2048;
99

10-
export class Encoder {
10+
export class Encoder<ContextType> {
1111
private pos = 0;
1212
private view = new DataView(new ArrayBuffer(this.initialBufferSize));
1313
private bytes = new Uint8Array(this.view.buffer);
1414

1515
constructor(
16-
readonly extensionCodec = ExtensionCodec.defaultCodec,
16+
readonly extensionCodec: ExtensionCodecType<ContextType> = ExtensionCodec.defaultCodec as any,
17+
readonly context: ContextType,
1718
readonly maxDepth = DEFAULT_MAX_DEPTH,
1819
readonly initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE,
1920
readonly sortKeys = false,
@@ -173,7 +174,7 @@ export class Encoder {
173174

174175
encodeObject(object: unknown, depth: number) {
175176
// try to encode objects with custom codec first of non-primitives
176-
const ext = this.extensionCodec.tryToEncode(object);
177+
const ext = this.extensionCodec.tryToEncode(object, this.context);
177178
if (ext != null) {
178179
this.encodeExtension(ext);
179180
} else if (Array.isArray(object)) {

src/ExtensionCodec.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,37 @@
33
import { ExtData } from "./ExtData";
44
import { timestampExtension } from "./timestamp";
55

6-
export type ExtensionDecoderType = (data: Uint8Array, extensionType: number) => unknown;
6+
export type ExtensionDecoderType<ContextType> = (
7+
data: Uint8Array,
8+
extensionType: number,
9+
context: ContextType,
10+
) => unknown;
711

8-
export type ExtensionEncoderType = (input: unknown) => Uint8Array | null;
12+
export type ExtensionEncoderType<ContextType> = (input: unknown, context: ContextType) => Uint8Array | null;
913

1014
// immutable interfce to ExtensionCodec
11-
export type ExtensionCodecType = {
12-
tryToEncode(object: unknown): ExtData | null;
13-
decode(data: Uint8Array, extType: number): unknown;
15+
export type ExtensionCodecType<ContextType> = {
16+
dummy: ContextType;
17+
tryToEncode(object: unknown, context: ContextType): ExtData | null;
18+
decode(data: Uint8Array, extType: number, context: ContextType): unknown;
1419
};
1520

16-
export class ExtensionCodec implements ExtensionCodecType {
17-
public static readonly defaultCodec: ExtensionCodecType = new ExtensionCodec();
21+
const typeDummy: any = undefined;
22+
23+
export class ExtensionCodec<ContextType = undefined> implements ExtensionCodecType<ContextType> {
24+
public static readonly defaultCodec: ExtensionCodecType<undefined> = new ExtensionCodec();
25+
26+
// ensures ExtensionCodecType<X> matches ExtensionCodec<X>
27+
// this will make type errors a lot more clear
28+
dummy: ContextType = typeDummy;
1829

1930
// built-in extensions
20-
private readonly builtInEncoders: Array<ExtensionEncoderType | undefined | null> = [];
21-
private readonly builtInDecoders: Array<ExtensionDecoderType | undefined | null> = [];
31+
private readonly builtInEncoders: Array<ExtensionEncoderType<ContextType> | undefined | null> = [];
32+
private readonly builtInDecoders: Array<ExtensionDecoderType<ContextType> | undefined | null> = [];
2233

2334
// custom extensions
24-
private readonly encoders: Array<ExtensionEncoderType | undefined | null> = [];
25-
private readonly decoders: Array<ExtensionDecoderType | undefined | null> = [];
35+
private readonly encoders: Array<ExtensionEncoderType<ContextType> | undefined | null> = [];
36+
private readonly decoders: Array<ExtensionDecoderType<ContextType> | undefined | null> = [];
2637

2738
public constructor() {
2839
this.register(timestampExtension);
@@ -34,8 +45,8 @@ export class ExtensionCodec implements ExtensionCodecType {
3445
decode,
3546
}: {
3647
type: number;
37-
encode: ExtensionEncoderType;
38-
decode: ExtensionDecoderType;
48+
encode: ExtensionEncoderType<ContextType>;
49+
decode: ExtensionDecoderType<ContextType>;
3950
}): void {
4051
if (type >= 0) {
4152
// custom extensions
@@ -49,12 +60,12 @@ export class ExtensionCodec implements ExtensionCodecType {
4960
}
5061
}
5162

52-
public tryToEncode(object: unknown): ExtData | null {
63+
public tryToEncode(object: unknown, context: ContextType): ExtData | null {
5364
// built-in extensions
5465
for (let i = 0; i < this.builtInEncoders.length; i++) {
5566
const encoder = this.builtInEncoders[i];
5667
if (encoder != null) {
57-
const data = encoder(object);
68+
const data = encoder(object, context);
5869
if (data != null) {
5970
const type = -1 - i;
6071
return new ExtData(type, data);
@@ -66,7 +77,7 @@ export class ExtensionCodec implements ExtensionCodecType {
6677
for (let i = 0; i < this.encoders.length; i++) {
6778
const encoder = this.encoders[i];
6879
if (encoder != null) {
69-
const data = encoder(object);
80+
const data = encoder(object, context);
7081
if (data != null) {
7182
const type = i;
7283
return new ExtData(type, data);
@@ -81,10 +92,10 @@ export class ExtensionCodec implements ExtensionCodecType {
8192
return null;
8293
}
8394

84-
public decode(data: Uint8Array, type: number): unknown {
95+
public decode(data: Uint8Array, type: number, context: ContextType): unknown {
8596
const decoder = type < 0 ? this.builtInDecoders[-1 - type] : this.decoders[type];
8697
if (decoder) {
87-
return decoder(data, type);
98+
return decoder(data, type, context);
8899
} else {
89100
// decode() does not fail, returns ExtData instead.
90101
return new ExtData(type, data);

src/context.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export type SplitTypes<T, U> = U extends T ? U : Exclude<T, U>;
2+
export type SplitUndefined<T> = SplitTypes<T, undefined>;
3+
4+
export type ContextOf<ContextType> = ContextType extends undefined
5+
? {}
6+
: {
7+
/**
8+
* Custom user-defined data, read/writable
9+
*/
10+
context: ContextType;
11+
};

src/decode.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ExtensionCodecType } from "./ExtensionCodec";
22
import { Decoder } from "./Decoder";
3+
import { ContextOf, SplitUndefined } from "./context";
34

4-
export type DecodeOptions = Partial<
5-
Readonly<{
6-
extensionCodec: ExtensionCodecType;
5+
export type DecodeOptions<ContextType = undefined> = Readonly<
6+
Partial<{
7+
extensionCodec: ExtensionCodecType<ContextType>;
78

89
/**
910
* Maximum string length.
@@ -31,7 +32,8 @@ export type DecodeOptions = Partial<
3132
*/
3233
maxExtLength: number;
3334
}>
34-
>;
35+
> &
36+
ContextOf<ContextType>;
3537

3638
export const defaultDecodeOptions: DecodeOptions = {};
3739

@@ -40,12 +42,13 @@ export const defaultDecodeOptions: DecodeOptions = {};
4042
*
4143
* This is a synchronous decoding function. See other variants for asynchronous decoding: `decodeAsync()`, `decodeStream()`, `decodeArrayStream()`.
4244
*/
43-
export function decode(
45+
export function decode<ContextType>(
4446
buffer: ArrayLike<number> | ArrayBuffer,
45-
options: DecodeOptions = defaultDecodeOptions,
47+
options: DecodeOptions<SplitUndefined<ContextType>> = defaultDecodeOptions as any,
4648
): unknown {
47-
const decoder = new Decoder(
49+
const decoder = new Decoder<ContextType>(
4850
options.extensionCodec,
51+
(options as typeof options & { context: any }).context,
4952
options.maxStrLength,
5053
options.maxBinLength,
5154
options.maxArrayLength,

src/decodeAsync.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { Decoder } from "./Decoder";
22
import { defaultDecodeOptions, DecodeOptions } from "./decode";
33
import { ensureAsyncIterabe, ReadableStreamLike } from "./utils/stream";
4+
import { SplitUndefined } from "./context";
45

5-
export async function decodeAsync(
6+
export async function decodeAsync<ContextType>(
67
streamLike: ReadableStreamLike<ArrayLike<number>>,
7-
options: DecodeOptions = defaultDecodeOptions,
8+
options: DecodeOptions<SplitUndefined<ContextType>> = defaultDecodeOptions as any,
89
): Promise<unknown> {
910
const stream = ensureAsyncIterabe(streamLike);
1011

11-
const decoder = new Decoder(
12+
const decoder = new Decoder<ContextType>(
1213
options.extensionCodec,
14+
(options as typeof options & { context: any }).context,
1315
options.maxStrLength,
1416
options.maxBinLength,
1517
options.maxArrayLength,
@@ -19,14 +21,15 @@ export async function decodeAsync(
1921
return decoder.decodeSingleAsync(stream);
2022
}
2123

22-
export function decodeArrayStream(
24+
export function decodeArrayStream<ContextType>(
2325
streamLike: ReadableStreamLike<ArrayLike<number>>,
24-
options: DecodeOptions = defaultDecodeOptions,
26+
options: DecodeOptions<SplitUndefined<ContextType>> = defaultDecodeOptions as any,
2527
) {
2628
const stream = ensureAsyncIterabe(streamLike);
2729

28-
const decoder = new Decoder(
30+
const decoder = new Decoder<ContextType>(
2931
options.extensionCodec,
32+
(options as typeof options & { context: any }).context,
3033
options.maxStrLength,
3134
options.maxBinLength,
3235
options.maxArrayLength,
@@ -37,14 +40,15 @@ export function decodeArrayStream(
3740
return decoder.decodeArrayStream(stream);
3841
}
3942

40-
export function decodeStream(
43+
export function decodeStream<ContextType>(
4144
streamLike: ReadableStreamLike<ArrayLike<number>>,
42-
options: DecodeOptions = defaultDecodeOptions,
45+
options: DecodeOptions<SplitUndefined<ContextType>> = defaultDecodeOptions as any,
4346
) {
4447
const stream = ensureAsyncIterabe(streamLike);
4548

46-
const decoder = new Decoder(
49+
const decoder = new Decoder<ContextType>(
4750
options.extensionCodec,
51+
(options as typeof options & { context: any }).context,
4852
options.maxStrLength,
4953
options.maxBinLength,
5054
options.maxArrayLength,

src/encode.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { ExtensionCodecType } from "./ExtensionCodec";
22
import { Encoder } from "./Encoder";
3+
import { ContextOf, SplitUndefined } from "./context";
34

4-
export type EncodeOptions = Partial<
5+
export type EncodeOptions<ContextType = undefined> = Partial<
56
Readonly<{
6-
extensionCodec: ExtensionCodecType;
7+
extensionCodec: ExtensionCodecType<ContextType>;
78
maxDepth: number;
89
initialBufferSize: number;
910
sortKeys: boolean;
@@ -15,19 +16,24 @@ export type EncodeOptions = Partial<
1516
*/
1617
forceFloat32: boolean;
1718
}>
18-
>;
19+
> &
20+
ContextOf<ContextType>;
1921

20-
const defaultEncodeOptions = {};
22+
const defaultEncodeOptions: EncodeOptions = {};
2123

2224
/**
2325
* It encodes `value` in the MessagePack format and
2426
* returns a byte buffer.
2527
*
2628
* The returned buffer is a slice of a larger `ArrayBuffer`, so you have to use its `#byteOffset` and `#byteLength` in order to convert it to another typed arrays including NodeJS `Buffer`.
2729
*/
28-
export function encode(value: unknown, options: EncodeOptions = defaultEncodeOptions): Uint8Array {
29-
const encoder = new Encoder(
30+
export function encode<ContextType>(
31+
value: unknown,
32+
options: EncodeOptions<SplitUndefined<ContextType>> = defaultEncodeOptions as any,
33+
): Uint8Array {
34+
const encoder = new Encoder<ContextType>(
3035
options.extensionCodec,
36+
(options as typeof options & { context: any }).context,
3137
options.maxDepth,
3238
options.initialBufferSize,
3339
options.sortKeys,

0 commit comments

Comments
 (0)