Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit bec1ee6

Browse files
committed
fix(core): fix json encoding
1 parent 0d59297 commit bec1ee6

File tree

5 files changed

+357
-46
lines changed

5 files changed

+357
-46
lines changed

packages/rivetkit/src/actor/protocol/serde.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,81 @@ export function encodeDataToString(message: OutputData): string {
123123
}
124124
}
125125

126+
function base64DecodeToUint8Array(base64: string): Uint8Array {
127+
// Check if Buffer is available (Node.js)
128+
if (typeof Buffer !== "undefined") {
129+
return new Uint8Array(Buffer.from(base64, "base64"));
130+
}
131+
132+
// Browser environment - use atob
133+
const binary = atob(base64);
134+
const len = binary.length;
135+
const bytes = new Uint8Array(len);
136+
for (let i = 0; i < len; i++) {
137+
bytes[i] = binary.charCodeAt(i);
138+
}
139+
return bytes;
140+
}
141+
142+
function base64DecodeToArrayBuffer(base64: string): ArrayBuffer {
143+
return base64DecodeToUint8Array(base64).buffer as ArrayBuffer;
144+
}
145+
126146
/** Stringifies with compat for values that BARE & CBOR supports. */
127147
export function jsonStringifyCompat(input: any): string {
128-
return JSON.stringify(input, (_key, value) =>
129-
typeof value === "bigint" ? value.toString() : value,
130-
);
148+
return JSON.stringify(input, (_key, value) => {
149+
if (typeof value === "bigint") {
150+
return ["$BigInt", value.toString()];
151+
} else if (value instanceof ArrayBuffer) {
152+
return ["$ArrayBuffer", base64EncodeArrayBuffer(value)];
153+
} else if (value instanceof Uint8Array) {
154+
return ["$Uint8Array", base64EncodeUint8Array(value)];
155+
}
156+
157+
// Escape user arrays that start with $ by prepending another $
158+
if (
159+
Array.isArray(value) &&
160+
value.length === 2 &&
161+
typeof value[0] === "string" &&
162+
value[0].startsWith("$")
163+
) {
164+
return ["$" + value[0], value[1]];
165+
}
166+
167+
return value;
168+
});
169+
}
170+
171+
/** Parses JSON with compat for values that BARE & CBOR supports. */
172+
export function jsonParseCompat(input: string): any {
173+
return JSON.parse(input, (_key, value) => {
174+
// Handle arrays with $ prefix
175+
if (
176+
Array.isArray(value) &&
177+
value.length === 2 &&
178+
typeof value[0] === "string" &&
179+
value[0].startsWith("$")
180+
) {
181+
// Known special types
182+
if (value[0] === "$BigInt") {
183+
return BigInt(value[1]);
184+
} else if (value[0] === "$ArrayBuffer") {
185+
return base64DecodeToArrayBuffer(value[1]);
186+
} else if (value[0] === "$Uint8Array") {
187+
return base64DecodeToUint8Array(value[1]);
188+
}
189+
190+
// Unescape user arrays that started with $ ($$foo -> $foo)
191+
if (value[0].startsWith("$$")) {
192+
return [value[0].substring(1), value[1]];
193+
}
194+
195+
// Unknown type starting with $ - this is an error
196+
throw new Error(
197+
`Unknown JSON encoding type: ${value[0]}. This may indicate corrupted data or a version mismatch.`,
198+
);
199+
}
200+
201+
return value;
202+
});
131203
}

packages/rivetkit/src/driver-test-suite/mod.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createNodeWebSocket, type NodeWebSocket } from "@hono/node-ws";
33
import { bundleRequire } from "bundle-require";
44
import invariant from "invariant";
55
import { describe } from "vitest";
6-
import type { Transport } from "@/client/mod";
6+
import type { Encoding, Transport } from "@/client/mod";
77
import { configureInspectorAccessToken } from "@/inspector/utils";
88
import { createManagerRouter } from "@/manager/router";
99
import type { DriverConfig, Registry, RunConfig } from "@/mod";
@@ -55,6 +55,8 @@ export interface DriverTestConfig {
5555

5656
transport?: Transport;
5757

58+
encoding?: Encoding;
59+
5860
clientType: ClientType;
5961

6062
cleanup?: () => Promise<void>;
@@ -78,68 +80,81 @@ export interface DriverDeployOutput {
7880

7981
/** Runs all Vitest tests against the provided drivers. */
8082
export function runDriverTests(
81-
driverTestConfigPartial: Omit<DriverTestConfig, "clientType" | "transport">,
83+
driverTestConfigPartial: Omit<
84+
DriverTestConfig,
85+
"clientType" | "transport" | "encoding"
86+
>,
8287
) {
8388
const clientTypes: ClientType[] = driverTestConfigPartial.skip?.inline
8489
? ["http"]
8590
: ["http", "inline"];
8691
for (const clientType of clientTypes) {
87-
const driverTestConfig: DriverTestConfig = {
88-
...driverTestConfigPartial,
89-
clientType,
90-
};
91-
9292
describe(`client type (${clientType})`, () => {
93-
runActorDriverTests(driverTestConfig);
94-
runManagerDriverTests(driverTestConfig);
93+
const encodings: Encoding[] = ["bare", "cbor", "json"];
9594

96-
const transports: Transport[] = driverTestConfig.skip?.sse
97-
? ["websocket"]
98-
: ["websocket", "sse"];
99-
for (const transport of transports) {
100-
describe(`transport (${transport})`, () => {
101-
runActorConnTests({
102-
...driverTestConfig,
103-
transport,
104-
});
95+
for (const encoding of encodings) {
96+
describe(`encoding (${encoding})`, () => {
97+
const driverTestConfig: DriverTestConfig = {
98+
...driverTestConfigPartial,
99+
clientType,
100+
encoding,
101+
};
105102

106-
runActorConnStateTests({ ...driverTestConfig, transport });
103+
runActorDriverTests(driverTestConfig);
104+
runManagerDriverTests(driverTestConfig);
107105

108-
runActorReconnectTests({ ...driverTestConfig, transport });
106+
const transports: Transport[] = driverTestConfig.skip?.sse
107+
? ["websocket"]
108+
: ["websocket", "sse"];
109+
for (const transport of transports) {
110+
describe(`transport (${transport})`, () => {
111+
runActorConnTests({
112+
...driverTestConfig,
113+
transport,
114+
});
109115

110-
runRequestAccessTests({ ...driverTestConfig, transport });
116+
runActorConnStateTests({ ...driverTestConfig, transport });
111117

112-
runActorDriverTestsWithTransport({ ...driverTestConfig, transport });
113-
});
114-
}
118+
runActorReconnectTests({ ...driverTestConfig, transport });
115119

116-
runActorHandleTests(driverTestConfig);
120+
runRequestAccessTests({ ...driverTestConfig, transport });
117121

118-
runActionFeaturesTests(driverTestConfig);
122+
runActorDriverTestsWithTransport({
123+
...driverTestConfig,
124+
transport,
125+
});
126+
});
127+
}
119128

120-
runActorVarsTests(driverTestConfig);
129+
runActorHandleTests(driverTestConfig);
121130

122-
runActorMetadataTests(driverTestConfig);
131+
runActionFeaturesTests(driverTestConfig);
123132

124-
runActorOnStateChangeTests(driverTestConfig);
133+
runActorVarsTests(driverTestConfig);
125134

126-
runActorErrorHandlingTests(driverTestConfig);
135+
runActorMetadataTests(driverTestConfig);
127136

128-
runActorInlineClientTests(driverTestConfig);
137+
runActorOnStateChangeTests(driverTestConfig);
129138

130-
runRawHttpTests(driverTestConfig);
139+
runActorErrorHandlingTests(driverTestConfig);
131140

132-
runRawHttpRequestPropertiesTests(driverTestConfig);
141+
runActorInlineClientTests(driverTestConfig);
133142

134-
runRawWebSocketTests(driverTestConfig);
143+
runRawHttpTests(driverTestConfig);
135144

136-
// TODO: re-expose this once we can have actor queries on the gateway
137-
// runRawHttpDirectRegistryTests(driverTestConfig);
145+
runRawHttpRequestPropertiesTests(driverTestConfig);
138146

139-
// TODO: re-expose this once we can have actor queries on the gateway
140-
// runRawWebSocketDirectRegistryTests(driverTestConfig);
147+
runRawWebSocketTests(driverTestConfig);
141148

142-
runActorInspectorTests(driverTestConfig);
149+
// TODO: re-expose this once we can have actor queries on the gateway
150+
// runRawHttpDirectRegistryTests(driverTestConfig);
151+
152+
// TODO: re-expose this once we can have actor queries on the gateway
153+
// runRawWebSocketDirectRegistryTests(driverTestConfig);
154+
155+
runActorInspectorTests(driverTestConfig);
156+
});
157+
}
143158
});
144159
}
145160
}

packages/rivetkit/src/driver-test-suite/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,20 @@ export async function setupDriverTest(
3636
namespace,
3737
runnerName,
3838
transport: driverTestConfig.transport,
39+
encoding: driverTestConfig.encoding,
3940
});
4041
} else if (driverTestConfig.clientType === "inline") {
4142
// Use inline client from driver
4243
const transport = driverTestConfig.transport ?? "websocket";
44+
const encoding = driverTestConfig.encoding ?? "bare";
4345
const managerDriver = createTestInlineClientDriver(
4446
endpoint,
45-
"bare",
47+
encoding,
4648
transport,
4749
);
4850
const runConfig = RunConfigSchema.parse({
4951
transport: transport,
52+
encoding: encoding,
5053
});
5154
client = createClientWithDriver(managerDriver, runConfig);
5255
} else {

packages/rivetkit/src/serde.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import invariant from "invariant";
33
import { assertUnreachable } from "@/common/utils";
44
import type { VersionedDataHandler } from "@/common/versioned-data";
55
import type { Encoding } from "@/mod";
6-
import { jsonStringifyCompat } from "./actor/protocol/serde";
6+
import { jsonParseCompat, jsonStringifyCompat } from "./actor/protocol/serde";
77

88
export function uint8ArrayToBase64(uint8Array: Uint8Array): string {
99
// Check if Buffer is available (Node.js)
@@ -78,11 +78,11 @@ export function deserializeWithEncoding<T>(
7878
): T {
7979
if (encoding === "json") {
8080
if (typeof buffer === "string") {
81-
return JSON.parse(buffer);
81+
return jsonParseCompat(buffer);
8282
} else {
8383
const decoder = new TextDecoder("utf-8");
8484
const jsonString = decoder.decode(buffer);
85-
return JSON.parse(jsonString);
85+
return jsonParseCompat(jsonString);
8686
}
8787
} else if (encoding === "cbor") {
8888
invariant(

0 commit comments

Comments
 (0)