@automate.ax/codec 0.1.3
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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/buffered.d.ts +104 -0
- package/dist/buffered.js +156 -0
- package/dist/extensions.d.ts +1 -0
- package/dist/extensions.js +66 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +112 -0
- package/dist/lib/utils.d.ts +6 -0
- package/dist/lib/utils.js +9 -0
- package/dist/packr.d.ts +10 -0
- package/dist/packr.js +17 -0
- package/dist/schema.d.ts +5 -0
- package/dist/schema.js +1 -0
- package/dist/type-codes.d.ts +19 -0
- package/dist/type-codes.js +19 -0
- package/dist/walk.d.ts +19 -0
- package/dist/walk.js +69 -0
- package/package.json +70 -0
- package/src/buffered.ts +178 -0
- package/src/extensions.ts +93 -0
- package/src/index.ts +169 -0
- package/src/lib/utils.ts +9 -0
- package/src/packr.ts +18 -0
- package/src/schema.ts +13 -0
- package/src/type-codes.ts +19 -0
- package/src/walk.ts +97 -0
package/dist/walk.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { BufferedBlob, BufferedFile, BufferedRequest, BufferedResponse, } from "./buffered.js";
|
|
2
|
+
import { isPlainObject } from "./lib/utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Walks a value tree and replaces anything with an async body (`Request`,
|
|
5
|
+
* `Response`, `Blob`, `File`) with a sync-buffered snapshot, so the result can
|
|
6
|
+
* be handed to a synchronous encoder.
|
|
7
|
+
*
|
|
8
|
+
* Recurses through plain objects, arrays, `Map`s, and `Set`s. Leaves
|
|
9
|
+
* primitives, typed arrays, and sync class instances (`Headers`, `URL`, etc.)
|
|
10
|
+
* alone — those either have no async surface or msgpackr handles them natively
|
|
11
|
+
* via registered extensions at pack time.
|
|
12
|
+
*
|
|
13
|
+
* Dedupes by reference within a single call so that a tree sharing the same
|
|
14
|
+
* `Request`/`Response`/`Blob`/`File` in multiple places doesn't trigger "body
|
|
15
|
+
* already used".
|
|
16
|
+
*
|
|
17
|
+
* Does not detect cycles. Automation outputs are expected to be tree-shaped.
|
|
18
|
+
*
|
|
19
|
+
* @param value - Value tree to prepare for synchronous encoding.
|
|
20
|
+
*/
|
|
21
|
+
export async function bufferAsyncValues(value) {
|
|
22
|
+
return walk(value, new WeakMap());
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Walks a value while reusing in-flight work for repeated object references.
|
|
26
|
+
*
|
|
27
|
+
* @param value - Value at the current traversal position.
|
|
28
|
+
* @param cache - Buffered results keyed by source object identity.
|
|
29
|
+
*/
|
|
30
|
+
async function walk(value, cache) {
|
|
31
|
+
if (value === null || typeof value !== "object")
|
|
32
|
+
return value;
|
|
33
|
+
const cached = cache.get(value);
|
|
34
|
+
if (cached)
|
|
35
|
+
return cached;
|
|
36
|
+
const task = walkFresh(value, cache);
|
|
37
|
+
cache.set(value, task);
|
|
38
|
+
return task;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Buffers an object that does not yet have a cached traversal task.
|
|
42
|
+
*
|
|
43
|
+
* @param value - Object to inspect and buffer.
|
|
44
|
+
* @param cache - Buffered results keyed by source object identity.
|
|
45
|
+
*/
|
|
46
|
+
async function walkFresh(value, cache) {
|
|
47
|
+
if (value instanceof Request)
|
|
48
|
+
return BufferedRequest.from(value);
|
|
49
|
+
if (value instanceof Response)
|
|
50
|
+
return BufferedResponse.from(value);
|
|
51
|
+
// File extends Blob — must be checked first so we don't lose name/lastModified.
|
|
52
|
+
if (value instanceof File)
|
|
53
|
+
return BufferedFile.from(value);
|
|
54
|
+
if (value instanceof Blob)
|
|
55
|
+
return BufferedBlob.from(value);
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
return Promise.all(value.map((v) => walk(v, cache)));
|
|
58
|
+
}
|
|
59
|
+
if (value instanceof Map) {
|
|
60
|
+
return new Map(await Promise.all([...value.entries()].map(async ([k, v]) => [await walk(k, cache), await walk(v, cache)])));
|
|
61
|
+
}
|
|
62
|
+
if (value instanceof Set) {
|
|
63
|
+
return new Set(await Promise.all([...value].map((v) => walk(v, cache))));
|
|
64
|
+
}
|
|
65
|
+
if (isPlainObject(value)) {
|
|
66
|
+
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([k, v]) => [k, await walk(v, cache)])));
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@automate.ax/codec",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Msgpack-based encoding helpers for Automate.ax runtime data.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/zachsents/automate.ax.git",
|
|
9
|
+
"directory": "packages/codec"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://automate.ax",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/zachsents/automate.ax/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"zshy": {
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./*": "./src/*"
|
|
23
|
+
},
|
|
24
|
+
"cjs": false,
|
|
25
|
+
"conditions": {
|
|
26
|
+
"bun": "src"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"bun": "./src/index.ts",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./*": {
|
|
36
|
+
"bun": "./src/*",
|
|
37
|
+
"types": "./dist/*",
|
|
38
|
+
"default": "./dist/*"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"lint": "oxlint --type-aware",
|
|
44
|
+
"lint:fix": "oxlint --type-aware --fix-suggestions",
|
|
45
|
+
"check": "bun run typecheck && bun run lint",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"build": "zshy",
|
|
48
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"dist",
|
|
52
|
+
"src",
|
|
53
|
+
"README.md",
|
|
54
|
+
"LICENSE"
|
|
55
|
+
],
|
|
56
|
+
"main": "./dist/index.js",
|
|
57
|
+
"module": "./dist/index.js",
|
|
58
|
+
"types": "./dist/index.d.ts",
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@internal/config": "0.0.0",
|
|
61
|
+
"@types/bun": "latest",
|
|
62
|
+
"vitest": "^4.1.9",
|
|
63
|
+
"zshy": "^0.7.2"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@standard-schema/spec": "^1.1.0",
|
|
67
|
+
"msgpackr": "^1.11.9",
|
|
68
|
+
"zod": "^4.3.6"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/buffered.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync snapshots of async-bodied web types (`Request`, `Response`, `Blob`,
|
|
3
|
+
* `File`) after their bodies have been drained. msgpackr's extension `write`
|
|
4
|
+
* callback is synchronous, so anything with an async body must be pre-processed
|
|
5
|
+
* into one of these before packing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `Uint8Array<ArrayBuffer>` — the shape `BodyInit`/`BlobPart` expect under DOM
|
|
10
|
+
* lib.
|
|
11
|
+
*/
|
|
12
|
+
type Bytes = Uint8Array<ArrayBuffer>
|
|
13
|
+
|
|
14
|
+
const METHODS_WITHOUT_BODY = new Set(["GET", "HEAD"])
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps an `ArrayBuffer` in the byte-array shape accepted by web body APIs.
|
|
18
|
+
*
|
|
19
|
+
* @param buffer - Buffer to expose as bytes.
|
|
20
|
+
*/
|
|
21
|
+
function toBytes(buffer: ArrayBuffer): Bytes {
|
|
22
|
+
return new Uint8Array(buffer)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A serializable snapshot of a request and its buffered body. */
|
|
26
|
+
export class BufferedRequest {
|
|
27
|
+
/**
|
|
28
|
+
* Creates a buffered request snapshot.
|
|
29
|
+
*
|
|
30
|
+
* @param url - Original request URL.
|
|
31
|
+
* @param method - Original HTTP method.
|
|
32
|
+
* @param headers - Original headers as string entries.
|
|
33
|
+
* @param body - Buffered body, or `null` when the request has no body.
|
|
34
|
+
*/
|
|
35
|
+
constructor(
|
|
36
|
+
public readonly url: string,
|
|
37
|
+
public readonly method: string,
|
|
38
|
+
public readonly headers: Record<string, string>,
|
|
39
|
+
public readonly body: Bytes | null,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Drains a request into a serializable snapshot.
|
|
44
|
+
*
|
|
45
|
+
* @param req - Request to buffer.
|
|
46
|
+
*/
|
|
47
|
+
static async from(req: Request): Promise<BufferedRequest> {
|
|
48
|
+
return new BufferedRequest(
|
|
49
|
+
req.url,
|
|
50
|
+
req.method,
|
|
51
|
+
Object.fromEntries(req.headers),
|
|
52
|
+
req.body !== null && !METHODS_WITHOUT_BODY.has(req.method)
|
|
53
|
+
? toBytes(await req.arrayBuffer())
|
|
54
|
+
: null,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reconstructs a request from this snapshot. */
|
|
59
|
+
toRequest(): Request {
|
|
60
|
+
return new Request(this.url, {
|
|
61
|
+
method: this.method,
|
|
62
|
+
headers: new Headers(this.headers),
|
|
63
|
+
body:
|
|
64
|
+
this.body !== null && !METHODS_WITHOUT_BODY.has(this.method)
|
|
65
|
+
? this.body
|
|
66
|
+
: undefined,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A serializable snapshot of a response and its buffered body. */
|
|
72
|
+
export class BufferedResponse {
|
|
73
|
+
/**
|
|
74
|
+
* Creates a buffered response snapshot.
|
|
75
|
+
*
|
|
76
|
+
* @param status - Original response status code.
|
|
77
|
+
* @param statusText - Original response status text.
|
|
78
|
+
* @param headers - Original headers as string entries.
|
|
79
|
+
* @param body - Buffered body, or `null` when the response has no body.
|
|
80
|
+
*/
|
|
81
|
+
constructor(
|
|
82
|
+
public readonly status: number,
|
|
83
|
+
public readonly statusText: string,
|
|
84
|
+
public readonly headers: Record<string, string>,
|
|
85
|
+
public readonly body: Bytes | null,
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Drains a response into a serializable snapshot.
|
|
90
|
+
*
|
|
91
|
+
* @param res - Response to buffer.
|
|
92
|
+
*/
|
|
93
|
+
static async from(res: Response): Promise<BufferedResponse> {
|
|
94
|
+
return new BufferedResponse(
|
|
95
|
+
res.status,
|
|
96
|
+
res.statusText,
|
|
97
|
+
Object.fromEntries(res.headers),
|
|
98
|
+
res.body !== null ? toBytes(await res.arrayBuffer()) : null,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Reconstructs a response from this snapshot. */
|
|
103
|
+
toResponse(): Response {
|
|
104
|
+
return new Response(this.body, {
|
|
105
|
+
status: this.status,
|
|
106
|
+
statusText: this.statusText,
|
|
107
|
+
headers: new Headers(this.headers),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A serializable snapshot of a blob and its buffered bytes. */
|
|
113
|
+
export class BufferedBlob {
|
|
114
|
+
/**
|
|
115
|
+
* Creates a buffered blob snapshot.
|
|
116
|
+
*
|
|
117
|
+
* @param type - Original media type.
|
|
118
|
+
* @param bytes - Buffered blob contents.
|
|
119
|
+
*/
|
|
120
|
+
constructor(
|
|
121
|
+
public readonly type: string,
|
|
122
|
+
public readonly bytes: Bytes,
|
|
123
|
+
) {}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Drains a blob into a serializable snapshot.
|
|
127
|
+
*
|
|
128
|
+
* @param blob - Blob to buffer.
|
|
129
|
+
*/
|
|
130
|
+
static async from(blob: Blob): Promise<BufferedBlob> {
|
|
131
|
+
return new BufferedBlob(blob.type, toBytes(await blob.arrayBuffer()))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Reconstructs a blob from this snapshot. */
|
|
135
|
+
toBlob(): Blob {
|
|
136
|
+
return new Blob([this.bytes], { type: this.type })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A serializable snapshot of a file and its buffered bytes. */
|
|
141
|
+
export class BufferedFile {
|
|
142
|
+
/**
|
|
143
|
+
* Creates a buffered file snapshot.
|
|
144
|
+
*
|
|
145
|
+
* @param name - Original file name.
|
|
146
|
+
* @param type - Original media type.
|
|
147
|
+
* @param lastModified - Original modification timestamp.
|
|
148
|
+
* @param bytes - Buffered file contents.
|
|
149
|
+
*/
|
|
150
|
+
constructor(
|
|
151
|
+
public readonly name: string,
|
|
152
|
+
public readonly type: string,
|
|
153
|
+
public readonly lastModified: number,
|
|
154
|
+
public readonly bytes: Bytes,
|
|
155
|
+
) {}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Drains a file into a serializable snapshot.
|
|
159
|
+
*
|
|
160
|
+
* @param file - File to buffer.
|
|
161
|
+
*/
|
|
162
|
+
static async from(file: File): Promise<BufferedFile> {
|
|
163
|
+
return new BufferedFile(
|
|
164
|
+
file.name,
|
|
165
|
+
file.type,
|
|
166
|
+
file.lastModified,
|
|
167
|
+
toBytes(await file.arrayBuffer()),
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Reconstructs a file from this snapshot. */
|
|
172
|
+
toFile(): File {
|
|
173
|
+
return new File([this.bytes], this.name, {
|
|
174
|
+
type: this.type,
|
|
175
|
+
lastModified: this.lastModified,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { addExtension } from "msgpackr"
|
|
2
|
+
import {
|
|
3
|
+
BufferedBlob,
|
|
4
|
+
BufferedFile,
|
|
5
|
+
BufferedRequest,
|
|
6
|
+
BufferedResponse,
|
|
7
|
+
} from "./buffered"
|
|
8
|
+
import { EXT_CODES } from "./type-codes"
|
|
9
|
+
|
|
10
|
+
addExtension({
|
|
11
|
+
Class: BufferedRequest,
|
|
12
|
+
type: EXT_CODES.REQUEST,
|
|
13
|
+
write(instance: BufferedRequest) {
|
|
14
|
+
return [instance.url, instance.method, instance.headers, instance.body]
|
|
15
|
+
},
|
|
16
|
+
read([url, method, headers, body]: [
|
|
17
|
+
string,
|
|
18
|
+
string,
|
|
19
|
+
Record<string, string>,
|
|
20
|
+
Uint8Array<ArrayBuffer> | null,
|
|
21
|
+
]) {
|
|
22
|
+
return new BufferedRequest(url, method, headers, body).toRequest()
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
addExtension({
|
|
27
|
+
Class: BufferedResponse,
|
|
28
|
+
type: EXT_CODES.RESPONSE,
|
|
29
|
+
write(instance: BufferedResponse) {
|
|
30
|
+
return [
|
|
31
|
+
instance.status,
|
|
32
|
+
instance.statusText,
|
|
33
|
+
instance.headers,
|
|
34
|
+
instance.body,
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
read([status, statusText, headers, body]: [
|
|
38
|
+
number,
|
|
39
|
+
string,
|
|
40
|
+
Record<string, string>,
|
|
41
|
+
Uint8Array<ArrayBuffer> | null,
|
|
42
|
+
]) {
|
|
43
|
+
return new BufferedResponse(status, statusText, headers, body).toResponse()
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
addExtension({
|
|
48
|
+
Class: BufferedBlob,
|
|
49
|
+
type: EXT_CODES.BLOB,
|
|
50
|
+
write(instance: BufferedBlob) {
|
|
51
|
+
return [instance.type, instance.bytes]
|
|
52
|
+
},
|
|
53
|
+
read([type, bytes]: [string, Uint8Array<ArrayBuffer>]) {
|
|
54
|
+
return new BufferedBlob(type, bytes).toBlob()
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
addExtension({
|
|
59
|
+
Class: BufferedFile,
|
|
60
|
+
type: EXT_CODES.FILE,
|
|
61
|
+
write(instance: BufferedFile) {
|
|
62
|
+
return [instance.name, instance.type, instance.lastModified, instance.bytes]
|
|
63
|
+
},
|
|
64
|
+
read([name, type, lastModified, bytes]: [
|
|
65
|
+
string,
|
|
66
|
+
string,
|
|
67
|
+
number,
|
|
68
|
+
Uint8Array<ArrayBuffer>,
|
|
69
|
+
]) {
|
|
70
|
+
return new BufferedFile(name, type, lastModified, bytes).toFile()
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
addExtension({
|
|
75
|
+
Class: Headers,
|
|
76
|
+
type: EXT_CODES.HEADERS,
|
|
77
|
+
write: (h: Headers) => Object.fromEntries(h),
|
|
78
|
+
read: (entries: Record<string, string>) => new Headers(entries),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
addExtension({
|
|
82
|
+
Class: URL,
|
|
83
|
+
type: EXT_CODES.URL,
|
|
84
|
+
write: (u: URL) => u.href,
|
|
85
|
+
read: (href: string) => new URL(href),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
addExtension({
|
|
89
|
+
Class: URLSearchParams,
|
|
90
|
+
type: EXT_CODES.URL_SEARCH_PARAMS,
|
|
91
|
+
write: (p: URLSearchParams) => [...p.entries()],
|
|
92
|
+
read: (entries: [string, string][]) => new URLSearchParams(entries),
|
|
93
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import z from "zod"
|
|
2
|
+
import { isPlainObject } from "./lib/utils"
|
|
3
|
+
import { packr } from "./packr"
|
|
4
|
+
import { bufferAsyncValues } from "./walk"
|
|
5
|
+
|
|
6
|
+
export type { ObjectProducingSchema, ProducingSchema } from "./schema"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Values accepted by the codec encoder, including recursively nested containers
|
|
10
|
+
* and registered web-platform extension types.
|
|
11
|
+
*/
|
|
12
|
+
export type Encodable =
|
|
13
|
+
// primitives
|
|
14
|
+
| null
|
|
15
|
+
| undefined
|
|
16
|
+
| void
|
|
17
|
+
| boolean
|
|
18
|
+
| number
|
|
19
|
+
| bigint
|
|
20
|
+
| string
|
|
21
|
+
// typed arrays
|
|
22
|
+
| Uint8Array
|
|
23
|
+
| Uint8ClampedArray
|
|
24
|
+
| Int8Array
|
|
25
|
+
| Uint16Array
|
|
26
|
+
| Int16Array
|
|
27
|
+
| Uint32Array
|
|
28
|
+
| Int32Array
|
|
29
|
+
| Float32Array
|
|
30
|
+
| Float64Array
|
|
31
|
+
| BigUint64Array
|
|
32
|
+
| BigInt64Array
|
|
33
|
+
// other types
|
|
34
|
+
| Encodable[]
|
|
35
|
+
| readonly Encodable[]
|
|
36
|
+
| { readonly [key: string]: Encodable }
|
|
37
|
+
| Map<Encodable, Encodable>
|
|
38
|
+
| Set<Encodable>
|
|
39
|
+
| Date
|
|
40
|
+
| RegExp
|
|
41
|
+
| ArrayBuffer
|
|
42
|
+
| DataView
|
|
43
|
+
| Request
|
|
44
|
+
| Response
|
|
45
|
+
| Blob
|
|
46
|
+
| File
|
|
47
|
+
| Headers
|
|
48
|
+
| URL
|
|
49
|
+
| URLSearchParams
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Asserts that a value belongs to the codec's supported input surface.
|
|
53
|
+
*
|
|
54
|
+
* @param value - Value to validate.
|
|
55
|
+
* @throws {TypeError} When the value cannot be encoded.
|
|
56
|
+
*/
|
|
57
|
+
function assertEncodable(value: unknown): asserts value is Encodable {
|
|
58
|
+
if (!isEncodable(value)) {
|
|
59
|
+
throw new TypeError(
|
|
60
|
+
`Value is not encodable: ${Object.prototype.toString.call(value)}`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks whether an unknown value fits the codec's supported input surface.
|
|
67
|
+
*
|
|
68
|
+
* @param value - Value to inspect.
|
|
69
|
+
*/
|
|
70
|
+
export function isEncodable(value: unknown): value is Encodable {
|
|
71
|
+
const activePath = new WeakSet<object>()
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validates one value while tracking the active traversal path for cycles.
|
|
75
|
+
*
|
|
76
|
+
* @param value - Value at the current traversal position.
|
|
77
|
+
*/
|
|
78
|
+
function visit(value: unknown): value is Encodable {
|
|
79
|
+
if (value == null) return true
|
|
80
|
+
|
|
81
|
+
switch (typeof value) {
|
|
82
|
+
case "boolean":
|
|
83
|
+
case "bigint":
|
|
84
|
+
case "number":
|
|
85
|
+
case "string":
|
|
86
|
+
return true
|
|
87
|
+
case "function":
|
|
88
|
+
case "symbol":
|
|
89
|
+
return false
|
|
90
|
+
case "object":
|
|
91
|
+
break
|
|
92
|
+
default:
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
value instanceof Date ||
|
|
98
|
+
value instanceof RegExp ||
|
|
99
|
+
value instanceof ArrayBuffer ||
|
|
100
|
+
ArrayBuffer.isView(value) ||
|
|
101
|
+
value instanceof Request ||
|
|
102
|
+
value instanceof Response ||
|
|
103
|
+
value instanceof Blob ||
|
|
104
|
+
value instanceof File ||
|
|
105
|
+
value instanceof Headers ||
|
|
106
|
+
value instanceof URL ||
|
|
107
|
+
value instanceof URLSearchParams
|
|
108
|
+
) {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (activePath.has(value)) return false
|
|
113
|
+
|
|
114
|
+
activePath.add(value)
|
|
115
|
+
try {
|
|
116
|
+
return Array.isArray(value)
|
|
117
|
+
? value.every(visit)
|
|
118
|
+
: value instanceof Map
|
|
119
|
+
? [...value].every(([key, item]) => visit(key) && visit(item))
|
|
120
|
+
: value instanceof Set
|
|
121
|
+
? [...value].every(visit)
|
|
122
|
+
: isPlainObject(value) && Object.values(value).every(visit)
|
|
123
|
+
} finally {
|
|
124
|
+
activePath.delete(value)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return visit(value)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Zod schema for values accepted by the codec encoder. */
|
|
132
|
+
export const encodableSchema = z.custom<Encodable>(isEncodable, {
|
|
133
|
+
message: "Value is not encodable",
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Serializes a value after checking it against the codec's supported input
|
|
138
|
+
* surface.
|
|
139
|
+
*
|
|
140
|
+
* Pre-walks the value to buffer any async-bodied values (`Request`, `Response`)
|
|
141
|
+
* before handing off to the synchronous packer.
|
|
142
|
+
*
|
|
143
|
+
* @param value - Value to validate and serialize.
|
|
144
|
+
* @throws {TypeError} When the value is outside the supported input surface.
|
|
145
|
+
*/
|
|
146
|
+
export async function encode(value: unknown): Promise<Uint8Array> {
|
|
147
|
+
assertEncodable(value)
|
|
148
|
+
return packr.pack(await bufferAsyncValues(value))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deserializes msgpack bytes.
|
|
153
|
+
*
|
|
154
|
+
* Currently synchronous under the hood, but typed as async to leave room for
|
|
155
|
+
* future concerns that need await (e.g. resolving blob refs from external
|
|
156
|
+
* storage, streaming decodes).
|
|
157
|
+
*
|
|
158
|
+
* Returns the codec's supported value surface. Callers should still validate
|
|
159
|
+
* the domain-specific shape where they know the expected type.
|
|
160
|
+
*
|
|
161
|
+
* @param bytes - Msgpack bytes to deserialize.
|
|
162
|
+
* @throws {TypeError} When the decoded payload is outside the supported
|
|
163
|
+
* surface.
|
|
164
|
+
*/
|
|
165
|
+
export async function decode(bytes: Uint8Array): Promise<Encodable> {
|
|
166
|
+
const value = packr.unpack(bytes)
|
|
167
|
+
assertEncodable(value)
|
|
168
|
+
return value
|
|
169
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks whether an object has the standard object prototype or no prototype.
|
|
3
|
+
*
|
|
4
|
+
* @param value - Object to inspect.
|
|
5
|
+
*/
|
|
6
|
+
export function isPlainObject(value: object): value is Record<string, unknown> {
|
|
7
|
+
const proto = Object.getPrototypeOf(value)
|
|
8
|
+
return proto === Object.prototype || proto === null
|
|
9
|
+
}
|
package/src/packr.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Packr } from "msgpackr"
|
|
2
|
+
import "./extensions"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single configured `Packr` instance used for all encode/decode in the runtime.
|
|
6
|
+
*
|
|
7
|
+
* Extensions are registered globally via `addExtension` (see `./extensions`),
|
|
8
|
+
* so any `Packr` instance picks them up — but using one shared instance keeps
|
|
9
|
+
* option configuration consistent.
|
|
10
|
+
*/
|
|
11
|
+
export const packr = new Packr({
|
|
12
|
+
useRecords: true,
|
|
13
|
+
mapsAsObjects: false,
|
|
14
|
+
moreTypes: true,
|
|
15
|
+
int64AsType: "bigint",
|
|
16
|
+
useBigIntExtension: true,
|
|
17
|
+
encodeUndefinedAsNil: false,
|
|
18
|
+
})
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
2
|
+
|
|
3
|
+
/** Standard Schema whose validated output is assignable to the given type. */
|
|
4
|
+
export type ProducingSchema<TOutput> = StandardSchemaV1<
|
|
5
|
+
// oxlint-disable-next-line typescript/no-explicit-any -- schema input variance should not constrain output type
|
|
6
|
+
any,
|
|
7
|
+
TOutput
|
|
8
|
+
>
|
|
9
|
+
|
|
10
|
+
/** Standard Schema whose validated output is a string-keyed object. */
|
|
11
|
+
export type ObjectProducingSchema<TValue> = ProducingSchema<
|
|
12
|
+
Record<string, TValue>
|
|
13
|
+
>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable msgpack extension type codes for built-in runtime value types.
|
|
3
|
+
*
|
|
4
|
+
* These numbers are part of the wire format. Changing an existing code is a
|
|
5
|
+
* breaking change that would invalidate all previously stored outputs. Treat
|
|
6
|
+
* this file as append-only.
|
|
7
|
+
*
|
|
8
|
+
* Msgpackr reserves negative codes for MessagePack itself and 101-127 for its
|
|
9
|
+
* own use. Codes 1-100 are available to built-in application values.
|
|
10
|
+
*/
|
|
11
|
+
export const EXT_CODES = {
|
|
12
|
+
REQUEST: 1,
|
|
13
|
+
RESPONSE: 2,
|
|
14
|
+
BLOB: 3,
|
|
15
|
+
FILE: 4,
|
|
16
|
+
HEADERS: 5,
|
|
17
|
+
URL: 6,
|
|
18
|
+
URL_SEARCH_PARAMS: 7,
|
|
19
|
+
} as const
|