rivetkit 2.0.38 → 2.0.39

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 (93) hide show
  1. package/dist/tsup/{chunk-6WLJW57U.cjs → chunk-7E3RWMR6.cjs} +161 -115
  2. package/dist/tsup/chunk-7E3RWMR6.cjs.map +1 -0
  3. package/dist/tsup/{chunk-FA6FGAEC.js → chunk-BQ36VTSB.js} +74 -28
  4. package/dist/tsup/chunk-BQ36VTSB.js.map +1 -0
  5. package/dist/tsup/{chunk-GFKZZG2A.cjs → chunk-C64FV764.cjs} +3 -3
  6. package/dist/tsup/{chunk-GFKZZG2A.cjs.map → chunk-C64FV764.cjs.map} +1 -1
  7. package/dist/tsup/{chunk-4U45T5KW.js → chunk-CDK6DRO2.js} +2 -2
  8. package/dist/tsup/{chunk-4U45T5KW.js.map → chunk-CDK6DRO2.js.map} +1 -1
  9. package/dist/tsup/{chunk-IRTVRBJA.cjs → chunk-DY4H3ASE.cjs} +50 -46
  10. package/dist/tsup/chunk-DY4H3ASE.cjs.map +1 -0
  11. package/dist/tsup/{chunk-IWXMFQDT.cjs → chunk-KMYFL3OL.cjs} +264 -77
  12. package/dist/tsup/chunk-KMYFL3OL.cjs.map +1 -0
  13. package/dist/tsup/{chunk-FZQHTGQX.cjs → chunk-MZPYVTVG.cjs} +9 -9
  14. package/dist/tsup/{chunk-FZQHTGQX.cjs.map → chunk-MZPYVTVG.cjs.map} +1 -1
  15. package/dist/tsup/{chunk-K2RNF2ZR.js → chunk-OJZRCEIA.js} +5 -5
  16. package/dist/tsup/{chunk-WIZ4JGP6.js → chunk-PHCD25XO.js} +2 -2
  17. package/dist/tsup/{chunk-MIOU6BF3.js → chunk-PVKUXMOA.js} +209 -22
  18. package/dist/tsup/chunk-PVKUXMOA.js.map +1 -0
  19. package/dist/tsup/{chunk-UUEZVDRL.js → chunk-T7IPDBWH.js} +8 -4
  20. package/dist/tsup/{chunk-UUEZVDRL.js.map → chunk-T7IPDBWH.js.map} +1 -1
  21. package/dist/tsup/{chunk-LULP6HM2.cjs → chunk-UAX5E3EU.cjs} +311 -288
  22. package/dist/tsup/chunk-UAX5E3EU.cjs.map +1 -0
  23. package/dist/tsup/{chunk-O433HWWG.cjs → chunk-X72X7I7T.cjs} +2 -2
  24. package/dist/tsup/{chunk-O433HWWG.cjs.map → chunk-X72X7I7T.cjs.map} +1 -1
  25. package/dist/tsup/{chunk-EEL32AJM.js → chunk-XU74APB4.js} +72 -49
  26. package/dist/tsup/chunk-XU74APB4.js.map +1 -0
  27. package/dist/tsup/client/mod.cjs +5 -5
  28. package/dist/tsup/client/mod.d.cts +3 -3
  29. package/dist/tsup/client/mod.d.ts +3 -3
  30. package/dist/tsup/client/mod.js +4 -4
  31. package/dist/tsup/common/log.cjs +2 -2
  32. package/dist/tsup/common/log.js +1 -1
  33. package/dist/tsup/common/websocket.cjs +3 -3
  34. package/dist/tsup/common/websocket.js +2 -2
  35. package/dist/tsup/{config-CwJCQyP1.d.cts → config-BuBlMs6C.d.cts} +72 -5
  36. package/dist/tsup/{config-CbIHPGKl.d.ts → config-CBwo4ooA.d.ts} +72 -5
  37. package/dist/tsup/{driver-Lw_oORox.d.cts → driver-CPXmh8f8.d.cts} +1 -1
  38. package/dist/tsup/{driver-CMN823Lc.d.ts → driver-DxWa6HUO.d.ts} +1 -1
  39. package/dist/tsup/driver-helpers/mod.cjs +3 -3
  40. package/dist/tsup/driver-helpers/mod.d.cts +2 -2
  41. package/dist/tsup/driver-helpers/mod.d.ts +2 -2
  42. package/dist/tsup/driver-helpers/mod.js +2 -2
  43. package/dist/tsup/driver-test-suite/mod.cjs +81 -35
  44. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -1
  45. package/dist/tsup/driver-test-suite/mod.d.cts +2 -2
  46. package/dist/tsup/driver-test-suite/mod.d.ts +2 -2
  47. package/dist/tsup/driver-test-suite/mod.js +407 -361
  48. package/dist/tsup/driver-test-suite/mod.js.map +1 -1
  49. package/dist/tsup/{kv-CTM8sCvx.d.cts → keys-Chhy4ylv.d.cts} +1 -0
  50. package/dist/tsup/{kv-CTM8sCvx.d.ts → keys-Chhy4ylv.d.ts} +1 -0
  51. package/dist/tsup/mod.cjs +9 -7
  52. package/dist/tsup/mod.cjs.map +1 -1
  53. package/dist/tsup/mod.d.cts +5 -5
  54. package/dist/tsup/mod.d.ts +5 -5
  55. package/dist/tsup/mod.js +8 -6
  56. package/dist/tsup/test/mod.cjs +7 -7
  57. package/dist/tsup/test/mod.d.cts +1 -1
  58. package/dist/tsup/test/mod.d.ts +1 -1
  59. package/dist/tsup/test/mod.js +6 -6
  60. package/dist/tsup/utils.cjs +2 -2
  61. package/dist/tsup/utils.js +1 -1
  62. package/package.json +2 -2
  63. package/src/actor/config.ts +183 -34
  64. package/src/actor/contexts/base/actor.ts +12 -0
  65. package/src/actor/instance/connection-manager.ts +1 -1
  66. package/src/actor/instance/keys.ts +29 -0
  67. package/src/actor/instance/kv.ts +240 -14
  68. package/src/actor/instance/mod.ts +5 -4
  69. package/src/actor/instance/state-manager.ts +1 -1
  70. package/src/actor/mod.ts +2 -1
  71. package/src/actor/router-websocket-endpoints.ts +2 -1
  72. package/src/client/actor-handle.ts +13 -3
  73. package/src/client/mod.ts +1 -1
  74. package/src/driver-helpers/utils.ts +1 -1
  75. package/src/driver-test-suite/mod.ts +3 -0
  76. package/src/driver-test-suite/test-inline-client-driver.ts +3 -0
  77. package/src/driver-test-suite/tests/actor-kv.ts +44 -0
  78. package/src/driver-test-suite/utils.ts +4 -0
  79. package/src/drivers/engine/actor-driver.ts +3 -3
  80. package/src/drivers/file-system/manager.ts +5 -0
  81. package/src/manager/driver.ts +7 -0
  82. package/src/remote-manager-driver/actor-http-client.ts +5 -3
  83. package/src/remote-manager-driver/actor-websocket-client.ts +18 -7
  84. package/src/remote-manager-driver/mod.ts +10 -0
  85. package/dist/tsup/chunk-6WLJW57U.cjs.map +0 -1
  86. package/dist/tsup/chunk-EEL32AJM.js.map +0 -1
  87. package/dist/tsup/chunk-FA6FGAEC.js.map +0 -1
  88. package/dist/tsup/chunk-IRTVRBJA.cjs.map +0 -1
  89. package/dist/tsup/chunk-IWXMFQDT.cjs.map +0 -1
  90. package/dist/tsup/chunk-LULP6HM2.cjs.map +0 -1
  91. package/dist/tsup/chunk-MIOU6BF3.js.map +0 -1
  92. /package/dist/tsup/{chunk-K2RNF2ZR.js.map → chunk-OJZRCEIA.js.map} +0 -0
  93. /package/dist/tsup/{chunk-WIZ4JGP6.js.map → chunk-PHCD25XO.js.map} +0 -0
@@ -1,15 +1,241 @@
1
- export const KEYS = {
2
- PERSIST_DATA: Uint8Array.from([1]),
3
- CONN_PREFIX: Uint8Array.from([2]), // Prefix for connection keys
4
- INSPECTOR_TOKEN: Uint8Array.from([3]), // Inspector token key
5
- };
6
-
7
- // Helper to create a connection key
8
- export function makeConnKey(connId: string): Uint8Array {
9
- const encoder = new TextEncoder();
10
- const connIdBytes = encoder.encode(connId);
11
- const key = new Uint8Array(KEYS.CONN_PREFIX.length + connIdBytes.length);
12
- key.set(KEYS.CONN_PREFIX, 0);
13
- key.set(connIdBytes, KEYS.CONN_PREFIX.length);
14
- return key;
1
+ import type { ActorDriver } from "../driver";
2
+ import { makePrefixedKey, removePrefixFromKey } from "./keys";
3
+
4
+ /**
5
+ * User-facing KV storage interface exposed on ActorContext.
6
+ */
7
+ type KvValueType = "text" | "arrayBuffer" | "binary";
8
+ type KvKeyType = "text" | "binary";
9
+ type KvKey = Uint8Array | string;
10
+
11
+ type KvValueTypeMap = {
12
+ text: string;
13
+ arrayBuffer: ArrayBuffer;
14
+ binary: Uint8Array;
15
+ };
16
+
17
+ type KvKeyTypeMap = {
18
+ text: string;
19
+ binary: Uint8Array;
20
+ };
21
+
22
+ type KvValueOptions<T extends KvValueType = "text"> = {
23
+ type?: T;
24
+ };
25
+
26
+ type KvListOptions<
27
+ T extends KvValueType = "text",
28
+ K extends KvKeyType = "text",
29
+ > = KvValueOptions<T> & {
30
+ keyType?: K;
31
+ };
32
+
33
+ const textEncoder = new TextEncoder();
34
+ const textDecoder = new TextDecoder();
35
+
36
+ function encodeKey<K extends KvKeyType = KvKeyType>(
37
+ key: KvKeyTypeMap[K],
38
+ keyType?: K,
39
+ ): Uint8Array {
40
+ if (key instanceof Uint8Array) {
41
+ return key;
42
+ }
43
+ const resolvedKeyType = keyType ?? "text";
44
+ if (resolvedKeyType === "binary") {
45
+ throw new TypeError("Expected a Uint8Array when keyType is binary");
46
+ }
47
+ return textEncoder.encode(key);
48
+ }
49
+
50
+ function decodeKey<K extends KvKeyType = "text">(
51
+ key: Uint8Array,
52
+ keyType?: K,
53
+ ): KvKeyTypeMap[K] {
54
+ const resolvedKeyType = keyType ?? "text";
55
+ switch (resolvedKeyType) {
56
+ case "text":
57
+ return textDecoder.decode(key) as KvKeyTypeMap[K];
58
+ case "binary":
59
+ return key as KvKeyTypeMap[K];
60
+ default:
61
+ throw new TypeError("Invalid kv key type");
62
+ }
63
+ }
64
+
65
+ function resolveValueType(
66
+ value: string | Uint8Array | ArrayBuffer,
67
+ ): KvValueType {
68
+ if (typeof value === "string") {
69
+ return "text";
70
+ }
71
+ if (value instanceof Uint8Array) {
72
+ return "binary";
73
+ }
74
+ if (value instanceof ArrayBuffer) {
75
+ return "arrayBuffer";
76
+ }
77
+ throw new TypeError("Invalid kv value");
78
+ }
79
+
80
+ function encodeValue<T extends KvValueType = KvValueType>(
81
+ value: KvValueTypeMap[T],
82
+ options?: KvValueOptions<T>,
83
+ ): Uint8Array {
84
+ const type =
85
+ options?.type ??
86
+ resolveValueType(value as string | Uint8Array | ArrayBuffer);
87
+ switch (type) {
88
+ case "text":
89
+ if (typeof value !== "string") {
90
+ throw new TypeError("Expected a string when type is text");
91
+ }
92
+ return textEncoder.encode(value);
93
+ case "arrayBuffer":
94
+ if (!(value instanceof ArrayBuffer)) {
95
+ throw new TypeError("Expected an ArrayBuffer when type is arrayBuffer");
96
+ }
97
+ return new Uint8Array(value);
98
+ case "binary":
99
+ if (!(value instanceof Uint8Array)) {
100
+ throw new TypeError("Expected a Uint8Array when type is binary");
101
+ }
102
+ return value;
103
+ default:
104
+ throw new TypeError("Invalid kv value type");
105
+ }
106
+ }
107
+
108
+ function decodeValue<T extends KvValueType = "text">(
109
+ value: Uint8Array,
110
+ options?: KvValueOptions<T>,
111
+ ): KvValueTypeMap[T] {
112
+ const type = options?.type ?? "text";
113
+ switch (type) {
114
+ case "text":
115
+ return textDecoder.decode(value) as KvValueTypeMap[T];
116
+ case "arrayBuffer": {
117
+ const copy = new Uint8Array(value.byteLength);
118
+ copy.set(value);
119
+ return copy.buffer as KvValueTypeMap[T];
120
+ }
121
+ case "binary":
122
+ return value as KvValueTypeMap[T];
123
+ default:
124
+ throw new TypeError("Invalid kv value type");
125
+ }
126
+ }
127
+
128
+ export class ActorKv {
129
+ #driver: ActorDriver;
130
+ #actorId: string;
131
+
132
+ constructor(driver: ActorDriver, actorId: string) {
133
+ this.#driver = driver;
134
+ this.#actorId = actorId;
135
+ }
136
+
137
+ /**
138
+ * Get a single value by key.
139
+ */
140
+ async get<T extends KvValueType = "text">(
141
+ key: KvKey,
142
+ options?: KvValueOptions<T>,
143
+ ): Promise<KvValueTypeMap[T] | null> {
144
+ const results = await this.#driver.kvBatchGet(this.#actorId, [
145
+ makePrefixedKey(encodeKey(key)),
146
+ ]);
147
+ const result = results[0];
148
+ if (!result) {
149
+ return null;
150
+ }
151
+ return decodeValue(result, options);
152
+ }
153
+
154
+ /**
155
+ * Get multiple values by keys.
156
+ */
157
+ async getBatch<T extends KvValueType = "text">(
158
+ keys: KvKey[],
159
+ options?: KvValueOptions<T>,
160
+ ): Promise<(KvValueTypeMap[T] | null)[]> {
161
+ const prefixedKeys = keys.map((key) =>
162
+ makePrefixedKey(encodeKey(key)),
163
+ );
164
+ const results = await this.#driver.kvBatchGet(
165
+ this.#actorId,
166
+ prefixedKeys,
167
+ );
168
+ return results.map((result) =>
169
+ result ? decodeValue(result, options) : null,
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Put a single key-value pair.
175
+ */
176
+ async put<T extends KvValueType = KvValueType>(
177
+ key: KvKey,
178
+ value: KvValueTypeMap[T],
179
+ options?: KvValueOptions<T>,
180
+ ): Promise<void> {
181
+ await this.#driver.kvBatchPut(this.#actorId, [
182
+ [makePrefixedKey(encodeKey(key)), encodeValue(value, options)],
183
+ ]);
184
+ }
185
+
186
+ /**
187
+ * Put multiple key-value pairs.
188
+ */
189
+ async putBatch<T extends KvValueType = KvValueType>(
190
+ entries: [KvKey, KvValueTypeMap[T]][],
191
+ options?: KvValueOptions<T>,
192
+ ): Promise<void> {
193
+ const prefixedEntries: [Uint8Array, Uint8Array][] = entries.map(
194
+ ([key, value]) => [
195
+ makePrefixedKey(encodeKey(key)),
196
+ encodeValue(value, options),
197
+ ],
198
+ );
199
+ await this.#driver.kvBatchPut(this.#actorId, prefixedEntries);
200
+ }
201
+
202
+ /**
203
+ * Delete a single key.
204
+ */
205
+ async delete(key: KvKey): Promise<void> {
206
+ await this.#driver.kvBatchDelete(this.#actorId, [
207
+ makePrefixedKey(encodeKey(key)),
208
+ ]);
209
+ }
210
+
211
+ /**
212
+ * Delete multiple keys.
213
+ */
214
+ async deleteBatch(keys: KvKey[]): Promise<void> {
215
+ const prefixedKeys = keys.map((key) =>
216
+ makePrefixedKey(encodeKey(key)),
217
+ );
218
+ await this.#driver.kvBatchDelete(this.#actorId, prefixedKeys);
219
+ }
220
+
221
+ /**
222
+ * List all keys with a given prefix.
223
+ * Returns key-value pairs where keys have the user prefix removed.
224
+ */
225
+ async list<T extends KvValueType = "text", K extends KvKeyType = "text">(
226
+ prefix: KvKeyTypeMap[K],
227
+ options?: KvListOptions<T, K>,
228
+ ): Promise<[KvKeyTypeMap[K], KvValueTypeMap[T]][]> {
229
+ const prefixedPrefix = makePrefixedKey(
230
+ encodeKey(prefix, options?.keyType),
231
+ );
232
+ const results = await this.#driver.kvListPrefix(
233
+ this.#actorId,
234
+ prefixedPrefix,
235
+ );
236
+ return results.map(([key, value]) => [
237
+ decodeKey<K>(removePrefixFromKey(key), options?.keyType),
238
+ decodeValue<T>(value, options),
239
+ ]);
240
+ }
15
241
  }
@@ -44,7 +44,7 @@ import {
44
44
  } from "../utils";
45
45
  import { ConnectionManager } from "./connection-manager";
46
46
  import { EventManager } from "./event-manager";
47
- import { KEYS } from "./kv";
47
+ import { KEYS } from "./keys";
48
48
  import {
49
49
  convertActorFromBarePersisted,
50
50
  type PersistedActor,
@@ -203,7 +203,7 @@ export class ActorInstance<S, CP, CS, V, I, DB extends AnyDatabaseProvider> {
203
203
  }
204
204
 
205
205
  get actions(): string[] {
206
- return Object.keys(this.#config.actions);
206
+ return Object.keys(this.#config.actions ?? {});
207
207
  }
208
208
 
209
209
  get config(): ActorConfig<S, CP, CS, V, I, DB> {
@@ -516,12 +516,13 @@ export class ActorInstance<S, CP, CS, V, I, DB extends AnyDatabaseProvider> {
516
516
  ): Promise<unknown> {
517
517
  this.assertReady();
518
518
 
519
- if (!(actionName in this.#config.actions)) {
519
+ const actions = this.#config.actions ?? {};
520
+ if (!(actionName in actions)) {
520
521
  this.#rLog.warn({ msg: "action does not exist", actionName });
521
522
  throw new errors.ActionNotFound(actionName);
522
523
  }
523
524
 
524
- const actionFunction = this.#config.actions[actionName];
525
+ const actionFunction = actions[actionName];
525
526
  if (typeof actionFunction !== "function") {
526
527
  this.#rLog.warn({
527
528
  msg: "action is not a function",
@@ -12,7 +12,7 @@ import { convertConnToBarePersistedConn } from "../conn/persisted";
12
12
  import type { ActorDriver } from "../driver";
13
13
  import * as errors from "../errors";
14
14
  import { isConnStatePath, isStatePath } from "../utils";
15
- import { KEYS, makeConnKey } from "./kv";
15
+ import { KEYS, makeConnKey } from "./keys";
16
16
  import type { ActorInstance } from "./mod";
17
17
  import { convertActorToBarePersisted, type PersistedActor } from "./persisted";
18
18
 
package/src/actor/mod.ts CHANGED
@@ -76,7 +76,8 @@ export type { AnyConn, Conn } from "./conn/mod";
76
76
  export type { ActorDefinition, AnyActorDefinition } from "./definition";
77
77
  export { lookupInRegistry } from "./definition";
78
78
  export { UserError, type UserErrorOptions } from "./errors";
79
- export { KEYS as KV_KEYS } from "./instance/kv";
79
+ export { KEYS as KV_KEYS } from "./instance/keys";
80
+ export { ActorKv } from "./instance/kv";
80
81
  export type { AnyActorInstance } from "./instance/mod";
81
82
  export {
82
83
  type ActorRouter,
@@ -384,7 +384,8 @@ export function parseWebSocketProtocols(
384
384
  }
385
385
  }
386
386
 
387
- const encoding = EncodingSchema.parse(encodingRaw);
387
+ // Default to "json" encoding for raw WebSocket connections without subprotocols
388
+ const encoding = EncodingSchema.parse(encodingRaw ?? "json");
388
389
  const connParams = connParamsRaw ? JSON.parse(connParamsRaw) : undefined;
389
390
 
390
391
  return { encoding, connParams };
@@ -239,9 +239,7 @@ export class ActorHandleRaw {
239
239
  }
240
240
 
241
241
  /**
242
- * Resolves the actor to get its unique actor ID
243
- *
244
- * @returns {Promise<string>} - A promise that resolves to the actor's ID
242
+ * Resolves the actor to get its unique actor ID.
245
243
  */
246
244
  async resolve({ signal }: { signal?: AbortSignal } = {}): Promise<string> {
247
245
  if (
@@ -277,6 +275,18 @@ export class ActorHandleRaw {
277
275
  assertUnreachable(this.#actorQuery);
278
276
  }
279
277
  }
278
+
279
+ /**
280
+ * Returns the raw URL for routing traffic to the actor.
281
+ */
282
+ async getGatewayUrl(): Promise<string> {
283
+ const { actorId } = await queryActor(
284
+ undefined,
285
+ this.#actorQuery,
286
+ this.#driver,
287
+ );
288
+ return await this.#driver.buildGatewayUrl(actorId);
289
+ }
280
290
  }
281
291
 
282
292
  /**
package/src/client/mod.ts CHANGED
@@ -22,7 +22,7 @@ export {
22
22
  ManagerError,
23
23
  } from "@/client/errors";
24
24
  export type { CreateRequest } from "@/manager/protocol/query";
25
- export { KEYS as KV_KEYS } from "../actor/instance/kv";
25
+ export { KEYS as KV_KEYS } from "../actor/instance/keys";
26
26
  export type { ActorActionFunction } from "./actor-common";
27
27
  export type {
28
28
  ActorConn,
@@ -1,5 +1,5 @@
1
1
  import * as cbor from "cbor-x";
2
- import { KEYS } from "@/actor/instance/kv";
2
+ import { KEYS } from "@/actor/instance/keys";
3
3
  import type * as persistSchema from "@/schemas/actor-persist/mod";
4
4
  import {
5
5
  ACTOR_VERSIONED,
@@ -22,6 +22,7 @@ import { runActorErrorHandlingTests } from "./tests/actor-error-handling";
22
22
  import { runActorHandleTests } from "./tests/actor-handle";
23
23
  import { runActorInlineClientTests } from "./tests/actor-inline-client";
24
24
  import { runActorInspectorTests } from "./tests/actor-inspector";
25
+ import { runActorKvTests } from "./tests/actor-kv";
25
26
  import { runActorMetadataTests } from "./tests/actor-metadata";
26
27
  import { runActorOnStateChangeTests } from "./tests/actor-onstatechange";
27
28
  import { runActorVarsTests } from "./tests/actor-vars";
@@ -122,6 +123,8 @@ export function runDriverTests(
122
123
 
123
124
  runActorInlineClientTests(driverTestConfig);
124
125
 
126
+ runActorKvTests(driverTestConfig);
127
+
125
128
  runRawHttpTests(driverTestConfig);
126
129
 
127
130
  runRawHttpRequestPropertiesTests(driverTestConfig);
@@ -237,6 +237,9 @@ export function createTestInlineClientDriver(
237
237
  );
238
238
  return upgradeWebSocket(() => wsHandler)(c, noopNext());
239
239
  },
240
+ async buildGatewayUrl(actorId: string): Promise<string> {
241
+ return `${endpoint}/gateway/${actorId}`;
242
+ },
240
243
  displayInformation(): ManagerDisplayInformation {
241
244
  return { properties: {} };
242
245
  },
@@ -0,0 +1,44 @@
1
+ import type { DriverTestConfig } from "../mod";
2
+ import { setupDriverTest } from "../utils";
3
+ import { describe, expect, test, type TestContext } from "vitest";
4
+
5
+ export function runActorKvTests(driverTestConfig: DriverTestConfig) {
6
+ describe("Actor KV Tests", () => {
7
+ test("supports text encoding and decoding", async (c: TestContext) => {
8
+ const { client } = await setupDriverTest(c, driverTestConfig);
9
+ const kvHandle = client.kvActor.getOrCreate(["kv-text"]);
10
+
11
+ await kvHandle.putText("greeting", "hello");
12
+ const value = await kvHandle.getText("greeting");
13
+ expect(value).toBe("hello");
14
+
15
+ await kvHandle.putText("prefix-a", "alpha");
16
+ await kvHandle.putText("prefix-b", "beta");
17
+
18
+ const results = await kvHandle.listText("prefix-");
19
+ const sorted = results.sort((a, b) => a.key.localeCompare(b.key));
20
+ expect(sorted).toEqual([
21
+ { key: "prefix-a", value: "alpha" },
22
+ { key: "prefix-b", value: "beta" },
23
+ ]);
24
+ });
25
+
26
+ test(
27
+ "supports arrayBuffer encoding and decoding",
28
+ async (c: TestContext) => {
29
+ const { client } = await setupDriverTest(c, driverTestConfig);
30
+ const kvHandle = client.kvActor.getOrCreate(["kv-array-buffer"]);
31
+
32
+ const values = await kvHandle.roundtripArrayBuffer("bytes", [
33
+ 4,
34
+ 8,
35
+ 15,
36
+ 16,
37
+ 23,
38
+ 42,
39
+ ]);
40
+ expect(values).toEqual([4, 8, 15, 16, 23, 42]);
41
+ },
42
+ );
43
+ });
44
+ }
@@ -39,6 +39,10 @@ export async function setupDriverTest(
39
39
  namespace,
40
40
  runnerName,
41
41
  encoding: driverTestConfig.encoding,
42
+ // Disable metadata lookup to prevent redirect to the wrong port.
43
+ // Each test starts a new server on a dynamic port, but the
44
+ // registry's publicEndpoint defaults to port 6420.
45
+ disableMetadataLookup: true,
42
46
  });
43
47
  } else if (driverTestConfig.clientType === "inline") {
44
48
  // Use inline client from driver
@@ -11,7 +11,7 @@ import { WSContext, type WSContextInit } from "hono/ws";
11
11
  import invariant from "invariant";
12
12
  import { type AnyConn, CONN_STATE_MANAGER_SYMBOL } from "@/actor/conn/mod";
13
13
  import { lookupInRegistry } from "@/actor/definition";
14
- import { KEYS } from "@/actor/instance/kv";
14
+ import { KEYS } from "@/actor/instance/keys";
15
15
  import { deserializeActorKey } from "@/actor/keys";
16
16
  import { getValueLength } from "@/actor/protocol/old";
17
17
  import { type ActorRouter, createActorRouter } from "@/actor/router";
@@ -153,7 +153,7 @@ export class EngineActorDriver implements ActorDriver {
153
153
  onConnected: () => {
154
154
  this.#runnerStarted.resolve(undefined);
155
155
  },
156
- onDisconnected: (_code, _reason) => { },
156
+ onDisconnected: (_code, _reason) => {},
157
157
  onShutdown: () => {
158
158
  this.#runnerStopped.resolve(undefined);
159
159
  this.#isRunnerStopped = true;
@@ -358,7 +358,7 @@ export class EngineActorDriver implements ActorDriver {
358
358
  async serverlessHandleStart(c: HonoContext): Promise<Response> {
359
359
  return streamSSE(c, async (stream) => {
360
360
  // NOTE: onAbort does not work reliably
361
- stream.onAbort(() => { });
361
+ stream.onAbort(() => {});
362
362
  c.req.raw.signal.addEventListener("abort", () => {
363
363
  logger().debug("SSE aborted, shutting down runner");
364
364
 
@@ -147,6 +147,11 @@ export class FileSystemManagerDriver implements ManagerDriver {
147
147
  return upgradeWebSocket(() => wsHandler)(c, noopNext());
148
148
  }
149
149
 
150
+ async buildGatewayUrl(actorId: string): Promise<string> {
151
+ const port = this.#config.managerPort ?? 6420;
152
+ return `http://127.0.0.1:${port}/gateway/${encodeURIComponent(actorId)}`;
153
+ }
154
+
150
155
  async getForId({
151
156
  actorId,
152
157
  }: GetForIdInput): Promise<ActorOutput | undefined> {
@@ -32,6 +32,13 @@ export interface ManagerDriver {
32
32
  params: unknown,
33
33
  ): Promise<Response>;
34
34
 
35
+ /**
36
+ * Build a public gateway URL for a specific actor.
37
+ *
38
+ * This lives on the driver because the base endpoint varies by runtime.
39
+ */
40
+ buildGatewayUrl(actorId: string): Promise<string>;
41
+
35
42
  displayInformation(): ManagerDisplayInformation;
36
43
 
37
44
  extraStartupLog?: () => Record<string, unknown>;
@@ -1,6 +1,6 @@
1
1
  import type { ClientConfig } from "@/client/config";
2
2
  import { HEADER_RIVET_TOKEN } from "@/common/actor-router-consts";
3
- import { combineUrlPath } from "@/utils";
3
+ import { buildActorGatewayUrl } from "./actor-websocket-client";
4
4
  import { getEndpoint } from "./api-utils";
5
5
 
6
6
  export async function sendHttpRequestToActor(
@@ -11,9 +11,11 @@ export async function sendHttpRequestToActor(
11
11
  // Route through guard port
12
12
  const url = new URL(actorRequest.url);
13
13
  const endpoint = getEndpoint(runConfig);
14
- const guardUrl = combineUrlPath(
14
+ const guardUrl = buildActorGatewayUrl(
15
15
  endpoint,
16
- `/gateway/${actorId}${url.pathname}${url.search}`,
16
+ actorId,
17
+ runConfig.token,
18
+ `${url.pathname}${url.search}`,
17
19
  );
18
20
 
19
21
  // Handle body properly based on method and presence
@@ -13,6 +13,18 @@ import { combineUrlPath } from "@/utils";
13
13
  import { getEndpoint } from "./api-utils";
14
14
  import { logger } from "./log";
15
15
 
16
+ export function buildActorGatewayUrl(
17
+ endpoint: string,
18
+ actorId: string,
19
+ token: string | undefined,
20
+ path = "",
21
+ ): string {
22
+ const tokenSegment =
23
+ token !== undefined ? `@${encodeURIComponent(token)}` : "";
24
+ const gatewayPath = `/gateway/${encodeURIComponent(actorId)}${tokenSegment}${path}`;
25
+ return combineUrlPath(endpoint, gatewayPath);
26
+ }
27
+
16
28
  export async function openWebSocketToActor(
17
29
  runConfig: ClientConfig,
18
30
  path: string,
@@ -24,13 +36,12 @@ export async function openWebSocketToActor(
24
36
 
25
37
  // WebSocket connections go through guard
26
38
  const endpoint = getEndpoint(runConfig);
27
- let gatewayPath;
28
- if (runConfig.token !== undefined) {
29
- gatewayPath = `/gateway/${encodeURIComponent(actorId)}@${encodeURIComponent(runConfig.token)}${path}`;
30
- } else {
31
- gatewayPath = `/gateway/${encodeURIComponent(actorId)}${path}`;
32
- }
33
- const guardUrl = combineUrlPath(endpoint, gatewayPath);
39
+ const guardUrl = buildActorGatewayUrl(
40
+ endpoint,
41
+ actorId,
42
+ runConfig.token,
43
+ path,
44
+ );
34
45
 
35
46
  logger().debug({
36
47
  msg: "opening websocket to actor via guard",
@@ -21,6 +21,7 @@ import { combineUrlPath, type GetUpgradeWebSocket } from "@/utils";
21
21
  import { getNextPhase } from "@/utils/env-vars";
22
22
  import { sendHttpRequestToActor } from "./actor-http-client";
23
23
  import {
24
+ buildActorGatewayUrl,
24
25
  buildWebSocketProtocols,
25
26
  openWebSocketToActor,
26
27
  } from "./actor-websocket-client";
@@ -309,6 +310,15 @@ export class RemoteManagerDriver implements ManagerDriver {
309
310
  );
310
311
  }
311
312
 
313
+ async buildGatewayUrl(actorId: string): Promise<string> {
314
+ if (this.#metadataPromise) {
315
+ await this.#metadataPromise;
316
+ }
317
+
318
+ const endpoint = getEndpoint(this.#config);
319
+ return buildActorGatewayUrl(endpoint, actorId, this.#config.token);
320
+ }
321
+
312
322
  async proxyRequest(
313
323
  _c: HonoContext,
314
324
  actorRequest: Request,