@nnilky/structo 1.0.7 → 1.0.8

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 (45) hide show
  1. package/README.md +3 -3
  2. package/dist/datatypes/containers/array.js +9 -0
  3. package/dist/datatypes/containers/exhuastiveArray.d.ts +2 -2
  4. package/dist/datatypes/containers/exhuastiveArray.js +13 -2
  5. package/dist/datatypes/containers/fastObject.d.ts +2 -2
  6. package/dist/datatypes/containers/fastObject.js +2 -2
  7. package/dist/datatypes/containers/list.js +14 -2
  8. package/dist/datatypes/containers/object.js +8 -8
  9. package/dist/datatypes/values/string.test.js +3 -0
  10. package/dist/index.d.ts +1 -1
  11. package/dist/read.d.ts +2 -6
  12. package/dist/read.js +9 -1
  13. package/dist/transforms/encode.d.ts +4 -1
  14. package/dist/transforms/encode.js +2 -1
  15. package/dist/transforms/enum.js +11 -8
  16. package/dist/transforms/index.d.ts +1 -2
  17. package/dist/transforms/index.js +1 -2
  18. package/dist/transforms/literal.js +11 -8
  19. package/dist/transforms/modify.d.ts +19 -1
  20. package/dist/transforms/modify.js +21 -1
  21. package/dist/transforms/modify.test.d.ts +1 -0
  22. package/dist/transforms/modify.test.js +17 -0
  23. package/dist/transforms/offset.d.ts +2 -0
  24. package/dist/transforms/offset.js +20 -0
  25. package/dist/transforms/offset.test.d.ts +1 -0
  26. package/dist/transforms/offset.test.js +39 -0
  27. package/dist/transforms/pipe.js +1 -1
  28. package/dist/transforms/toAscii.js +6 -3
  29. package/dist/transforms/toBase64.js +4 -1
  30. package/dist/transforms/toBytes.js +4 -1
  31. package/dist/transforms/toHex.js +4 -1
  32. package/dist/transforms/toTypedArray.js +4 -1
  33. package/dist/types.d.ts +3 -0
  34. package/dist/utilities/index.d.ts +1 -0
  35. package/dist/utilities/index.js +1 -0
  36. package/dist/utilities/lazy.js +4 -4
  37. package/dist/utilities/reference.d.ts +18 -0
  38. package/dist/utilities/reference.js +91 -0
  39. package/dist/utilities/reference.test.d.ts +1 -0
  40. package/dist/utilities/reference.test.js +63 -0
  41. package/dist/utilities/remember.d.ts +6 -5
  42. package/dist/utilities/remember.js +45 -8
  43. package/dist/utilities/remember.test.js +1 -5
  44. package/dist/write.js +9 -1
  45. package/package.json +9 -9
package/README.md CHANGED
@@ -21,9 +21,9 @@ const Entity = st.object({
21
21
  ```
22
22
 
23
23
  - Lightweight, base size is <1KB and each datatype is a few hundred bytes
24
- - Fast, from [benchmarks](./benchmark) only 2-
25
- - Supports Web/Node.js compatible
26
- - Easily implemented
24
+ - Fast![benchmarks](./benchmark) show only 1.5-5x slower than ideal implementaiton
25
+ - Designed for both Web/Node.js compatible
26
+ - Easily implement your own serializers
27
27
 
28
28
  Each serializer is completely seperate from the base library, meaning you only pay for what you use.
29
29
 
@@ -7,6 +7,7 @@
7
7
  *
8
8
  */
9
9
  export function array(size, type) {
10
+ const SHOW_ERRORS = size < 4096;
10
11
  const { read: readType, write: writeType, size: typeSize } = type;
11
12
  return {
12
13
  size: type.size ? size * type.size : undefined,
@@ -16,13 +17,21 @@ export function array(size, type) {
16
17
  if (typeSize)
17
18
  ctx.alloc(size * typeSize);
18
19
  for (let i = 0; i < size; i++) {
20
+ if (SHOW_ERRORS)
21
+ ctx.stack.push(`[${i}]`);
19
22
  writeType(ctx, value[i]);
23
+ if (SHOW_ERRORS)
24
+ ctx.stack.pop();
20
25
  }
21
26
  },
22
27
  read: (ctx) => {
23
28
  const arr = new Array(size);
24
29
  for (let i = 0; i < size; i++) {
30
+ if (SHOW_ERRORS)
31
+ ctx.stack.push(`[${i}]`);
25
32
  arr[i] = readType(ctx);
33
+ if (SHOW_ERRORS)
34
+ ctx.stack.pop();
26
35
  }
27
36
  return arr;
28
37
  },
@@ -1,9 +1,9 @@
1
- import * as st from "../..";
1
+ import * as st from "../../index";
2
2
  /**
3
3
  * exhuastiveArray is read until the end of the data
4
4
  *
5
5
  * ```py
6
- * exhuastiveArray(st.u32())
6
+ * exhuastiveArray(st.string(st.u8()))
7
7
  * ```
8
8
  */
9
9
  export declare function exhuastiveArray<T>(type: st.Serializer<T>): st.Serializer<T[]>;
@@ -1,23 +1,34 @@
1
- import * as st from "../..";
1
+ import * as st from "../../index";
2
2
  /**
3
3
  * exhuastiveArray is read until the end of the data
4
4
  *
5
5
  * ```py
6
- * exhuastiveArray(st.u32())
6
+ * exhuastiveArray(st.string(st.u8()))
7
7
  * ```
8
8
  */
9
9
  export function exhuastiveArray(type) {
10
10
  return {
11
11
  read(ctx) {
12
12
  let arr = [];
13
+ let i = 0;
13
14
  while (ctx.offset < ctx.view.byteLength) {
15
+ const TRACK_STACK = i++ < 4096;
16
+ if (TRACK_STACK)
17
+ ctx.stack.push(`[${arr.length - 1}]`);
14
18
  arr.push(type.read(ctx));
19
+ if (TRACK_STACK)
20
+ ctx.stack.pop();
15
21
  }
16
22
  return arr;
17
23
  },
18
24
  write(ctx, value) {
25
+ const TRACK_STACK = value.length < 4096;
19
26
  for (let i = 0; i < value.length; i++) {
27
+ if (TRACK_STACK)
28
+ ctx.stack.push(`[${i}]`);
20
29
  type.write(ctx, value[i]);
30
+ if (TRACK_STACK)
31
+ ctx.stack.pop();
21
32
  }
22
33
  },
23
34
  };
@@ -3,7 +3,7 @@ type InferObject<T> = T extends Record<string, Serializer<any>> ? {
3
3
  [Key in keyof T]: InferInput<T[Key]>;
4
4
  } : never;
5
5
  /**
6
- * `fastObject` is equivelent to object but it uses eval to improve performance
6
+ * `fastObject` is equivelent to object but it uses eval to improve performance, additionally it omits error reporting
7
7
  *
8
8
  * This means that is should be avoided in scenarios where CSP is required (so can't be the default), but should be fine for all other cases
9
9
  *
@@ -15,7 +15,7 @@ type InferObject<T> = T extends Record<string, Serializer<any>> ? {
15
15
  * })
16
16
  * ```
17
17
  *
18
- * Note: Using st.lazy affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
18
+ * Note: Using `st.lazy` affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
19
19
  */
20
20
  export declare function fastObject<T extends Record<string, Serializer<any>>>(definition: T): Serializer<InferObject<T>>;
21
21
  export {};
@@ -1,6 +1,6 @@
1
1
  const definitionSymbol = Symbol();
2
2
  /**
3
- * `fastObject` is equivelent to object but it uses eval to improve performance
3
+ * `fastObject` is equivelent to object but it uses eval to improve performance, additionally it omits error reporting
4
4
  *
5
5
  * This means that is should be avoided in scenarios where CSP is required (so can't be the default), but should be fine for all other cases
6
6
  *
@@ -12,7 +12,7 @@ const definitionSymbol = Symbol();
12
12
  * })
13
13
  * ```
14
14
  *
15
- * Note: Using st.lazy affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
15
+ * Note: Using `st.lazy` affects the performance of fastObject as it prevents discovery of subobjects, avoid when performance is a requirement
16
16
  */
17
17
  export function fastObject(definition) {
18
18
  let serializers = [];
@@ -12,17 +12,29 @@ export function list(length, type) {
12
12
  return {
13
13
  write: (ctx, value) => {
14
14
  length.write(ctx, value.length);
15
- if (type.size)
15
+ const TRACK_STACK = value.length < 4096;
16
+ if (type.size) {
16
17
  ctx.alloc(value.length * type.size);
17
- for (const v of value) {
18
+ }
19
+ for (let i = 0; i < value.length; i++) {
20
+ if (TRACK_STACK)
21
+ ctx.stack.push(`[${i}]`);
22
+ const v = value[i];
18
23
  type.write(ctx, v);
24
+ if (TRACK_STACK)
25
+ ctx.stack.pop();
19
26
  }
20
27
  },
21
28
  read: (ctx) => {
22
29
  const size = length.read(ctx);
30
+ const TRACK_STACK = size < 4096;
23
31
  const arr = new Array(size);
24
32
  for (let i = 0; i < size; i++) {
33
+ if (TRACK_STACK)
34
+ ctx.stack.push(`[${i}]`);
25
35
  arr[i] = type.read(ctx);
36
+ if (TRACK_STACK)
37
+ ctx.stack.pop();
26
38
  }
27
39
  return arr;
28
40
  },
@@ -21,19 +21,19 @@ export function object(definition) {
21
21
  if (size)
22
22
  ctx.alloc(size);
23
23
  for (let i = 0; i < entires.length; i++) {
24
- const key = entires[i][0];
25
- try {
26
- entires[i][1].write(ctx, value[key]);
27
- }
28
- catch (e) {
29
- throw new Error(`Failed to encode key '${key}'`, { cause: e });
30
- }
24
+ const [key, serializer] = entires[i];
25
+ ctx.stack.push(`.${key}`);
26
+ serializer.write(ctx, value[key]);
27
+ ctx.stack.pop();
31
28
  }
32
29
  },
33
30
  read: (ctx) => {
34
31
  const output = new Array(entires.length);
35
32
  for (let i = 0; i < entires.length; i++) {
36
- output[i] = [entires[i][0], entires[i][1].read(ctx)];
33
+ const [key, serializer] = entires[i];
34
+ ctx.stack.push(`.${key}`);
35
+ output[i] = [key, serializer.read(ctx)];
36
+ ctx.stack.pop();
37
37
  }
38
38
  return Object.fromEntries(output);
39
39
  },
@@ -12,6 +12,9 @@ describe("st.string", () => {
12
12
  //@ts-expect-error, intentional mistake
13
13
  encodeFailTest(st.string(st.u8()), 8);
14
14
  });
15
+ it("works on long strings", () => {
16
+ encodeTest(st.string(st.u32()), "A".repeat(2 ** 24));
17
+ });
15
18
  it("errors on too long strings", () => {
16
19
  encodeFailTest(st.string(st.u8()), "A".repeat(256));
17
20
  });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { InferInput as Infer, InferInput, InferOutput, ReaderContext, WriterContext, Serializer, } from "./types";
1
+ export type { InferOutput as Infer, InferInput, InferOutput, ReaderContext, WriterContext, Serializer, } from "./types";
2
2
  export { read, createReaderContext } from "./read";
3
3
  export { write, createdWriterContext } from "./write";
4
4
  export * from "./datatypes/index";
package/dist/read.d.ts CHANGED
@@ -1,7 +1,3 @@
1
- import type { Serializer } from "./types";
1
+ import type { Serializer, ReaderContext } from "./types";
2
2
  export declare function read<TIn, TOut>(serializer: Serializer<TIn, TOut>, buffer: ArrayBuffer): TOut;
3
- export declare function createReaderContext(buffer: ArrayBuffer): {
4
- offset: number;
5
- buffer: ArrayBuffer;
6
- view: DataView<ArrayBuffer>;
7
- };
3
+ export declare function createReaderContext(buffer: ArrayBuffer): ReaderContext;
package/dist/read.js CHANGED
@@ -1,8 +1,16 @@
1
1
  export function read(serializer, buffer) {
2
- return serializer.read(createReaderContext(buffer));
2
+ const ctx = createReaderContext(buffer);
3
+ try {
4
+ return serializer.read(ctx);
5
+ }
6
+ catch (e) {
7
+ const path = ctx.stack.join("") ?? "root";
8
+ throw new Error(`Deserialization error at serializer${path}`, { cause: e });
9
+ }
3
10
  }
4
11
  export function createReaderContext(buffer) {
5
12
  return {
13
+ stack: [],
6
14
  offset: 0,
7
15
  buffer,
8
16
  view: new DataView(buffer),
@@ -1,2 +1,5 @@
1
1
  import type { Transform } from "./pipe";
2
- export declare function encode<TIn, TOut>(encode: (value: TOut) => TIn, decode: (value: TIn) => TOut): Transform<TOut, TIn>;
2
+ export declare function encode<TIn, TOut>(options: {
3
+ encode: (value: TOut) => TIn;
4
+ decode: (value: TIn) => TOut;
5
+ }): Transform<TOut, TIn>;
@@ -1,4 +1,5 @@
1
- export function encode(encode, decode) {
1
+ export function encode(options) {
2
+ const { encode, decode } = options;
2
3
  return (type) => ({
3
4
  size: type.size,
4
5
  read: (ctx) => decode(type.read(ctx)),
@@ -1,13 +1,16 @@
1
1
  import { encode } from "./encode";
2
2
  export function enum_(values) {
3
- return encode((v) => {
4
- if (!values.includes(v))
5
- throw new Error(`Invalid enum variant: ${v}`);
6
- return v;
7
- }, (v) => {
8
- if (!values.includes(v))
9
- throw new Error(`Invalid enum variant: ${v}`);
10
- return v;
3
+ return encode({
4
+ encode: (v) => {
5
+ if (!values.includes(v))
6
+ throw new Error(`Invalid enum variant: ${v}`);
7
+ return v;
8
+ },
9
+ decode: (v) => {
10
+ if (!values.includes(v))
11
+ throw new Error(`Invalid enum variant: ${v}`);
12
+ return v;
13
+ },
11
14
  });
12
15
  }
13
16
  export { enum_ as enum };
@@ -3,8 +3,7 @@ export { encode } from "./encode";
3
3
  export { modify } from "./modify";
4
4
  export { literal } from "./literal";
5
5
  export { enum } from "./enum";
6
- export { fixedOffset } from "./fixedOffset";
7
- export { noAdvance } from "./noAdvance";
6
+ export { offset } from "./offset";
8
7
  export { toAscii } from "./toAscii";
9
8
  export { toBytes } from "./toBytes";
10
9
  export { toHex } from "./toHex";
@@ -3,8 +3,7 @@ export { encode } from "./encode";
3
3
  export { modify } from "./modify";
4
4
  export { literal } from "./literal";
5
5
  export { enum } from "./enum";
6
- export { fixedOffset } from "./fixedOffset";
7
- export { noAdvance } from "./noAdvance";
6
+ export { offset } from "./offset";
8
7
  export { toAscii } from "./toAscii";
9
8
  export { toBytes } from "./toBytes";
10
9
  export { toHex } from "./toHex";
@@ -1,12 +1,15 @@
1
1
  import { encode } from "./encode";
2
2
  export function literal(value) {
3
- return encode((v) => {
4
- if (v !== value)
5
- throw new Error(`Invalid literal variant: ${v}`);
6
- return v;
7
- }, (v) => {
8
- if (v !== value)
9
- throw new Error(`Invalid literal variant: ${v}`);
10
- return v;
3
+ return encode({
4
+ encode: (v) => {
5
+ if (v !== value)
6
+ throw new Error(`Invalid literal variant: ${v}`);
7
+ return v;
8
+ },
9
+ decode: (v) => {
10
+ if (v !== value)
11
+ throw new Error(`Invalid literal variant: ${v}`);
12
+ return v;
13
+ },
11
14
  });
12
15
  }
@@ -1,2 +1,20 @@
1
1
  import type { Transform } from "./pipe";
2
- export declare function modify<T>(callback: (value: T) => T): Transform<T>;
2
+ /**
3
+ * A readonly modification function, useful for sizes or offsets.
4
+ *
5
+ * If you want a writable value, use `st.encode` instead
6
+ *
7
+ *
8
+ * ```ts
9
+ * length: length.save(st.u32())
10
+ * st.sizedBytes(
11
+ * st.pipe(
12
+ * length.load(),
13
+ * st.modify(v => v - 8),
14
+ * )
15
+ * ))
16
+ *
17
+ * st.pipe()
18
+ * ```
19
+ */
20
+ export declare function modify<TIn, TOut>(callback: (value: TIn) => TOut): Transform<TOut, TIn>;
@@ -1,7 +1,27 @@
1
+ /**
2
+ * A readonly modification function, useful for sizes or offsets.
3
+ *
4
+ * If you want a writable value, use `st.encode` instead
5
+ *
6
+ *
7
+ * ```ts
8
+ * length: length.save(st.u32())
9
+ * st.sizedBytes(
10
+ * st.pipe(
11
+ * length.load(),
12
+ * st.modify(v => v - 8),
13
+ * )
14
+ * ))
15
+ *
16
+ * st.pipe()
17
+ * ```
18
+ */
1
19
  export function modify(callback) {
2
20
  return (type) => ({
3
21
  size: type.size,
4
22
  read: (ctx) => callback(type.read(ctx)),
5
- write: (ctx, value) => type.write(ctx, callback(value)),
23
+ write: () => {
24
+ throw new Error("Cannot write an encoded value");
25
+ },
6
26
  });
7
27
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { encodeFailTest } from "../utils.test";
3
+ import * as st from "../index";
4
+ describe("st.modify", () => {
5
+ it("encodes correctly", () => {
6
+ const data = st.write(st.u32(), 10);
7
+ const output = st.read(st.pipe(st.u32(), st.modify((v) => v + 8)), data);
8
+ expect(output).toBe(18);
9
+ });
10
+ it("cannot be written", () => {
11
+ encodeFailTest(st.pipe(st.u32(), st.modify((v) => v + 8)), 3);
12
+ });
13
+ it("does not modify size", () => {
14
+ const spec = st.pipe(st.u32(), st.modify((v) => v + 8));
15
+ expect(spec.size).toBe(4);
16
+ });
17
+ });
@@ -0,0 +1,2 @@
1
+ import type { Serializer } from "../types";
2
+ export declare function offset<TIn, TOut>(behaviour: "relative" | "absolute", offset: Serializer<number> | number): (type: Serializer<TIn, TOut>) => Serializer<TIn, TOut>;
@@ -0,0 +1,20 @@
1
+ export function offset(behaviour, offset) {
2
+ return (type) => ({
3
+ size: type.size,
4
+ read: (ctx) => {
5
+ let start = ctx.offset;
6
+ const offsetValue = typeof offset === "number" ? offset : offset.read(ctx);
7
+ ctx.offset = behaviour === "absolute" ? offsetValue : ctx.offset + offsetValue;
8
+ const value = type.read(ctx);
9
+ ctx.offset = start;
10
+ return value;
11
+ },
12
+ write: (ctx, value) => {
13
+ let start = ctx.offset;
14
+ const offsetValue = typeof offset === "number" ? offset : offset.read(ctx);
15
+ ctx.offset = behaviour === "absolute" ? offsetValue : ctx.offset + offsetValue;
16
+ type.write(ctx, value);
17
+ ctx.offset = start;
18
+ },
19
+ });
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { describe, it } from "bun:test";
2
+ import { encodeTest } from "../utils.test";
3
+ import * as st from "../index";
4
+ describe("st.offset(relative)", () => {
5
+ it("encodes correctly with fixed offset", () => {
6
+ const spec = st.object({
7
+ foo: st.u32(),
8
+ bar: st.pipe(st.u32(), st.offset("relative", -4)),
9
+ });
10
+ encodeTest(spec, { foo: 1, bar: 5 }, { foo: 5, bar: 5 });
11
+ });
12
+ it("encodes correctly with read offset", () => {
13
+ const offset = st.createRememberedValue();
14
+ const spec = st.object({
15
+ foo: st.u32(),
16
+ offset: offset.save(st.s16()),
17
+ bar: st.pipe(st.u32(), st.offset("relative", offset.load())),
18
+ });
19
+ encodeTest(spec, { foo: 0, offset: -6, bar: 5 }, { foo: 5, offset: -6, bar: 5 });
20
+ });
21
+ });
22
+ describe("st.offset(absolute)", () => {
23
+ it("encodes correctly with fixed offset", () => {
24
+ const spec = st.object({
25
+ foo: st.u32(),
26
+ bar: st.pipe(st.u32(), st.offset("absolute", 0)),
27
+ });
28
+ encodeTest(spec, { foo: 0, bar: 5 }, { foo: 5, bar: 5 });
29
+ });
30
+ it("encodes correctly with read offset", () => {
31
+ const offset = st.createRememberedValue();
32
+ const spec = st.object({
33
+ foo: st.u32(),
34
+ offset: offset.save(st.u32()),
35
+ bar: st.pipe(st.u32(), st.offset("absolute", offset.load())),
36
+ });
37
+ encodeTest(spec, { foo: 0, offset: 0, bar: 5 }, { foo: 5, offset: 0, bar: 5 });
38
+ });
39
+ });
@@ -6,7 +6,7 @@
6
6
  * ageInMonths: st.pipe(
7
7
  * st.u32(),
8
8
  * st.modify(v => v * 12),
9
- * st.offset(-8),
9
+ * st.offset("relative", -8),
10
10
  * )})
11
11
  * ```
12
12
  */
@@ -1,8 +1,11 @@
1
1
  import { encode } from "./encode";
2
2
  export function toAscii() {
3
- return encode((v) => new Uint8Array(Array.from(v).map((char) => validateAscii(char.charCodeAt(0)))).buffer, (v) => Array.from(new Uint8Array(v))
4
- .map((v) => String.fromCharCode(v))
5
- .join(""));
3
+ return encode({
4
+ encode: (v) => new Uint8Array(Array.from(v).map((char) => validateAscii(char.charCodeAt(0)))).buffer,
5
+ decode: (v) => Array.from(new Uint8Array(v))
6
+ .map((v) => String.fromCharCode(v))
7
+ .join(""),
8
+ });
6
9
  }
7
10
  const validateAscii = (v) => {
8
11
  if (v >= 0 && v <= 127)
@@ -1,4 +1,7 @@
1
1
  import { encode } from "./encode";
2
2
  export function toBase64() {
3
- return encode((v) => Uint8Array.fromBase64(v).buffer, (v) => new Uint8Array(v).toBase64());
3
+ return encode({
4
+ encode: (v) => Uint8Array.fromBase64(v).buffer,
5
+ decode: (v) => new Uint8Array(v).toBase64(),
6
+ });
4
7
  }
@@ -12,7 +12,10 @@ import { encode } from "./encode";
12
12
  * `ArrayBuffer([0, 255, c])` => `[0, 255, ArrayBuffer([0, 255, 0])]`
13
13
  */
14
14
  export function toBytes() {
15
- return encode((v) => new Uint8Array(v.map(validateByte)).buffer, (v) => Array.from(new Uint8Array(v)));
15
+ return encode({
16
+ encode: (v) => new Uint8Array(v.map(validateByte)).buffer,
17
+ decode: (v) => Array.from(new Uint8Array(v)),
18
+ });
16
19
  }
17
20
  const validateByte = (v) => {
18
21
  if (v >= 0 && v <= 255)
@@ -12,5 +12,8 @@ import { encode } from "./encode";
12
12
  * `ArrayBuffer([0, 255, 0])` => `00FF00`
13
13
  */
14
14
  export function toHex() {
15
- return encode((v) => Uint8Array.fromHex(v).buffer, (v) => new Uint8Array(v).toHex().toUpperCase());
15
+ return encode({
16
+ encode: (v) => Uint8Array.fromHex(v).buffer,
17
+ decode: (v) => new Uint8Array(v).toHex().toUpperCase(),
18
+ });
16
19
  }
@@ -10,5 +10,8 @@ import { encode } from "./encode";
10
10
  * ```
11
11
  */
12
12
  export function toTypedArray(arrayType) {
13
- return encode((v) => v.buffer, (v) => new arrayType(v));
13
+ return encode({
14
+ encode: (v) => v.buffer,
15
+ decode: (v) => new arrayType(v),
16
+ });
14
17
  }
package/dist/types.d.ts CHANGED
@@ -3,13 +3,16 @@ export type Serializer<TIn, TOut = TIn> = {
3
3
  write: (ctx: WriterContext, value: TIn) => void;
4
4
  read: (ctx: ReaderContext) => TOut;
5
5
  };
6
+ export type SerializationContext = WriterContext | ReaderContext;
6
7
  export interface WriterContext {
8
+ stack: string[];
7
9
  buffer: ArrayBuffer;
8
10
  view: DataView;
9
11
  offset: number;
10
12
  alloc: (length: number) => void;
11
13
  }
12
14
  export interface ReaderContext {
15
+ stack: string[];
13
16
  buffer: ArrayBuffer;
14
17
  view: DataView;
15
18
  offset: number;
@@ -1,2 +1,3 @@
1
1
  export { lazy } from "./lazy";
2
+ export { createReference } from "./reference";
2
3
  export { createRememberedValue } from "./remember";
@@ -1,2 +1,3 @@
1
1
  export { lazy } from "./lazy";
2
+ export { createReference } from "./reference";
2
3
  export { createRememberedValue } from "./remember";
@@ -1,11 +1,11 @@
1
1
  export function lazy(type) {
2
2
  let _size;
3
3
  let resolve = () => {
4
- const t = type();
5
- _size = t.size ?? undefined;
6
- serializer.read = t.read;
7
- serializer.write = t.write;
8
4
  resolve = () => { };
5
+ const { size, write, read } = type();
6
+ _size = size ?? undefined;
7
+ serializer.read = read;
8
+ serializer.write = write;
9
9
  };
10
10
  const serializer = {
11
11
  get size() {
@@ -0,0 +1,18 @@
1
+ import type { Serializer } from "../types";
2
+ /**
3
+ * createReference lets you defer writing a value until later, useful for lengths and sizes
4
+ *
5
+ * ---
6
+ * ```
7
+ * const length = st.createReference<number>()
8
+ * st.object({
9
+ * length: length.pointer(st.u32()),
10
+ * type: st.u8(),
11
+ * data: st.sizedBytes(length.deref())
12
+ * })
13
+ * ```
14
+ */
15
+ export declare function createReference<T>(): {
16
+ pointer: (serializer: Serializer<T>) => Serializer<T>;
17
+ deref: () => Serializer<T>;
18
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * createReference lets you defer writing a value until later, useful for lengths and sizes
3
+ *
4
+ * ---
5
+ * ```
6
+ * const length = st.createReference<number>()
7
+ * st.object({
8
+ * length: length.pointer(st.u32()),
9
+ * type: st.u8(),
10
+ * data: st.sizedBytes(length.deref())
11
+ * })
12
+ * ```
13
+ */
14
+ export function createReference() {
15
+ const readStack = [];
16
+ const writeStack = [];
17
+ /**
18
+ * When reading: reads the value normal and saves it for later
19
+ *
20
+ * When writing: omits this value, it is written by deref
21
+ *
22
+ * ---
23
+ * ```
24
+ * const length = st.createReference<number>()
25
+ * st.object({
26
+ * length: length.pointer(st.u32()),
27
+ * type: st.u8(),
28
+ * data: st.sizedBytes(length.deref())
29
+ * })
30
+ * ```
31
+ */
32
+ function pointer(serializer) {
33
+ const size = serializer.size;
34
+ if (size === undefined)
35
+ throw new Error("Serializer must be sized");
36
+ return {
37
+ size,
38
+ read: (ctx) => {
39
+ const value = serializer.read(ctx);
40
+ readStack.push(value);
41
+ return value;
42
+ },
43
+ write: (ctx, value) => {
44
+ writeStack.push({
45
+ offset: ctx.offset,
46
+ serializer,
47
+ });
48
+ ctx.offset += size;
49
+ return value;
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * When reading: gets the value stored in pointer
55
+ *
56
+ * When writing: saves the value into the pointer
57
+ *
58
+ * By default the value is removed from the pointer,
59
+ * but you can retain it to ensure the next value is
60
+ *
61
+ * ---
62
+ * ```
63
+ * const length = st.createReference<number>()
64
+ * st.object({
65
+ * length: length.pointer(st.u32()),
66
+ * type: st.u8(),
67
+ * data: st.sizedBytes(length.deref())
68
+ * })
69
+ * ```
70
+ */
71
+ function deref() {
72
+ return {
73
+ size: 0,
74
+ read: () => {
75
+ if (readStack.length === 0)
76
+ throw new Error("Read Stack Empty");
77
+ return readStack.pop();
78
+ },
79
+ write: (ctx, value) => {
80
+ if (writeStack.length === 0)
81
+ throw new Error("Write Stack is emtpy");
82
+ const { offset, serializer } = writeStack.pop();
83
+ const start = ctx.offset;
84
+ ctx.offset = offset;
85
+ serializer.write(ctx, value);
86
+ ctx.offset = start;
87
+ },
88
+ };
89
+ }
90
+ return { pointer, deref };
91
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { encodeTest, encodeSnapshotTest, expectError } from "../utils.test";
3
+ import * as st from "../index";
4
+ describe("st.createReference", () => {
5
+ it("encodes correctly", () => {
6
+ const length = st.createReference();
7
+ const spec = st.object({
8
+ length: length.pointer(st.u32()),
9
+ numbers: st.list(length.deref(), st.u8()),
10
+ });
11
+ expect(spec.size).toBe(undefined);
12
+ encodeTest(spec, //
13
+ { length: 0, numbers: [1, 3, 4] }, { length: 3, numbers: [1, 3, 4] });
14
+ });
15
+ it("cant create reference to pointer", () => {
16
+ const length = st.createReference();
17
+ expectError(() => {
18
+ length.pointer(st.sizedBytes(st.u8()));
19
+ });
20
+ });
21
+ it("multiple runs function", () => {
22
+ const v = st.createReference();
23
+ const spec = st.object({
24
+ a: v.pointer(st.u32()),
25
+ b: st.list(v.deref(), st.u8()),
26
+ });
27
+ for (let i = 0; i < 3; i++) {
28
+ encodeTest(spec, //
29
+ { a: NaN, b: [1, 3, 4] }, { a: 3, b: [1, 3, 4] });
30
+ }
31
+ });
32
+ it("handles recursive objects", () => {
33
+ const size = st.createReference();
34
+ //@ts-expect-error, recursive types
35
+ const Node = st.lazy(() => st.object({
36
+ size: size.pointer(st.u32()),
37
+ children: st.list(size.deref(), Node),
38
+ }));
39
+ encodeTest(Node, {
40
+ size: NaN,
41
+ children: [
42
+ { size: NaN, children: [] },
43
+ { size: NaN, children: [{ size: NaN, children: [] }] },
44
+ ],
45
+ }, {
46
+ size: 2,
47
+ children: [
48
+ { size: 0, children: [] },
49
+ { size: 1, children: [{ size: 0, children: [] }] },
50
+ ],
51
+ });
52
+ });
53
+ it("snapshots are correct", () => {
54
+ const v = st.createReference();
55
+ encodeSnapshotTest(st.object({
56
+ a: v.pointer(st.u32()),
57
+ b: st.list(v.deref(), st.u8()),
58
+ }), {
59
+ a: 3,
60
+ b: [1, 3, 4],
61
+ });
62
+ });
63
+ });
@@ -2,16 +2,17 @@ import type { Serializer } from "../types";
2
2
  /**
3
3
  * createRememberedValue lets you save and recall a value when serializing
4
4
  *
5
+ * This is useful for checksums and other values calculated from a writen value
6
+ *
5
7
  * ```
6
- * const length = st.createRememberedValue<number>()
8
+ * const data = st.createRememberedValue<ArrayBuffer>()
7
9
  * st.object({
8
- * length: length.save(st.u32()),
9
- * type: st.u8(),
10
- * data: st.sizedBytes(length.load())
10
+ * data: data.save(st.bytes(1024))
11
+ * checksum: crc32(data.load())
11
12
  * })
12
13
  * ```
13
14
  */
14
15
  export declare function createRememberedValue<T>(): {
15
16
  save: (serializer: Serializer<T>) => Serializer<T>;
16
- load: (serializer?: Serializer<T>) => Serializer<T>;
17
+ load: (behaviour?: "pop" | "retain") => Serializer<T>;
17
18
  };
@@ -1,17 +1,30 @@
1
1
  /**
2
2
  * createRememberedValue lets you save and recall a value when serializing
3
3
  *
4
+ * This is useful for checksums and other values calculated from a writen value
5
+ *
4
6
  * ```
5
- * const length = st.createRememberedValue<number>()
7
+ * const data = st.createRememberedValue<ArrayBuffer>()
6
8
  * st.object({
7
- * length: length.save(st.u32()),
8
- * type: st.u8(),
9
- * data: st.sizedBytes(length.load())
9
+ * data: data.save(st.bytes(1024))
10
+ * checksum: crc32(data.load())
10
11
  * })
11
12
  * ```
12
13
  */
13
14
  export function createRememberedValue() {
14
15
  const stack = [];
16
+ /**
17
+ * Transparently writes/reads the underlying value but stores the value written.
18
+ *
19
+ * ---
20
+ * ```
21
+ * const data = st.createRememberedValue<ArrayBuffer>()
22
+ * st.object({
23
+ * data: data.save(st.bytes(1024))
24
+ * checksum: crc32(data.load())
25
+ * })
26
+ * ```
27
+ * */
15
28
  function save(serializer) {
16
29
  return {
17
30
  size: serializer.size,
@@ -27,11 +40,35 @@ export function createRememberedValue() {
27
40
  },
28
41
  };
29
42
  }
30
- function load(serializer) {
43
+ /**
44
+ * Retrieves the last value written by `.save()`
45
+ *
46
+ * By default, the value is forgotten when written so that memory isn't leaked.
47
+ * It can be retained by setting `behaviour` to `"retain"`
48
+ *
49
+ * When writing this type does nothing
50
+ *
51
+ * ---
52
+ * ```
53
+ * const data = st.createRememberedValue<ArrayBuffer>()
54
+ * st.object({
55
+ * data: data.save(st.bytes(1024))
56
+ * checksum: crc32(data.load())
57
+ * })
58
+ * ```
59
+ */
60
+ function load(behaviour = "pop") {
31
61
  return {
32
- size: serializer?.size ?? 0,
33
- read: () => stack.pop(),
34
- write: (ctx) => serializer && serializer.write(ctx, stack.pop()),
62
+ size: 0,
63
+ read: () => {
64
+ if (behaviour === "pop") {
65
+ return stack.pop();
66
+ }
67
+ else {
68
+ return stack.at(-1);
69
+ }
70
+ },
71
+ write: () => { },
35
72
  };
36
73
  }
37
74
  return { save: save, load: load };
@@ -37,7 +37,7 @@ describe("st.remember", () => {
37
37
  const v = st.createRememberedValue();
38
38
  const spec = st.object({
39
39
  a: v.save(st.u32()),
40
- b: v.load(st.u8()),
40
+ b: v.load(),
41
41
  });
42
42
  encodeTest(spec, { a: 3, b: 0 }, { a: 3, b: 3 });
43
43
  });
@@ -68,9 +68,5 @@ describe("st.remember", () => {
68
68
  a: 3,
69
69
  b: [1, 3, 4],
70
70
  });
71
- encodeSnapshotTest(st.object({
72
- a: v.save(st.u32()),
73
- b: v.load(st.u8()),
74
- }), { a: 3, b: 0 });
75
71
  });
76
72
  });
package/dist/write.js CHANGED
@@ -1,6 +1,12 @@
1
1
  export function write(serializer, value) {
2
2
  const ctx = createdWriterContext(serializer);
3
- serializer.write(ctx, value);
3
+ try {
4
+ serializer.write(ctx, value);
5
+ }
6
+ catch (e) {
7
+ const path = ctx.stack.join("") ?? "root";
8
+ throw new Error(`Serialization error at value${path}`, { cause: e });
9
+ }
4
10
  if (ctx.buffer.byteLength === ctx.offset) {
5
11
  return ctx.buffer;
6
12
  }
@@ -13,6 +19,7 @@ export function createdWriterContext(type) {
13
19
  let buffer = new ArrayBuffer(type.size);
14
20
  const view = new DataView(buffer);
15
21
  return {
22
+ stack: [],
16
23
  offset: 0,
17
24
  buffer,
18
25
  view,
@@ -24,6 +31,7 @@ export function createdWriterContext(type) {
24
31
  let buffer = new ArrayBuffer(bufferLength);
25
32
  const view = new DataView(buffer);
26
33
  return {
34
+ stack: [],
27
35
  offset: 0,
28
36
  buffer,
29
37
  view,
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "node"
10
10
  ],
11
11
  "license": "ISC",
12
- "version": "1.0.7",
12
+ "version": "1.0.8",
13
13
  "author": "Ben Brady",
14
14
  "private": false,
15
15
  "publishConfig": {
@@ -44,12 +44,6 @@
44
44
  "./dist/**/*.js.map",
45
45
  "./dist/**/*.js"
46
46
  ],
47
- "scripts": {
48
- "build": "tsc",
49
- "prepublish": "tsc",
50
- "check": "tsc --noEmit",
51
- "test": "bun test src --only-failures --coverage"
52
- },
53
47
  "devDependencies": {
54
48
  "@types/bun": "^1.3.14",
55
49
  "@types/node": "^25.9.2",
@@ -57,5 +51,11 @@
57
51
  "jest": "^30.4.2",
58
52
  "typescript": "^6.0.3"
59
53
  },
60
- "packageManager": "pnpm@10.13.1"
61
- }
54
+ "scripts": {
55
+ "build": "tsc",
56
+ "prepublish": "tsc",
57
+ "check": "tsc --noEmit",
58
+ "test": "bun test src --only-failures",
59
+ "test:coverage": "bun test src --only-failures --coverage"
60
+ }
61
+ }