@smoothglue/sync-whiteboard 1.1.0 → 1.1.2

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 (2) hide show
  1. package/dist/assets.js +33 -35
  2. package/package.json +11 -8
package/dist/assets.js CHANGED
@@ -15,8 +15,26 @@ if (!ASSET_STORAGE_URL) {
15
15
  process.exit(1);
16
16
  }
17
17
  logger_1.default.info({ assetStorageUrl: ASSET_STORAGE_URL }, `[ASSETS] Using Asset Storage URL`);
18
+ /**
19
+ * Buffers a Readable stream into an ArrayBuffer.
20
+ * @param stream The Node.js Readable stream.
21
+ * @returns A promise that resolves with an ArrayBuffer.
22
+ */
23
+ async function streamToArrayBuffer(stream) {
24
+ return new Promise((resolve, reject) => {
25
+ const chunks = [];
26
+ stream.on("data", (chunk) => chunks.push(chunk));
27
+ stream.on("end", () => {
28
+ const buffer = Buffer.concat(chunks);
29
+ // Convert the final Node.js Buffer to a standard ArrayBuffer
30
+ resolve(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength));
31
+ });
32
+ stream.on("error", reject);
33
+ });
34
+ }
18
35
  /**
19
36
  * Stores an asset by proxying a PUT request to the configured asset storage backend.
37
+ * Wraps the file in a multipart/form-data request.
20
38
  * @param id - The unique identifier for the asset (generated by the client).
21
39
  * @param fileStream - The readable stream containing the asset data (from the client request).
22
40
  * @param contentType - The MIME type of the asset.
@@ -32,39 +50,35 @@ async function storeAsset(id, fileStream, contentType = "application/octet-strea
32
50
  logger_1.default.error({ assetId: id, receivedType: typeof fileStream }, "[ASSETS] Error: storeAsset received a non-readable stream type.");
33
51
  throw new Error("Invalid stream type provided to storeAsset.");
34
52
  }
35
- let webStream = null;
36
53
  try {
37
- // Convert Node.js stream to Web Standard stream for fetch body
38
- webStream = stream_1.Readable.toWeb(fileStream);
39
- // Sanitize the filename for use in the Content-Disposition header
40
- // This removes non-ASCII characters that can crash the fetch call.
41
- const sanitizedFilename = originalFilename.replace(/[^\x00-\x7F]/g, "");
54
+ // Buffer the stream into memory to create a Blob.
55
+ const arrayBuffer = await streamToArrayBuffer(fileStream);
56
+ const blob = new Blob([arrayBuffer], { type: contentType });
57
+ // Create a FormData payload. The backend expects the file under the 'file' field name.
58
+ const formData = new FormData();
59
+ formData.append("file", blob, originalFilename);
60
+ // Prepare headers. Do NOT manually set the 'Content-Type' header.
61
+ // The 'fetch' API will automatically set the correct 'multipart/form-data' boundary.
42
62
  const headers = {
43
- "Content-Type": contentType,
44
- "X-Original-Filename": encodeURIComponent(originalFilename), // Pass the original filename (properly encoded) for the backend to use
45
- "Content-Disposition": `attachment; filename="${sanitizedFilename}"`, // Use the sanitized filename in the Content-Disposition header to avoid errors
63
+ "X-Original-Filename": encodeURIComponent(originalFilename),
64
+ "Content-Disposition": `attachment; filename="${originalFilename.replace(/[^\x00-\x7F]/g, "")}"`,
46
65
  };
47
- // forward auth headers if included from client
48
- // Prioritize Authorization header, but fall back to Cookie
49
66
  if (credentials?.authorization) {
50
67
  headers["Authorization"] = credentials.authorization;
51
68
  }
52
69
  if (credentials?.cookie) {
53
70
  headers["Cookie"] = credentials.cookie;
54
71
  }
55
- // Make the PUT request to the asset storage backend
72
+ // Make the request using the FormData as the body.
56
73
  const response = await fetch(url, {
57
74
  method: "PUT",
58
75
  headers: headers,
59
- body: webStream, // Cast is necessary due to type mismatches
60
- // @ts-ignore - duplex: 'half' is required for streaming request bodies with Node fetch
61
- duplex: "half",
76
+ body: formData, // Use the FormData object as the body
62
77
  });
63
78
  logger_1.default.debug({ assetId: id, status: response.status }, `[ASSETS] Backend PUT response status`);
64
- // Handle backend errors
65
79
  if (!response.ok) {
66
80
  const errorBody = await response.text();
67
- const err = new Error(`Backend failed to store asset ${id}. Status: ${response.status}. Body: ${errorBody}. Headers ${headers}`);
81
+ const err = new Error(`Backend failed to store asset ${id}. Status: ${response.status}. Body: ${errorBody}. Headers ${JSON.stringify(headers)}`);
68
82
  logger_1.default.error({
69
83
  err,
70
84
  assetId: id,
@@ -72,33 +86,17 @@ async function storeAsset(id, fileStream, contentType = "application/octet-strea
72
86
  responseStatusText: response.statusText,
73
87
  responseBody: errorBody,
74
88
  }, `[ASSETS] Error response from backend storing asset`);
75
- // Ensure streams are closed on error
76
- if (webStream) {
77
- await webStream.cancel().catch((cancelErr) => logger_1.default.error({
78
- err: cancelErr,
79
- assetId: id,
80
- stage: "cancel_upload_after_failed_fetch",
81
- }, `[ASSETS] Error cancelling upload webStream`));
82
- }
83
89
  throw err;
84
90
  }
85
91
  logger_1.default.debug({ assetId: id, targetUrl: url }, `[ASSETS] Successfully proxied storage for asset`);
86
- return id; // Return the ID, confirming success
92
+ return id;
87
93
  }
88
94
  catch (error) {
89
95
  logger_1.default.error({ err: error, assetId: id, targetUrl: url, operation: "storeAsset" }, `[ASSETS] Network or fetch error storing asset`);
90
- // Clean up streams on error
91
96
  if (fileStream instanceof stream_1.Readable && !fileStream.destroyed) {
92
97
  fileStream.destroy(error instanceof Error ? error : new Error(String(error)));
93
98
  }
94
- if (webStream) {
95
- await webStream.cancel().catch((cancelErr) => logger_1.default.error({
96
- err: cancelErr,
97
- assetId: id,
98
- stage: "cancel_upload_during_error_handling",
99
- }, `[ASSETS] Error cancelling upload webStream`));
100
- }
101
- throw error; // Re-throw error for the server handler
99
+ throw error;
102
100
  }
103
101
  }
104
102
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothglue/sync-whiteboard",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "main": "dist/server.js",
5
5
  "scripts": {
6
6
  "dev": "ts-node-dev --respawn --transpile-only src/server.ts",
@@ -25,19 +25,22 @@
25
25
  "url": "https://code.build.smoothglue.io/braingu/smoothglue/frontend/sync-whiteboard.git"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^22.18.0",
29
- "@types/ws": "^8.18.1",
30
- "ts-node": "^10.9.2",
31
- "ts-node-dev": "^2.0.0",
32
- "typescript": "^5.9.2"
28
+ "@types/node": "22.18.0",
29
+ "@types/ws": "8.18.1",
30
+ "ts-node": "10.9.2",
31
+ "ts-node-dev": "2.0.0",
32
+ "typescript": "5.9.2"
33
33
  },
34
34
  "dependencies": {
35
35
  "@fastify/cors": "^11.0.1",
36
36
  "@fastify/websocket": "^11.0.2",
37
37
  "@tldraw/sync-core": "^3.12.0",
38
38
  "@tldraw/tlschema": "^3.12.0",
39
- "fastify": "^5.3.0",
40
- "pino": "^9.7.0",
39
+ "fastify": "^5.7.4",
40
+ "pino": "^10.3.0",
41
41
  "ws": "^8.18.1"
42
+ },
43
+ "overrides": {
44
+ "diff": "^4.0.4"
42
45
  }
43
46
  }