rwsdk 0.1.23 → 0.1.24

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.
@@ -6,12 +6,14 @@ export type ActionResponse<Result> = {
6
6
  };
7
7
  type TransportContext = {
8
8
  setRscPayload: <Result>(v: Promise<ActionResponse<Result>>) => void;
9
+ handleResponse?: (response: Response) => boolean;
9
10
  };
10
11
  export type Transport = (context: TransportContext) => CallServerCallback;
11
12
  export type CreateCallServer = (context: TransportContext) => <Result>(id: null | string, args: null | unknown[]) => Promise<Result>;
12
13
  export declare const fetchTransport: Transport;
13
- export declare const initClient: ({ transport, hydrateRootOptions, }?: {
14
+ export declare const initClient: ({ transport, hydrateRootOptions, handleResponse, }?: {
14
15
  transport?: Transport;
15
16
  hydrateRootOptions?: HydrationOptions;
17
+ handleResponse?: (response: Response) => boolean;
16
18
  }) => Promise<void>;
17
19
  export {};
@@ -11,21 +11,41 @@ export const fetchTransport = (transportContext) => {
11
11
  if (id != null) {
12
12
  url.searchParams.set("__rsc_action_id", id);
13
13
  }
14
- const streamData = createFromFetch(fetch(url, {
14
+ const fetchPromise = fetch(url, {
15
15
  method: "POST",
16
16
  body: args != null ? await encodeReply(args) : null,
17
- }), { callServer: fetchCallServer });
17
+ });
18
+ // If there's a response handler, check the response first
19
+ if (transportContext.handleResponse) {
20
+ const response = await fetchPromise;
21
+ const shouldContinue = transportContext.handleResponse(response);
22
+ if (!shouldContinue) {
23
+ return;
24
+ }
25
+ // Continue with the response if handler returned true
26
+ const streamData = createFromFetch(Promise.resolve(response), {
27
+ callServer: fetchCallServer,
28
+ });
29
+ transportContext.setRscPayload(streamData);
30
+ const result = await streamData;
31
+ return result.actionResult;
32
+ }
33
+ // Original behavior when no handler is present
34
+ const streamData = createFromFetch(fetchPromise, {
35
+ callServer: fetchCallServer,
36
+ });
18
37
  transportContext.setRscPayload(streamData);
19
38
  const result = await streamData;
20
39
  return result.actionResult;
21
40
  };
22
41
  return fetchCallServer;
23
42
  };
24
- export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, } = {}) => {
43
+ export const initClient = async ({ transport = fetchTransport, hydrateRootOptions, handleResponse, } = {}) => {
25
44
  const React = await import("react");
26
45
  const { hydrateRoot } = await import("react-dom/client");
27
46
  const transportContext = {
28
47
  setRscPayload: () => { },
48
+ handleResponse,
29
49
  };
30
50
  let transportCallServer = transport(transportContext);
31
51
  const callServer = (id, args) => transportCallServer(id, args);
@@ -1,4 +1,6 @@
1
1
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
2
2
  export declare function initClientNavigation(opts?: {
3
3
  onNavigate: () => void;
4
- }): void;
4
+ }): {
5
+ handleResponse: (response: Response) => boolean;
6
+ };
@@ -50,4 +50,15 @@ export function initClientNavigation(opts = {
50
50
  window.addEventListener("popstate", async function handlePopState() {
51
51
  await opts.onNavigate();
52
52
  });
53
+ // Return a handleResponse function for use with initClient
54
+ return {
55
+ handleResponse: function handleResponse(response) {
56
+ if (!response.ok) {
57
+ // Redirect to the current page (window.location) to show the error
58
+ window.location.href = window.location.href;
59
+ return false;
60
+ }
61
+ return true;
62
+ },
63
+ };
53
64
  }
@@ -1,7 +1,9 @@
1
1
  import { type Transport } from "../../client";
2
- export declare const initRealtimeClient: ({ key, }?: {
2
+ export declare const initRealtimeClient: ({ key, handleResponse, }?: {
3
3
  key?: string;
4
+ handleResponse?: (response: Response) => boolean;
4
5
  }) => Promise<void>;
5
- export declare const realtimeTransport: ({ key }: {
6
+ export declare const realtimeTransport: ({ key, handleResponse, }: {
6
7
  key?: string;
8
+ handleResponse?: (response: Response) => boolean;
7
9
  }) => Transport;
@@ -1,12 +1,13 @@
1
1
  import { initClient } from "../../client";
2
2
  import { createFromReadableStream } from "react-server-dom-webpack/client.browser";
3
3
  import { MESSAGE_TYPE } from "./shared";
4
+ import { packMessage, unpackMessage, } from "./protocol";
4
5
  const DEFAULT_KEY = "default";
5
- export const initRealtimeClient = ({ key = DEFAULT_KEY, } = {}) => {
6
- const transport = realtimeTransport({ key });
7
- return initClient({ transport });
6
+ export const initRealtimeClient = ({ key = DEFAULT_KEY, handleResponse, } = {}) => {
7
+ const transport = realtimeTransport({ key, handleResponse });
8
+ return initClient({ transport, handleResponse });
8
9
  };
9
- export const realtimeTransport = ({ key = DEFAULT_KEY }) => (transportContext) => {
10
+ export const realtimeTransport = ({ key = DEFAULT_KEY, handleResponse, }) => (transportContext) => {
10
11
  let ws = null;
11
12
  let isConnected = false;
12
13
  const clientId = crypto.randomUUID();
@@ -14,14 +15,15 @@ export const realtimeTransport = ({ key = DEFAULT_KEY }) => (transportContext) =
14
15
  const isHttps = clientUrl.protocol === "https:";
15
16
  clientUrl.protocol = "";
16
17
  clientUrl.host = "";
17
- const setupWebSocket = () => {
18
+ const setupWebSocket = async () => {
18
19
  if (ws)
19
20
  return;
20
21
  const protocol = isHttps ? "wss" : "ws";
21
22
  ws = new WebSocket(`${protocol}://${window.location.host}/__realtime?` +
22
23
  `key=${encodeURIComponent(key)}&` +
23
24
  `url=${encodeURIComponent(clientUrl.toString())}&` +
24
- `clientId=${encodeURIComponent(clientId)}`);
25
+ `clientId=${encodeURIComponent(clientId)}&` +
26
+ `shouldForwardResponses=${encodeURIComponent(handleResponse ? "true" : "false")}`);
25
27
  ws.binaryType = "arraybuffer";
26
28
  ws.addEventListener("open", () => {
27
29
  isConnected = true;
@@ -29,45 +31,15 @@ export const realtimeTransport = ({ key = DEFAULT_KEY }) => (transportContext) =
29
31
  ws.addEventListener("error", (event) => {
30
32
  console.error("[Realtime] WebSocket error", event);
31
33
  });
32
- ws.addEventListener("message", (event) => {
33
- const data = new Uint8Array(event.data);
34
- const messageType = data[0];
35
- if (messageType === MESSAGE_TYPE.RSC_START) {
36
- const decoder = new TextDecoder();
37
- const rscId = decoder.decode(data.slice(1, 37)); // Extract RSC stream ID
38
- const stream = new ReadableStream({
39
- start(controller) {
40
- ws.addEventListener("message", function streamHandler(event) {
41
- const data = new Uint8Array(event.data);
42
- const messageType = data[0];
43
- // Extract the RSC stream ID and verify it matches
44
- const responseId = decoder.decode(data.slice(1, 37));
45
- if (responseId !== rscId) {
46
- return; // Not for this stream
47
- }
48
- const payload = data.slice(37);
49
- if (messageType === MESSAGE_TYPE.RSC_CHUNK) {
50
- controller.enqueue(payload);
51
- }
52
- else if (messageType === MESSAGE_TYPE.RSC_END) {
53
- controller.close();
54
- ws.removeEventListener("message", streamHandler);
55
- }
56
- });
57
- },
58
- });
59
- const rscPayload = createFromReadableStream(stream, {
60
- callServer: realtimeCallServer,
61
- });
62
- transportContext.setRscPayload(rscPayload);
63
- }
64
- });
65
34
  ws.addEventListener("close", () => {
66
35
  console.warn("[Realtime] WebSocket closed, attempting to reconnect...");
67
36
  ws = null;
68
37
  isConnected = false;
69
38
  setTimeout(setupWebSocket, 5000);
70
39
  });
40
+ listenForUpdates(ws, (response) => {
41
+ processResponse(response);
42
+ });
71
43
  };
72
44
  const ensureWs = () => {
73
45
  if (!ws && isConnected) {
@@ -89,78 +61,139 @@ export const realtimeTransport = ({ key = DEFAULT_KEY }) => (transportContext) =
89
61
  clientUrl.host = "";
90
62
  const encodedArgs = args != null ? await encodeReply(args) : null;
91
63
  const requestId = crypto.randomUUID();
92
- const messageData = JSON.stringify({
64
+ const message = packMessage({
65
+ type: MESSAGE_TYPE.ACTION_REQUEST,
93
66
  id,
94
67
  args: encodedArgs,
95
68
  requestId,
96
- clientUrl,
69
+ clientUrl: clientUrl.toString(),
97
70
  });
98
- const encoder = new TextEncoder();
99
- const messageBytes = encoder.encode(messageData);
100
- const message = new Uint8Array(messageBytes.length + 1);
101
- message[0] = MESSAGE_TYPE.ACTION_REQUEST;
102
- message.set(messageBytes, 1);
71
+ const promisedResponse = respondToRequest(requestId, socket);
103
72
  socket.send(message);
104
- return new Promise(async (resolve, reject) => {
105
- const stream = new ReadableStream({
106
- start(controller) {
107
- const messageHandler = (event) => {
108
- const data = new Uint8Array(event.data);
109
- const messageType = data[0];
110
- // First byte is message type
111
- // Next 36 bytes (or fixed size) should be UUID as requestId
112
- // Remaining bytes are the payload
113
- // Extract the requestId from the message
114
- const decoder = new TextDecoder();
115
- const responseId = decoder.decode(data.slice(1, 37)); // Assuming UUID is 36 chars
116
- // Only process messages meant for this request
117
- if (responseId !== requestId) {
118
- return;
119
- }
120
- // The actual payload starts after the requestId
121
- const payload = data.slice(37);
122
- if (messageType === MESSAGE_TYPE.ACTION_CHUNK) {
123
- controller.enqueue(payload);
124
- }
125
- else if (messageType === MESSAGE_TYPE.ACTION_END) {
126
- controller.close();
127
- socket.removeEventListener("message", messageHandler);
128
- }
129
- else if (messageType === MESSAGE_TYPE.ACTION_ERROR) {
130
- const errorJson = decoder.decode(payload);
131
- let errorMsg = "Unknown error";
132
- try {
133
- const errorObj = JSON.parse(errorJson);
134
- errorMsg = errorObj.error || errorMsg;
135
- }
136
- catch (e) {
137
- // Use default error message
138
- }
139
- controller.error(new Error(errorMsg));
140
- socket.removeEventListener("message", messageHandler);
141
- }
142
- };
143
- socket.addEventListener("message", messageHandler);
144
- },
145
- });
146
- const rscPayload = createFromReadableStream(stream, {
147
- callServer: realtimeCallServer,
148
- });
149
- transportContext.setRscPayload(rscPayload);
150
- try {
151
- const result = await rscPayload;
152
- resolve(result.actionResult);
153
- }
154
- catch (rscPayloadError) {
155
- reject(rscPayloadError);
156
- }
157
- });
73
+ return await processResponse(await promisedResponse);
158
74
  }
159
75
  catch (e) {
160
76
  console.error("[Realtime] Error calling server", e);
161
77
  return null;
162
78
  }
163
79
  };
80
+ const processResponse = async (response) => {
81
+ try {
82
+ let streamForRsc;
83
+ let shouldContinue = true;
84
+ if (transportContext.handleResponse) {
85
+ const [stream1, stream2] = response.body.tee();
86
+ const clonedResponse = new Response(stream1, response);
87
+ streamForRsc = stream2;
88
+ shouldContinue = transportContext.handleResponse(clonedResponse);
89
+ }
90
+ else {
91
+ streamForRsc = response.body;
92
+ }
93
+ if (!shouldContinue) {
94
+ return null;
95
+ }
96
+ const rscPayload = createFromReadableStream(streamForRsc, {
97
+ callServer: realtimeCallServer,
98
+ });
99
+ transportContext.setRscPayload(rscPayload);
100
+ return (await rscPayload).actionResult;
101
+ }
102
+ catch (err) {
103
+ throw err;
104
+ }
105
+ };
164
106
  setupWebSocket();
165
107
  return realtimeCallServer;
166
108
  };
109
+ function respondToRequest(requestId, socket) {
110
+ const messageTypes = {
111
+ start: MESSAGE_TYPE.ACTION_START,
112
+ chunk: MESSAGE_TYPE.ACTION_CHUNK,
113
+ end: MESSAGE_TYPE.ACTION_END,
114
+ error: MESSAGE_TYPE.ACTION_ERROR,
115
+ };
116
+ return new Promise((resolve, reject) => {
117
+ const handler = (event) => {
118
+ const unpacked = unpackMessage(new Uint8Array(event.data));
119
+ if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST) {
120
+ return;
121
+ }
122
+ if (unpacked.id !== requestId) {
123
+ return;
124
+ }
125
+ if (unpacked.type === messageTypes.start) {
126
+ const message = unpacked;
127
+ socket.removeEventListener("message", handler);
128
+ const stream = createUpdateStreamFromSocket(requestId, socket, messageTypes, reject);
129
+ const response = new Response(stream, {
130
+ status: message.status,
131
+ headers: { "Content-Type": "text/plain" },
132
+ });
133
+ resolve(response);
134
+ }
135
+ };
136
+ socket.addEventListener("message", handler);
137
+ });
138
+ }
139
+ function listenForUpdates(socket, onUpdate) {
140
+ const messageTypes = {
141
+ start: MESSAGE_TYPE.RSC_START,
142
+ chunk: MESSAGE_TYPE.RSC_CHUNK,
143
+ end: MESSAGE_TYPE.RSC_END,
144
+ };
145
+ const handler = async (event) => {
146
+ const unpacked = unpackMessage(new Uint8Array(event.data));
147
+ if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST ||
148
+ unpacked.type === MESSAGE_TYPE.ACTION_CHUNK ||
149
+ unpacked.type === MESSAGE_TYPE.ACTION_END ||
150
+ unpacked.type === MESSAGE_TYPE.ACTION_ERROR) {
151
+ return;
152
+ }
153
+ if (unpacked.type === messageTypes.start) {
154
+ const message = unpacked;
155
+ const stream = createUpdateStreamFromSocket(message.id, socket, messageTypes, (error) => {
156
+ console.error("[Realtime] Error creating update stream", error);
157
+ });
158
+ const response = new Response(stream, {
159
+ status: message.status,
160
+ headers: { "Content-Type": "text/plain" },
161
+ });
162
+ onUpdate(response);
163
+ }
164
+ };
165
+ socket.addEventListener("message", handler);
166
+ }
167
+ const createUpdateStreamFromSocket = (id, socket, messageTypes, onError) => {
168
+ let deferredStreamController = Promise.withResolvers();
169
+ const stream = new ReadableStream({
170
+ start(controller) {
171
+ deferredStreamController.resolve(controller);
172
+ },
173
+ });
174
+ const handler = async (event) => {
175
+ const unpacked = unpackMessage(new Uint8Array(event.data));
176
+ if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST) {
177
+ return;
178
+ }
179
+ if (unpacked.id !== id) {
180
+ return;
181
+ }
182
+ const streamController = await deferredStreamController.promise;
183
+ if (unpacked.type === messageTypes.chunk) {
184
+ const message = unpacked;
185
+ streamController.enqueue(message.payload);
186
+ }
187
+ else if (unpacked.type === messageTypes.end) {
188
+ streamController.close();
189
+ socket.removeEventListener("message", handler);
190
+ }
191
+ else if (messageTypes.error && unpacked.type === messageTypes.error) {
192
+ const message = unpacked;
193
+ onError(new Error(message.error));
194
+ socket.removeEventListener("message", handler);
195
+ }
196
+ };
197
+ socket.addEventListener("message", handler);
198
+ return stream;
199
+ };
@@ -3,6 +3,7 @@ interface ClientInfo {
3
3
  url: string;
4
4
  clientId: string;
5
5
  cookieHeaders: string;
6
+ shouldForwardResponses: boolean;
6
7
  }
7
8
  export declare class RealtimeDurableObject extends DurableObject {
8
9
  state: DurableObjectState;
@@ -1,6 +1,7 @@
1
1
  import { DurableObject } from "cloudflare:workers";
2
2
  import { MESSAGE_TYPE } from "./shared";
3
3
  import { validateUpgradeRequest } from "./validateUpgradeRequest";
4
+ import { packMessage, unpackMessage, } from "./protocol";
4
5
  export class RealtimeDurableObject extends DurableObject {
5
6
  constructor(state, env) {
6
7
  super(state, env);
@@ -23,6 +24,7 @@ export class RealtimeDurableObject extends DurableObject {
23
24
  url: url.searchParams.get("url"),
24
25
  clientId: url.searchParams.get("clientId"),
25
26
  cookieHeaders: request.headers.get("Cookie") || "",
27
+ shouldForwardResponses: url.searchParams.get("shouldForwardResponses") === "true",
26
28
  };
27
29
  }
28
30
  async storeClientInfo(clientInfo) {
@@ -51,54 +53,62 @@ export class RealtimeDurableObject extends DurableObject {
51
53
  async webSocketMessage(ws, data) {
52
54
  const clientId = ws.deserializeAttachment();
53
55
  let clientInfo = await this.getClientInfo(clientId);
54
- const message = new Uint8Array(data);
55
- const messageType = message[0];
56
- if (messageType === MESSAGE_TYPE.ACTION_REQUEST) {
57
- const decoder = new TextDecoder();
58
- const jsonData = decoder.decode(message.slice(1));
59
- const { id, args, requestId, clientUrl } = JSON.parse(jsonData);
56
+ const unpacked = unpackMessage(new Uint8Array(data));
57
+ if (unpacked.type === MESSAGE_TYPE.ACTION_REQUEST) {
58
+ const message = unpacked;
60
59
  clientInfo = {
61
60
  ...clientInfo,
62
- url: clientUrl,
61
+ url: message.clientUrl,
63
62
  };
64
63
  await this.storeClientInfo(clientInfo);
65
64
  try {
66
- await this.handleAction(ws, id, args, clientInfo, requestId, clientUrl);
65
+ await this.handleAction(ws, message.id, message.args, clientInfo, message.requestId, message.clientUrl);
67
66
  }
68
67
  catch (error) {
69
- const encoder = new TextEncoder();
70
- const requestIdBytes = encoder.encode(requestId);
71
- const errorData = JSON.stringify({
68
+ ws.send(packMessage({
69
+ type: MESSAGE_TYPE.ACTION_ERROR,
70
+ id: message.requestId,
72
71
  error: error instanceof Error ? error.message : String(error),
73
- });
74
- const errorBytes = encoder.encode(errorData);
75
- const errorResponse = new Uint8Array(1 + requestIdBytes.length + errorBytes.length);
76
- errorResponse[0] = MESSAGE_TYPE.ACTION_ERROR;
77
- errorResponse.set(requestIdBytes, 1);
78
- errorResponse.set(errorBytes, 1 + requestIdBytes.length);
79
- ws.send(errorResponse);
72
+ }));
80
73
  }
81
74
  }
82
75
  }
83
76
  async streamResponse(response, ws, messageTypes, streamId) {
77
+ const startMessage = messageTypes.start === MESSAGE_TYPE.ACTION_START
78
+ ? {
79
+ type: MESSAGE_TYPE.ACTION_START,
80
+ id: streamId,
81
+ status: response.status,
82
+ }
83
+ : {
84
+ type: MESSAGE_TYPE.RSC_START,
85
+ id: streamId,
86
+ status: response.status,
87
+ };
88
+ ws.send(packMessage(startMessage));
84
89
  const reader = response.body.getReader();
85
- const encoder = new TextEncoder();
86
- const streamIdBytes = encoder.encode(streamId);
87
90
  try {
88
91
  while (true) {
89
92
  const { done, value } = await reader.read();
90
93
  if (done) {
91
- const endMessage = new Uint8Array(1 + streamIdBytes.length);
92
- endMessage[0] = messageTypes.end;
93
- endMessage.set(streamIdBytes, 1);
94
- ws.send(endMessage);
94
+ const endMessage = messageTypes.end === MESSAGE_TYPE.ACTION_END
95
+ ? { type: MESSAGE_TYPE.ACTION_END, id: streamId }
96
+ : { type: MESSAGE_TYPE.RSC_END, id: streamId };
97
+ ws.send(packMessage(endMessage));
95
98
  break;
96
99
  }
97
- const chunkMessage = new Uint8Array(1 + streamIdBytes.length + value.length);
98
- chunkMessage[0] = messageTypes.chunk;
99
- chunkMessage.set(streamIdBytes, 1);
100
- chunkMessage.set(value, 1 + streamIdBytes.length);
101
- ws.send(chunkMessage);
100
+ const chunkMessage = messageTypes.chunk === MESSAGE_TYPE.ACTION_CHUNK
101
+ ? {
102
+ type: MESSAGE_TYPE.ACTION_CHUNK,
103
+ id: streamId,
104
+ payload: value,
105
+ }
106
+ : {
107
+ type: MESSAGE_TYPE.RSC_CHUNK,
108
+ id: streamId,
109
+ payload: value,
110
+ };
111
+ ws.send(packMessage(chunkMessage));
102
112
  }
103
113
  }
104
114
  finally {
@@ -119,13 +129,14 @@ export class RealtimeDurableObject extends DurableObject {
119
129
  Cookie: clientInfo.cookieHeaders,
120
130
  },
121
131
  });
122
- if (!response.ok) {
132
+ if (!response.ok && !clientInfo.shouldForwardResponses) {
123
133
  throw new Error(`Action failed: ${response.statusText}`);
124
134
  }
125
135
  this.render({
126
136
  exclude: [clientInfo.clientId],
127
137
  });
128
138
  await this.streamResponse(response, ws, {
139
+ start: MESSAGE_TYPE.ACTION_START,
129
140
  chunk: MESSAGE_TYPE.ACTION_CHUNK,
130
141
  end: MESSAGE_TYPE.ACTION_END,
131
142
  }, requestId);
@@ -167,11 +178,8 @@ export class RealtimeDurableObject extends DurableObject {
167
178
  return;
168
179
  }
169
180
  const rscId = crypto.randomUUID();
170
- const startMessage = new Uint8Array(1 + 36);
171
- startMessage[0] = MESSAGE_TYPE.RSC_START;
172
- startMessage.set(new TextEncoder().encode(rscId), 1);
173
- socket.send(startMessage);
174
181
  await this.streamResponse(response, socket, {
182
+ start: MESSAGE_TYPE.RSC_START,
175
183
  chunk: MESSAGE_TYPE.RSC_CHUNK,
176
184
  end: MESSAGE_TYPE.RSC_END,
177
185
  }, rscId);
@@ -0,0 +1,50 @@
1
+ import { MESSAGE_TYPE } from "./shared";
2
+ export type Message = ActionRequestMessage | ActionStartMessage | ActionChunkMessage | ActionEndMessage | ActionErrorMessage | RscStartMessage | RscChunkMessage | RscEndMessage;
3
+ export type ActionRequestMessage = {
4
+ type: typeof MESSAGE_TYPE.ACTION_REQUEST;
5
+ id: string | null;
6
+ args: any;
7
+ requestId: string;
8
+ clientUrl: string;
9
+ };
10
+ export type ActionStartMessage = {
11
+ type: typeof MESSAGE_TYPE.ACTION_START;
12
+ id: string;
13
+ status: number;
14
+ };
15
+ export type ActionChunkMessage = {
16
+ type: typeof MESSAGE_TYPE.ACTION_CHUNK;
17
+ id: string;
18
+ payload: Uint8Array;
19
+ };
20
+ export type ActionEndMessage = {
21
+ type: typeof MESSAGE_TYPE.ACTION_END;
22
+ id: string;
23
+ };
24
+ export type ActionErrorMessage = {
25
+ type: typeof MESSAGE_TYPE.ACTION_ERROR;
26
+ id: string;
27
+ error: string;
28
+ };
29
+ export type RscStartMessage = {
30
+ type: typeof MESSAGE_TYPE.RSC_START;
31
+ id: string;
32
+ status: number;
33
+ };
34
+ export type RscChunkMessage = {
35
+ type: typeof MESSAGE_TYPE.RSC_CHUNK;
36
+ id: string;
37
+ payload: Uint8Array;
38
+ };
39
+ export type RscEndMessage = {
40
+ type: typeof MESSAGE_TYPE.RSC_END;
41
+ id: string;
42
+ };
43
+ /**
44
+ * Packs a message object into a Uint8Array for sending over WebSocket.
45
+ */
46
+ export declare function packMessage(message: Message): Uint8Array;
47
+ /**
48
+ * Unpacks a Uint8Array from WebSocket into a message object.
49
+ */
50
+ export declare function unpackMessage(data: Uint8Array): Message;
@@ -0,0 +1,141 @@
1
+ import { MESSAGE_TYPE } from "./shared";
2
+ const TEXT_ENCODER = new TextEncoder();
3
+ const TEXT_DECODER = new TextDecoder();
4
+ const ID_LENGTH = 36; // Length of a UUID string
5
+ /**
6
+ * Packs a message object into a Uint8Array for sending over WebSocket.
7
+ */
8
+ export function packMessage(message) {
9
+ switch (message.type) {
10
+ case MESSAGE_TYPE.ACTION_REQUEST: {
11
+ const msg = message;
12
+ const jsonPayload = JSON.stringify({
13
+ id: msg.id,
14
+ args: msg.args,
15
+ requestId: msg.requestId,
16
+ clientUrl: msg.clientUrl,
17
+ });
18
+ const payloadBytes = TEXT_ENCODER.encode(jsonPayload);
19
+ const packed = new Uint8Array(1 + payloadBytes.length);
20
+ packed[0] = msg.type;
21
+ packed.set(payloadBytes, 1);
22
+ return packed;
23
+ }
24
+ case MESSAGE_TYPE.ACTION_START:
25
+ case MESSAGE_TYPE.RSC_START: {
26
+ const msg = message;
27
+ const idBytes = TEXT_ENCODER.encode(msg.id);
28
+ if (idBytes.length !== ID_LENGTH) {
29
+ throw new Error("Invalid message ID length for START message");
30
+ }
31
+ const packed = new Uint8Array(1 + 2 + ID_LENGTH); // 1 for type, 2 for status
32
+ const view = new DataView(packed.buffer);
33
+ view.setUint8(0, msg.type);
34
+ view.setUint16(1, msg.status, false); // Big-endian
35
+ packed.set(idBytes, 3);
36
+ return packed;
37
+ }
38
+ case MESSAGE_TYPE.ACTION_CHUNK:
39
+ case MESSAGE_TYPE.RSC_CHUNK: {
40
+ const msg = message;
41
+ const idBytes = TEXT_ENCODER.encode(msg.id);
42
+ if (idBytes.length !== ID_LENGTH) {
43
+ throw new Error("Invalid message ID length for CHUNK message");
44
+ }
45
+ const packed = new Uint8Array(1 + ID_LENGTH + msg.payload.length);
46
+ packed[0] = msg.type;
47
+ packed.set(idBytes, 1);
48
+ packed.set(msg.payload, 1 + ID_LENGTH);
49
+ return packed;
50
+ }
51
+ case MESSAGE_TYPE.ACTION_END:
52
+ case MESSAGE_TYPE.RSC_END: {
53
+ const msg = message;
54
+ const idBytes = TEXT_ENCODER.encode(msg.id);
55
+ if (idBytes.length !== ID_LENGTH) {
56
+ throw new Error("Invalid message ID length for END message");
57
+ }
58
+ const packed = new Uint8Array(1 + ID_LENGTH);
59
+ packed[0] = msg.type;
60
+ packed.set(idBytes, 1);
61
+ return packed;
62
+ }
63
+ case MESSAGE_TYPE.ACTION_ERROR: {
64
+ const msg = message;
65
+ const idBytes = TEXT_ENCODER.encode(msg.id);
66
+ if (idBytes.length !== ID_LENGTH) {
67
+ throw new Error("Invalid message ID length for ERROR message");
68
+ }
69
+ const errorPayload = JSON.stringify({ error: msg.error });
70
+ const errorBytes = TEXT_ENCODER.encode(errorPayload);
71
+ const packed = new Uint8Array(1 + ID_LENGTH + errorBytes.length);
72
+ packed[0] = msg.type;
73
+ packed.set(idBytes, 1);
74
+ packed.set(errorBytes, 1 + ID_LENGTH);
75
+ return packed;
76
+ }
77
+ default:
78
+ // This should be unreachable if all message types are handled
79
+ throw new Error(`Unknown message type for packing`);
80
+ }
81
+ }
82
+ /**
83
+ * Unpacks a Uint8Array from WebSocket into a message object.
84
+ */
85
+ export function unpackMessage(data) {
86
+ if (data.length === 0) {
87
+ throw new Error("Cannot unpack empty message");
88
+ }
89
+ const messageType = data[0];
90
+ switch (messageType) {
91
+ case MESSAGE_TYPE.ACTION_REQUEST: {
92
+ const jsonPayload = TEXT_DECODER.decode(data.slice(1));
93
+ const parsed = JSON.parse(jsonPayload);
94
+ return { type: messageType, ...parsed };
95
+ }
96
+ case MESSAGE_TYPE.ACTION_START:
97
+ case MESSAGE_TYPE.RSC_START: {
98
+ if (data.length !== 1 + 2 + ID_LENGTH) {
99
+ throw new Error("Invalid START message length");
100
+ }
101
+ const view = new DataView(data.buffer);
102
+ const status = view.getUint16(1, false);
103
+ const id = TEXT_DECODER.decode(data.slice(3));
104
+ return { type: messageType, id, status };
105
+ }
106
+ case MESSAGE_TYPE.ACTION_CHUNK:
107
+ case MESSAGE_TYPE.RSC_CHUNK: {
108
+ if (data.length < 1 + ID_LENGTH) {
109
+ throw new Error("Invalid CHUNK message length");
110
+ }
111
+ const id = TEXT_DECODER.decode(data.slice(1, 1 + ID_LENGTH));
112
+ const payload = data.slice(1 + ID_LENGTH);
113
+ return { type: messageType, id, payload };
114
+ }
115
+ case MESSAGE_TYPE.ACTION_END:
116
+ case MESSAGE_TYPE.RSC_END: {
117
+ if (data.length !== 1 + ID_LENGTH) {
118
+ throw new Error("Invalid END message length");
119
+ }
120
+ const id = TEXT_DECODER.decode(data.slice(1, 1 + ID_LENGTH));
121
+ return { type: messageType, id };
122
+ }
123
+ case MESSAGE_TYPE.ACTION_ERROR: {
124
+ if (data.length < 1 + ID_LENGTH) {
125
+ throw new Error("Invalid ERROR message length");
126
+ }
127
+ const id = TEXT_DECODER.decode(data.slice(1, 1 + ID_LENGTH));
128
+ const errorPayload = TEXT_DECODER.decode(data.slice(1 + ID_LENGTH));
129
+ let error = "Unknown error";
130
+ try {
131
+ error = JSON.parse(errorPayload).error;
132
+ }
133
+ catch (e) {
134
+ // ignore if it's not a json
135
+ }
136
+ return { type: messageType, id, error };
137
+ }
138
+ default:
139
+ throw new Error(`Unknown message type for unpacking: ${messageType}`);
140
+ }
141
+ }
@@ -1,10 +1,10 @@
1
1
  export declare const MESSAGE_TYPE: {
2
- readonly RSC_START: 0;
3
- readonly RSC_CHUNK: 1;
4
- readonly RSC_END: 2;
5
- readonly ACTION_REQUEST: 3;
6
- readonly ACTION_RESPONSE: 4;
7
- readonly ACTION_ERROR: 5;
8
- readonly ACTION_CHUNK: 6;
9
- readonly ACTION_END: 7;
2
+ RSC_START: number;
3
+ RSC_CHUNK: number;
4
+ RSC_END: number;
5
+ ACTION_REQUEST: number;
6
+ ACTION_START: number;
7
+ ACTION_CHUNK: number;
8
+ ACTION_END: number;
9
+ ACTION_ERROR: number;
10
10
  };
@@ -3,8 +3,8 @@ export const MESSAGE_TYPE = {
3
3
  RSC_CHUNK: 1,
4
4
  RSC_END: 2,
5
5
  ACTION_REQUEST: 3,
6
- ACTION_RESPONSE: 4,
7
- ACTION_ERROR: 5,
8
- ACTION_CHUNK: 6,
9
- ACTION_END: 7,
6
+ ACTION_START: 4,
7
+ ACTION_CHUNK: 5,
8
+ ACTION_END: 6,
9
+ ACTION_ERROR: 7,
10
10
  };
@@ -15,7 +15,8 @@ const getPackageManagerInfo = (targetDir) => {
15
15
  if (existsSync(path.join(targetDir, "yarn.lock"))) {
16
16
  return { name: "yarn", lockFile: "yarn.lock", command: "add" };
17
17
  }
18
- if (existsSync(path.join(targetDir, "pnpm-lock.yaml"))) {
18
+ if (existsSync(path.join(targetDir, "pnpm-lock.yaml")) ||
19
+ existsSync(path.join(targetDir, "node_modules", ".pnpm"))) {
19
20
  return pnpmResult;
20
21
  }
21
22
  if (existsSync(path.join(targetDir, "package-lock.json"))) {
@@ -23,51 +24,75 @@ const getPackageManagerInfo = (targetDir) => {
23
24
  }
24
25
  return pnpmResult;
25
26
  };
26
- const performFullSync = async (sdkDir, targetDir) => {
27
- console.log("📦 Packing SDK...");
28
- const packResult = await $ `npm pack`;
29
- const tarballName = packResult.stdout?.trim() ?? "";
30
- if (!tarballName) {
31
- console.error("❌ Failed to get tarball name from npm pack.");
32
- return;
33
- }
34
- const tarballPath = path.resolve(sdkDir, tarballName);
35
- console.log(`💿 Installing ${tarballName} in ${targetDir}...`);
36
- const pm = getPackageManagerInfo(targetDir);
37
- const packageJsonPath = path.join(targetDir, "package.json");
38
- const lockfilePath = path.join(targetDir, pm.lockFile);
39
- const originalPackageJson = await fs
40
- .readFile(packageJsonPath, "utf-8")
41
- .catch(() => null);
42
- const originalLockfile = await fs
43
- .readFile(lockfilePath, "utf-8")
44
- .catch(() => null);
27
+ const performFullSync = async (sdkDir, targetDir, cacheBust = false) => {
28
+ const sdkPackageJsonPath = path.join(sdkDir, "package.json");
29
+ let originalSdkPackageJson = null;
30
+ let tarballPath = "";
31
+ let tarballName = "";
45
32
  try {
46
- const cmd = pm.name;
47
- const args = [pm.command];
48
- if (pm.name === "yarn") {
49
- args.push(`file:${tarballPath}`);
33
+ if (cacheBust) {
34
+ console.log("💥 Cache-busting version for full sync...");
35
+ originalSdkPackageJson = await fs.readFile(sdkPackageJsonPath, "utf-8");
36
+ const packageJson = JSON.parse(originalSdkPackageJson);
37
+ const now = Date.now();
38
+ // This is a temporary version used for cache busting
39
+ packageJson.version = `${packageJson.version}-dev.${now}`;
40
+ await fs.writeFile(sdkPackageJsonPath, JSON.stringify(packageJson, null, 2));
41
+ }
42
+ console.log("📦 Packing SDK...");
43
+ const packResult = await $({ cwd: sdkDir }) `npm pack`;
44
+ tarballName = packResult.stdout?.trim() ?? "";
45
+ if (!tarballName) {
46
+ console.error("❌ Failed to get tarball name from npm pack.");
47
+ return;
50
48
  }
51
- else {
52
- args.push(tarballPath);
49
+ tarballPath = path.resolve(sdkDir, tarballName);
50
+ console.log(`💿 Installing ${tarballName} in ${targetDir}...`);
51
+ const pm = getPackageManagerInfo(targetDir);
52
+ const packageJsonPath = path.join(targetDir, "package.json");
53
+ const lockfilePath = path.join(targetDir, pm.lockFile);
54
+ const originalPackageJson = await fs
55
+ .readFile(packageJsonPath, "utf-8")
56
+ .catch(() => null);
57
+ const originalLockfile = await fs
58
+ .readFile(lockfilePath, "utf-8")
59
+ .catch(() => null);
60
+ try {
61
+ const cmd = pm.name;
62
+ const args = [pm.command];
63
+ if (pm.name === "yarn") {
64
+ args.push(`file:${tarballPath}`);
65
+ }
66
+ else {
67
+ args.push(tarballPath);
68
+ }
69
+ await $(cmd, args, {
70
+ cwd: targetDir,
71
+ stdio: "inherit",
72
+ });
73
+ }
74
+ finally {
75
+ if (originalPackageJson) {
76
+ console.log("Restoring package.json...");
77
+ await fs.writeFile(packageJsonPath, originalPackageJson);
78
+ }
79
+ if (originalLockfile) {
80
+ console.log(`Restoring ${pm.lockFile}...`);
81
+ await fs.writeFile(lockfilePath, originalLockfile);
82
+ }
53
83
  }
54
- await $(cmd, args, {
55
- cwd: targetDir,
56
- stdio: "inherit",
57
- });
58
84
  }
59
85
  finally {
60
- if (originalPackageJson) {
86
+ if (originalSdkPackageJson) {
61
87
  console.log("Restoring package.json...");
62
- await fs.writeFile(packageJsonPath, originalPackageJson);
88
+ await fs.writeFile(sdkPackageJsonPath, originalSdkPackageJson);
63
89
  }
64
- if (originalLockfile) {
65
- console.log(`Restoring ${pm.lockFile}...`);
66
- await fs.writeFile(lockfilePath, originalLockfile);
90
+ if (tarballPath) {
91
+ console.log("Removing tarball...");
92
+ await fs.unlink(tarballPath).catch(() => {
93
+ // ignore if deletion fails
94
+ });
67
95
  }
68
- await fs.unlink(tarballPath).catch(() => {
69
- // ignore if deletion fails
70
- });
71
96
  }
72
97
  };
73
98
  const performFastSync = async (sdkDir, targetDir) => {
@@ -87,6 +112,13 @@ const performFastSync = async (sdkDir, targetDir) => {
87
112
  const performSync = async (sdkDir, targetDir) => {
88
113
  console.log("🏗️ Rebuilding SDK...");
89
114
  await $ `pnpm build`;
115
+ const forceFullSync = Boolean(process.env.RWSDK_FORCE_FULL_SYNC);
116
+ if (forceFullSync) {
117
+ console.log("🏃 Force full sync mode is enabled.");
118
+ await performFullSync(sdkDir, targetDir, true);
119
+ console.log("✅ Done syncing");
120
+ return;
121
+ }
90
122
  const sdkPackageJsonPath = path.join(sdkDir, "package.json");
91
123
  const installedSdkPackageJsonPath = path.join(targetDir, "node_modules/rwsdk/package.json");
92
124
  let packageJsonChanged = true;
@@ -166,8 +198,21 @@ export const debugSync = async (opts) => {
166
198
  ignoreInitial: true,
167
199
  cwd: sdkDir,
168
200
  });
169
- watcher.on("all", async () => {
170
- console.log("\nDetected change, re-syncing...");
201
+ let syncing = false;
202
+ // todo(justinvdm, 2025-07-22): Figure out wtf makes the full sync
203
+ // cause chokidar to find out about package.json after sync has resolved
204
+ let expectingFileChanges = Boolean(process.env.RWSDK_FORCE_FULL_SYNC);
205
+ watcher.on("all", async (_event, filePath) => {
206
+ if (syncing || filePath.endsWith(".tgz")) {
207
+ return;
208
+ }
209
+ if (expectingFileChanges && process.env.RWSDK_FORCE_FULL_SYNC) {
210
+ expectingFileChanges = false;
211
+ return;
212
+ }
213
+ syncing = true;
214
+ expectingFileChanges = true;
215
+ console.log(`\nDetected change, re-syncing... (file: ${filePath})`);
171
216
  if (childProc && !childProc.killed) {
172
217
  console.log("Stopping running process...");
173
218
  childProc.kill();
@@ -176,6 +221,7 @@ export const debugSync = async (opts) => {
176
221
  });
177
222
  }
178
223
  try {
224
+ watcher.unwatch(filesToWatch);
179
225
  await performSync(sdkDir, targetDir);
180
226
  runWatchedCommand();
181
227
  }
@@ -183,6 +229,10 @@ export const debugSync = async (opts) => {
183
229
  console.error("❌ Sync failed:", error);
184
230
  console.log(" Still watching for changes...");
185
231
  }
232
+ finally {
233
+ syncing = false;
234
+ watcher.add(filesToWatch);
235
+ }
186
236
  });
187
237
  const cleanup = async () => {
188
238
  console.log("\nCleaning up...");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {