Skip to content

Commit 66c4db3

Browse files
committed
feat: Support buffer views serialization
1 parent 952a541 commit 66c4db3

6 files changed

Lines changed: 181 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"capnweb": patch
3+
---
4+
5+
Support exact ArrayBuffer, DataView, and typed array serialization over RPC.

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,14 @@ The following types can be passed over RPC (in arguments or return values), and
199199
* Arrays
200200
* `bigint`
201201
* `Date`
202-
* `Uint8Array`
202+
* `ArrayBuffer`, `DataView`, and typed arrays
203203
* `Error` and its well-known subclasses
204204
* `Blob`
205205
* `ReadableStream` and `WritableStream`, with automatic flow control.
206206
* `Headers`, `Request`, and `Response` from the Fetch API.
207207

208208
The following types are not supported as of this writing, but may be added in the future:
209209
* `Map` and `Set`
210-
* `ArrayBuffer` and typed arrays other than `Uint8Array`
211210
* `RegExp`
212211

213212
The following are intentionally NOT supported:

__tests__/index.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,16 @@ describe("simple serialization", () => {
160160
expect(new Uint8Array(deserialized)).toStrictEqual(bytes);
161161
})
162162

163+
it("can serialize Uint8Array as legacy bytes without a type marker", () => {
164+
let bytes = new Uint8Array([72, 101, 108, 108, 111]);
165+
let serialized = serialize(bytes);
166+
expect(serialized).toBe('["bytes","SGVsbG8"]');
167+
168+
let deserialized = deserialize(serialized) as Uint8Array;
169+
expect(deserialized).toBeInstanceOf(Uint8Array);
170+
expect(new Uint8Array(deserialized)).toStrictEqual(bytes);
171+
})
172+
163173
it("can serialize Node.js Buffer as bytes", () => {
164174
if (typeof Buffer === "undefined") return; // skip in browsers
165175
let buf = Buffer.from("hello!");
@@ -170,6 +180,73 @@ describe("simple serialization", () => {
170180
expect(new Uint8Array(deserialized)).toStrictEqual(new Uint8Array(buf));
171181
})
172182

183+
it("can serialize ArrayBuffer as bytes with an ArrayBuffer marker", () => {
184+
let bytes = new Uint8Array([72, 101, 108, 108, 111]);
185+
let serialized = serialize(bytes.buffer);
186+
expect(serialized).toBe('["bytes","SGVsbG8","ArrayBuffer"]');
187+
188+
let deserialized = deserialize(serialized);
189+
expect(deserialized).toBeInstanceOf(ArrayBuffer);
190+
expect(new Uint8Array(deserialized as ArrayBuffer)).toStrictEqual(bytes);
191+
})
192+
193+
it("can serialize typed array views as bytes with type markers", () => {
194+
let cases = [
195+
{
196+
name: "DataView",
197+
elementSize: 1,
198+
makeView: (buffer: ArrayBuffer, offset: number, byteLength: number) =>
199+
new DataView(buffer, offset, byteLength),
200+
},
201+
...[
202+
Int8Array,
203+
Uint8ClampedArray,
204+
Int16Array,
205+
Uint16Array,
206+
Int32Array,
207+
Uint32Array,
208+
BigInt64Array,
209+
BigUint64Array,
210+
Float32Array,
211+
Float64Array,
212+
].map(Type => ({
213+
name: Type.name,
214+
elementSize: Type.BYTES_PER_ELEMENT,
215+
makeView: (buffer: ArrayBuffer, offset: number, byteLength: number) =>
216+
new Type(buffer, offset, byteLength / Type.BYTES_PER_ELEMENT),
217+
})),
218+
];
219+
220+
for (let {name, elementSize, makeView} of cases) {
221+
// Use a non-zero offset and extra trailing byte to verify only the view's visible byte range
222+
// is serialized, not the whole backing buffer.
223+
let byteLength = elementSize * 2;
224+
let offset = elementSize;
225+
let backing = new ArrayBuffer(offset + byteLength + 1);
226+
let bytes = new Uint8Array(byteLength);
227+
for (let i = 0; i < bytes.length; i++) bytes[i] = i + 1;
228+
new Uint8Array(backing, offset, byteLength).set(bytes);
229+
230+
let view = makeView(backing, offset, byteLength);
231+
let serialized = serialize(view);
232+
let parsed = JSON.parse(serialized) as [string, string, string];
233+
expect(parsed[0]).toBe("bytes");
234+
expect(parsed[2]).toBe(name);
235+
236+
let deserialized = deserialize(serialized) as ArrayBufferView;
237+
expect(Object.getPrototypeOf(deserialized)).toBe(Object.getPrototypeOf(view));
238+
expect(new Uint8Array(
239+
deserialized.buffer, deserialized.byteOffset, deserialized.byteLength)).toStrictEqual(bytes);
240+
}
241+
})
242+
243+
it("throws for unknown bytes type markers", () => {
244+
expect(() => deserialize('["bytes","SGVsbG8","invalidUint8Array"]')).toThrowError(
245+
"Unknown bytes type marker: invalidUint8Array");
246+
expect(() => deserialize('["bytes","SGVsbG8",123]')).toThrowError(
247+
"Unknown bytes type marker type: number");
248+
})
249+
173250
it("preserves Invalid Date values through serialization", () => {
174251
let invalidDate = new Date(NaN);
175252
let serialized = serialize(invalidDate);

protocol.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,11 @@ The literal value `undefined`.
168168

169169
The values Infinity, -Infinity, and NaN.
170170

171-
`["bytes", base64]`
171+
`["bytes", base64]`, `["bytes", base64, type]`
172172

173-
A `Uint8Array`, represented as a base64-encoded string.
173+
A `Uint8Array`, `ArrayBuffer`, `DataView`, or typed array, represented as a base64-encoded string.
174+
The optional third element preserves the byte container type across the wire. If it is
175+
omitted, the receiver should deserialize bytes as its default `Uint8Array` for backwards compatibility.
174176

175177
`["blob", type, readableExpression]`
176178

src/core.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,18 @@ export function typeForRpc(value: unknown): TypeForRpc {
9595

9696
case Uint8Array.prototype:
9797
case BUFFER_PROTOTYPE:
98+
case ArrayBuffer.prototype:
99+
case DataView.prototype:
100+
case Int8Array.prototype:
101+
case Uint8ClampedArray.prototype:
102+
case Int16Array.prototype:
103+
case Uint16Array.prototype:
104+
case Int32Array.prototype:
105+
case Uint32Array.prototype:
106+
case BigInt64Array.prototype:
107+
case BigUint64Array.prototype:
108+
case Float32Array.prototype:
109+
case Float64Array.prototype:
98110
return "bytes";
99111

100112
case WritableStream.prototype:

src/serialize.ts

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,59 @@ export class Devaluator {
162162
}
163163

164164
case "bytes": {
165-
let bytes = value as Uint8Array;
166-
if (bytes.toBase64) {
167-
return ["bytes", bytes.toBase64({omitPadding: true})];
165+
let alternateTypeName: string | undefined;
166+
let bytes: Uint8Array | undefined;
167+
switch (Object.getPrototypeOf(value)) {
168+
case ArrayBuffer.prototype:
169+
alternateTypeName = "ArrayBuffer";
170+
bytes = new Uint8Array(value as ArrayBuffer);
171+
break;
172+
case DataView.prototype:
173+
alternateTypeName = "DataView";
174+
break;
175+
case Int8Array.prototype:
176+
alternateTypeName = "Int8Array";
177+
break;
178+
case Uint8ClampedArray.prototype:
179+
alternateTypeName = "Uint8ClampedArray";
180+
break;
181+
case Int16Array.prototype:
182+
alternateTypeName = "Int16Array";
183+
break;
184+
case Uint16Array.prototype:
185+
alternateTypeName = "Uint16Array";
186+
break;
187+
case Int32Array.prototype:
188+
alternateTypeName = "Int32Array";
189+
break;
190+
case Uint32Array.prototype:
191+
alternateTypeName = "Uint32Array";
192+
break;
193+
case BigInt64Array.prototype:
194+
alternateTypeName = "BigInt64Array";
195+
break;
196+
case BigUint64Array.prototype:
197+
alternateTypeName = "BigUint64Array";
198+
break;
199+
case Float32Array.prototype:
200+
alternateTypeName = "Float32Array";
201+
break;
202+
case Float64Array.prototype:
203+
alternateTypeName = "Float64Array";
204+
break;
205+
default:
206+
bytes = value as Uint8Array;
207+
break;
208+
}
209+
if (bytes === undefined) {
210+
let view = value as ArrayBufferView;
211+
bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
168212
}
213+
169214
let b64: string;
170-
if (typeof Buffer !== "undefined") {
215+
if (bytes.toBase64) {
216+
b64 = bytes.toBase64({omitPadding: true});
217+
} else if (typeof Buffer !== "undefined") {
171218
let buf = bytes instanceof Buffer ? bytes
172219
: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
173220
b64 = buf.toString("base64");
@@ -178,7 +225,8 @@ export class Devaluator {
178225
}
179226
b64 = btoa(binary);
180227
}
181-
return ["bytes", b64.replace(/=+$/, "")];
228+
b64 = b64.replace(/=+$/, "");
229+
return alternateTypeName === undefined ? ["bytes", b64] : ["bytes", b64, alternateTypeName];
182230
}
183231

184232
case "headers":
@@ -601,19 +649,45 @@ export class Evaluator {
601649
break;
602650
case "bytes": {
603651
if (typeof value[1] == "string") {
652+
let bytes: Uint8Array;
604653
if (typeof Buffer !== "undefined") {
605-
return Buffer.from(value[1], "base64");
654+
bytes = Buffer.from(value[1], "base64");
606655
} else if (Uint8Array.fromBase64) {
607-
return Uint8Array.fromBase64(value[1]);
656+
bytes = Uint8Array.fromBase64(value[1]);
608657
} else {
609658
let bs = atob(value[1]);
610659
let len = bs.length;
611-
let bytes = new Uint8Array(len);
660+
bytes = new Uint8Array(len);
612661
for (let i = 0; i < len; i++) {
613662
bytes[i] = bs.charCodeAt(i);
614663
}
664+
}
665+
if (value.length === 2) {
615666
return bytes;
616667
}
668+
if (typeof value[2] !== "string") {
669+
throw new TypeError(`Unknown bytes type marker type: ${typeof value[2]}`);
670+
}
671+
672+
let buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
673+
switch (value[2]) {
674+
case "ArrayBuffer": return buffer;
675+
case "DataView": return new DataView(buffer);
676+
case "Int8Array": return new Int8Array(buffer);
677+
case "Uint8ClampedArray": return new Uint8ClampedArray(buffer);
678+
case "Int16Array": return new Int16Array(buffer);
679+
case "Uint16Array": return new Uint16Array(buffer);
680+
case "Int32Array": return new Int32Array(buffer);
681+
case "Uint32Array": return new Uint32Array(buffer);
682+
case "BigInt64Array": return new BigInt64Array(buffer);
683+
case "BigUint64Array": return new BigUint64Array(buffer);
684+
case "Float32Array": return new Float32Array(buffer);
685+
case "Float64Array": return new Float64Array(buffer);
686+
default: {
687+
let marker = value[2].slice(0, 64);
688+
throw new TypeError(`Unknown bytes type marker: ${marker}`);
689+
}
690+
}
617691
}
618692
break;
619693
}

0 commit comments

Comments
 (0)