@syncular/server-cloudflare 0.0.1 → 0.0.2-127
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/README.md +24 -0
- package/dist/durable-object.d.ts +1 -0
- package/dist/durable-object.d.ts.map +1 -1
- package/dist/durable-object.js +28 -3
- package/dist/durable-object.js.map +1 -1
- package/dist/index.js +3 -3
- package/dist/r2.d.ts +8 -0
- package/dist/r2.d.ts.map +1 -1
- package/dist/r2.js +134 -8
- package/dist/r2.js.map +1 -1
- package/package.json +27 -4
- package/src/durable-object.test.ts +65 -0
- package/src/durable-object.ts +33 -3
- package/src/r2.test.ts +120 -0
- package/src/r2.ts +174 -10
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# @syncular/server-cloudflare
|
|
2
|
+
|
|
3
|
+
Cloudflare Workers and Durable Objects adapters for running Syncular on the edge, plus Cloudflare R2 blob storage helpers.
|
|
4
|
+
|
|
5
|
+
This package is intentionally dialect-agnostic. You pair it with a database dialect (e.g. D1/SQLite or Neon/Postgres) and the matching Syncular server dialect.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @syncular/server-cloudflare
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
- Cloudflare adapter: https://syncular.dev/docs/server/cloudflare-adapter
|
|
16
|
+
- Dialects: https://syncular.dev/docs/server/dialects
|
|
17
|
+
- Blob storage: https://syncular.dev/docs/build/blob-storage
|
|
18
|
+
|
|
19
|
+
## Links
|
|
20
|
+
|
|
21
|
+
- GitHub: https://github.com/syncular/syncular
|
|
22
|
+
- Issues: https://github.com/syncular/syncular/issues
|
|
23
|
+
|
|
24
|
+
> Status: Alpha. APIs and storage layouts may change between releases.
|
package/dist/durable-object.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ export declare abstract class SyncDurableObject<E extends object = Record<string
|
|
|
58
58
|
Bindings: E;
|
|
59
59
|
}>, env: E, upgradeWebSocket: UpgradeWebSocket<WebSocket>): void | Promise<void>;
|
|
60
60
|
private getApp;
|
|
61
|
+
private closeUntrackedSockets;
|
|
61
62
|
/** Handle incoming HTTP requests (and WebSocket upgrades). */
|
|
62
63
|
fetch(request: Request): Promise<Response>;
|
|
63
64
|
/** Dispatch incoming WebSocket messages to Hono event handlers. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAY,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAY,MAAM,SAAS,CAAC;AA4E1D;;;;GAIG;AACH,8BAAsB,iBAAiB,CACrC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE1C,SAAS,CAAC,GAAG,EAAE,kBAAkB,CAAC;IAClC,SAAS,CAAC,GAAG,EAAE,CAAC,CAAC;IAEjB,OAAO,CAAC,GAAG,CAAsC;IACjD,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,kBAAkB,CAA8B;IAExD,YAAY,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,CAAC,EAK1C;IAED;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CACZ,GAAG,EAAE,IAAI,CAAC;QAAE,QAAQ,EAAE,CAAC,CAAA;KAAE,CAAC,EAC1B,GAAG,EAAE,CAAC,EACN,gBAAgB,EAAE,gBAAgB,CAAC,SAAS,CAAC,GAC5C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;YAEV,MAAM;IAcpB,OAAO,CAAC,qBAAqB;IAQ7B,8DAA8D;IACxD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAG/C;IAED,mEAAmE;IAC7D,gBAAgB,CACpB,EAAE,EAAE,SAAS,EACb,OAAO,EAAE,MAAM,GAAG,WAAW,GAC5B,OAAO,CAAC,IAAI,CAAC,CAUf;IAED,8DAA8D;IACxD,cAAc,CAClB,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,IAAI,CAAC,CAWf;IAED,8DAA8D;IACxD,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlE;CACF;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACrD,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,EAC7B,OAAO,CAAC,EAAE;IACR;;;OAGG;IACH,SAAS,CAAC,EAAE,CACV,EAAE,EAAE,sBAAsB,EAC1B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,CAAC,KACH,eAAe,CAAC;CACtB,GACA,eAAe,CAAC,CAAC,CAAC,CAiBpB"}
|
package/dist/durable-object.js
CHANGED
|
@@ -36,11 +36,21 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { Hono } from 'hono';
|
|
38
38
|
import { defineWebSocketHelper, WSContext } from 'hono/ws';
|
|
39
|
+
const STALE_SOCKET_CLOSE_CODE = 1012;
|
|
40
|
+
const STALE_SOCKET_CLOSE_REASON = 'WebSocket session expired; reconnect required';
|
|
39
41
|
/**
|
|
40
42
|
* WeakMap from server-side WebSocket → tag with event handlers.
|
|
41
43
|
* Populated on upgrade, read in webSocketMessage/webSocketClose.
|
|
42
44
|
*/
|
|
43
45
|
const socketTags = new WeakMap();
|
|
46
|
+
function closeStaleSocket(ws) {
|
|
47
|
+
try {
|
|
48
|
+
ws.close(STALE_SOCKET_CLOSE_CODE, STALE_SOCKET_CLOSE_REASON);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
44
54
|
function createWSContext(ws) {
|
|
45
55
|
return new WSContext({
|
|
46
56
|
send(data) {
|
|
@@ -94,6 +104,7 @@ export class SyncDurableObject {
|
|
|
94
104
|
this.ctx = ctx;
|
|
95
105
|
this.env = env;
|
|
96
106
|
this.doUpgradeWebSocket = createDOUpgradeWebSocket(ctx);
|
|
107
|
+
this.closeUntrackedSockets();
|
|
97
108
|
}
|
|
98
109
|
async getApp() {
|
|
99
110
|
if (this.app)
|
|
@@ -107,6 +118,14 @@ export class SyncDurableObject {
|
|
|
107
118
|
await this.initPromise;
|
|
108
119
|
return this.app;
|
|
109
120
|
}
|
|
121
|
+
closeUntrackedSockets() {
|
|
122
|
+
const sockets = this.ctx.getWebSockets();
|
|
123
|
+
for (const ws of sockets) {
|
|
124
|
+
if (socketTags.has(ws))
|
|
125
|
+
continue;
|
|
126
|
+
closeStaleSocket(ws);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
110
129
|
/** Handle incoming HTTP requests (and WebSocket upgrades). */
|
|
111
130
|
async fetch(request) {
|
|
112
131
|
const app = await this.getApp();
|
|
@@ -115,8 +134,10 @@ export class SyncDurableObject {
|
|
|
115
134
|
/** Dispatch incoming WebSocket messages to Hono event handlers. */
|
|
116
135
|
async webSocketMessage(ws, message) {
|
|
117
136
|
const tag = socketTags.get(ws);
|
|
118
|
-
if (!tag?.events.onMessage)
|
|
137
|
+
if (!tag?.events.onMessage) {
|
|
138
|
+
closeStaleSocket(ws);
|
|
119
139
|
return;
|
|
140
|
+
}
|
|
120
141
|
const wsCtx = createWSContext(ws);
|
|
121
142
|
const evt = new MessageEvent('message', { data: message });
|
|
122
143
|
tag.events.onMessage(evt, wsCtx);
|
|
@@ -124,8 +145,10 @@ export class SyncDurableObject {
|
|
|
124
145
|
/** Dispatch WebSocket close events to Hono event handlers. */
|
|
125
146
|
async webSocketClose(ws, code, reason, _wasClean) {
|
|
126
147
|
const tag = socketTags.get(ws);
|
|
127
|
-
if (!tag?.events.onClose)
|
|
148
|
+
if (!tag?.events.onClose) {
|
|
149
|
+
socketTags.delete(ws);
|
|
128
150
|
return;
|
|
151
|
+
}
|
|
129
152
|
const wsCtx = createWSContext(ws);
|
|
130
153
|
const evt = new CloseEvent('close', { code, reason });
|
|
131
154
|
tag.events.onClose(evt, wsCtx);
|
|
@@ -134,8 +157,10 @@ export class SyncDurableObject {
|
|
|
134
157
|
/** Dispatch WebSocket error events to Hono event handlers. */
|
|
135
158
|
async webSocketError(ws, _error) {
|
|
136
159
|
const tag = socketTags.get(ws);
|
|
137
|
-
if (!tag?.events.onError)
|
|
160
|
+
if (!tag?.events.onError) {
|
|
161
|
+
closeStaleSocket(ws);
|
|
138
162
|
return;
|
|
163
|
+
}
|
|
139
164
|
const wsCtx = createWSContext(ws);
|
|
140
165
|
const evt = new Event('error');
|
|
141
166
|
tag.events.onError(evt, wsCtx);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"durable-object.js","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAU3D;;;GAGG;AACH,MAAM,UAAU,GAAG,IAAI,OAAO,EAA2B,CAAC;AAE1D,SAAS,eAAe,CAAC,EAAa,EAAwB;IAC5D,OAAO,IAAI,SAAS,CAAY;QAC9B,IAAI,CAAC,IAAI,EAAE;YACT,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAAA,CACf;QACD,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE;YAClB,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAA,CACxB;QACD,GAAG,EAAE,EAAE;QACP,IAAI,UAAU,GAAG;YACf,OAAO,EAAE,CAAC,UAA2B,CAAC;QAAA,CACvC;KACF,CAAC,CAAC;AAAA,CACJ;AAED;;;;;;GAMG;AACH,SAAS,wBAAwB,CAC/B,OAA2B,EACE;IAC7B,OAAO,qBAAqB,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC;QAC3C,MAAM,IAAI,GAAG,IAAI,aAAa,EAAE,CAAC;QACjC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAA2B,CAAC;QAEvE,4DAA4D;QAC5D,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAEhC,6EAA6E;QAC7E,UAAU,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAA6B,EAAE,CAAC,CAAC;QAElE,yDAAyD;QACzD,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;QAE1C,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAAA,CAC/D,CAAC,CAAC;AAAA,CACJ;AAED,8EAA8E;AAC9E,+BAA+B;AAC/B,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,OAAgB,iBAAiB;IAG3B,GAAG,CAAqB;IACxB,GAAG,CAAI;IAET,GAAG,GAAiC,IAAI,CAAC;IACzC,WAAW,GAAyB,IAAI,CAAC;IACzC,kBAAkB,CAA8B;IAExD,YAAY,GAAuB,EAAE,GAAM,EAAE;QAC3C,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,kBAAkB,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAC;IAAA,
|
|
1
|
+
{"version":3,"file":"durable-object.js","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - D1 + SQLite: `@syncular/dialect-d1` + `@syncular/server-dialect-sqlite`
|
|
13
13
|
* - Neon + Postgres: `@syncular/dialect-neon` + `@syncular/server-dialect-postgres`
|
|
14
14
|
*/
|
|
15
|
-
export * from './durable-object';
|
|
16
|
-
export * from './r2';
|
|
17
|
-
export * from './worker';
|
|
15
|
+
export * from './durable-object.js';
|
|
16
|
+
export * from './r2.js';
|
|
17
|
+
export * from './worker.js';
|
|
18
18
|
//# sourceMappingURL=index.js.map
|
package/dist/r2.d.ts
CHANGED
|
@@ -73,10 +73,18 @@ export interface BlobStorageAdapter {
|
|
|
73
73
|
* Store blob data directly (for adapters that support direct storage).
|
|
74
74
|
*/
|
|
75
75
|
put?(hash: string, data: Uint8Array, metadata?: Record<string, unknown>): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Store blob data directly from a stream.
|
|
78
|
+
*/
|
|
79
|
+
putStream?(hash: string, stream: ReadableStream<Uint8Array>, metadata?: Record<string, unknown>): Promise<void>;
|
|
76
80
|
/**
|
|
77
81
|
* Get blob data directly (for adapters that support direct retrieval).
|
|
78
82
|
*/
|
|
79
83
|
get?(hash: string): Promise<Uint8Array | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Get blob data directly as a stream.
|
|
86
|
+
*/
|
|
87
|
+
getStream?(hash: string): Promise<ReadableStream<Uint8Array> | null>;
|
|
80
88
|
}
|
|
81
89
|
/**
|
|
82
90
|
* Token signer interface for creating/verifying upload/download tokens.
|
package/dist/r2.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,2BAA2B;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,kBAAkB;IAClB,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IACvB,uBAAuB;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEtE;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEhE;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvC;;OAEG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpC;;OAEG;IACH,WAAW,CAAC,CACV,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAEvD;;OAEG;IACH,GAAG,CAAC,CACF,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,UAAU,EAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB;;OAEG;IACH,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"r2.d.ts","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"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"}
|
package/dist/r2.js
CHANGED
|
@@ -75,6 +75,105 @@ export function createR2BlobStorageAdapter(options) {
|
|
|
75
75
|
const hex = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
76
76
|
return `${keyPrefix}${hex}`;
|
|
77
77
|
}
|
|
78
|
+
function resolveMimeType(metadata) {
|
|
79
|
+
return typeof metadata?.mimeType === 'string'
|
|
80
|
+
? metadata.mimeType
|
|
81
|
+
: 'application/octet-stream';
|
|
82
|
+
}
|
|
83
|
+
function resolveChecksum(hash, metadata) {
|
|
84
|
+
if (metadata?.disableChecksum === true) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const explicitChecksum = metadata?.checksumSha256;
|
|
88
|
+
if (typeof explicitChecksum === 'string' &&
|
|
89
|
+
/^[0-9a-f]{64}$/i.test(explicitChecksum)) {
|
|
90
|
+
return explicitChecksum.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
return hash.startsWith('sha256:') ? hash.slice(7) : undefined;
|
|
93
|
+
}
|
|
94
|
+
function resolveContentLength(metadata) {
|
|
95
|
+
const candidates = [
|
|
96
|
+
metadata?.contentLength,
|
|
97
|
+
metadata?.byteLength,
|
|
98
|
+
metadata?.size,
|
|
99
|
+
];
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
if (typeof candidate !== 'number')
|
|
102
|
+
continue;
|
|
103
|
+
if (!Number.isFinite(candidate) || candidate < 0)
|
|
104
|
+
continue;
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
async function streamToBytes(stream) {
|
|
110
|
+
const reader = stream.getReader();
|
|
111
|
+
try {
|
|
112
|
+
const chunks = [];
|
|
113
|
+
let total = 0;
|
|
114
|
+
while (true) {
|
|
115
|
+
const { done, value } = await reader.read();
|
|
116
|
+
if (done)
|
|
117
|
+
break;
|
|
118
|
+
if (!value)
|
|
119
|
+
continue;
|
|
120
|
+
chunks.push(value);
|
|
121
|
+
total += value.length;
|
|
122
|
+
}
|
|
123
|
+
const output = new Uint8Array(total);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const chunk of chunks) {
|
|
126
|
+
output.set(chunk, offset);
|
|
127
|
+
offset += chunk.length;
|
|
128
|
+
}
|
|
129
|
+
return output;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
reader.releaseLock();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function putStreamInternal(hash, stream, metadata) {
|
|
136
|
+
const key = getKey(hash);
|
|
137
|
+
const mimeType = resolveMimeType(metadata);
|
|
138
|
+
const checksum = resolveChecksum(hash, metadata);
|
|
139
|
+
const contentLength = resolveContentLength(metadata);
|
|
140
|
+
if (typeof contentLength === 'number' && contentLength >= 0) {
|
|
141
|
+
if (typeof FixedLengthStream !== 'undefined') {
|
|
142
|
+
const fixedLength = new FixedLengthStream(contentLength);
|
|
143
|
+
await Promise.all([
|
|
144
|
+
stream.pipeTo(fixedLength.writable),
|
|
145
|
+
bucket.put(key, fixedLength.readable, {
|
|
146
|
+
httpMetadata: { contentType: mimeType },
|
|
147
|
+
sha256: checksum,
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const bufferedBody = await streamToBytes(stream);
|
|
153
|
+
if (bufferedBody.byteLength !== contentLength) {
|
|
154
|
+
throw new Error(`Blob content length mismatch: expected ${contentLength}, got ${bufferedBody.byteLength}`);
|
|
155
|
+
}
|
|
156
|
+
await bucket.put(key, bufferedBody, {
|
|
157
|
+
httpMetadata: { contentType: mimeType },
|
|
158
|
+
sha256: checksum,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const bufferedBody = await streamToBytes(stream);
|
|
163
|
+
await bucket.put(key, bufferedBody, {
|
|
164
|
+
httpMetadata: {
|
|
165
|
+
contentType: mimeType,
|
|
166
|
+
},
|
|
167
|
+
sha256: checksum,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function getStreamInternal(hash) {
|
|
171
|
+
const key = getKey(hash);
|
|
172
|
+
const object = await bucket.get(key);
|
|
173
|
+
if (!object)
|
|
174
|
+
return null;
|
|
175
|
+
return object.body;
|
|
176
|
+
}
|
|
78
177
|
return {
|
|
79
178
|
name: 'r2',
|
|
80
179
|
async signUpload(opts) {
|
|
@@ -117,22 +216,49 @@ export function createR2BlobStorageAdapter(options) {
|
|
|
117
216
|
},
|
|
118
217
|
async put(hash, data, metadata) {
|
|
119
218
|
const key = getKey(hash);
|
|
120
|
-
const mimeType =
|
|
121
|
-
|
|
122
|
-
: 'application/octet-stream';
|
|
219
|
+
const mimeType = resolveMimeType(metadata);
|
|
220
|
+
const checksum = resolveChecksum(hash, metadata);
|
|
123
221
|
await bucket.put(key, data, {
|
|
124
222
|
httpMetadata: {
|
|
125
223
|
contentType: mimeType,
|
|
126
224
|
},
|
|
127
|
-
sha256:
|
|
225
|
+
sha256: checksum,
|
|
128
226
|
});
|
|
129
227
|
},
|
|
228
|
+
async putStream(hash, stream, metadata) {
|
|
229
|
+
await putStreamInternal(hash, stream, metadata);
|
|
230
|
+
},
|
|
130
231
|
async get(hash) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
if (!object)
|
|
232
|
+
const stream = await getStreamInternal(hash);
|
|
233
|
+
if (!stream)
|
|
134
234
|
return null;
|
|
135
|
-
|
|
235
|
+
const reader = stream.getReader();
|
|
236
|
+
try {
|
|
237
|
+
const chunks = [];
|
|
238
|
+
let total = 0;
|
|
239
|
+
while (true) {
|
|
240
|
+
const { done, value } = await reader.read();
|
|
241
|
+
if (done)
|
|
242
|
+
break;
|
|
243
|
+
if (!value)
|
|
244
|
+
continue;
|
|
245
|
+
chunks.push(value);
|
|
246
|
+
total += value.length;
|
|
247
|
+
}
|
|
248
|
+
const out = new Uint8Array(total);
|
|
249
|
+
let offset = 0;
|
|
250
|
+
for (const chunk of chunks) {
|
|
251
|
+
out.set(chunk, offset);
|
|
252
|
+
offset += chunk.length;
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
reader.releaseLock();
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
async getStream(hash) {
|
|
261
|
+
return getStreamInternal(hash);
|
|
136
262
|
},
|
|
137
263
|
};
|
|
138
264
|
}
|
package/dist/r2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"r2.js","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"r2.js","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/server-cloudflare",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2-127",
|
|
4
|
+
"description": "Cloudflare Workers adapter for the Syncular server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Benjamin Kniffler",
|
|
7
|
+
"homepage": "https://syncular.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/syncular/syncular.git",
|
|
11
|
+
"directory": "packages/server-cloudflare"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/syncular/syncular/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sync",
|
|
18
|
+
"offline-first",
|
|
19
|
+
"realtime",
|
|
20
|
+
"database",
|
|
21
|
+
"typescript",
|
|
22
|
+
"cloudflare",
|
|
23
|
+
"workers",
|
|
24
|
+
"edge"
|
|
25
|
+
],
|
|
4
26
|
"private": false,
|
|
5
27
|
"publishConfig": {
|
|
6
28
|
"access": "public"
|
|
@@ -38,14 +60,15 @@
|
|
|
38
60
|
},
|
|
39
61
|
"scripts": {
|
|
40
62
|
"tsgo": "tsgo --noEmit",
|
|
41
|
-
"build": "
|
|
63
|
+
"build": "tsgo",
|
|
64
|
+
"release": "bunx syncular-publish"
|
|
42
65
|
},
|
|
43
66
|
"peerDependencies": {
|
|
44
67
|
"hono": "^4.0.0"
|
|
45
68
|
},
|
|
46
69
|
"devDependencies": {
|
|
47
|
-
"@cloudflare/workers-types": "
|
|
48
|
-
"@syncular/config": "
|
|
70
|
+
"@cloudflare/workers-types": "*",
|
|
71
|
+
"@syncular/config": "0.0.0"
|
|
49
72
|
},
|
|
50
73
|
"files": [
|
|
51
74
|
"dist",
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { Hono } from 'hono';
|
|
3
|
+
import type { UpgradeWebSocket } from 'hono/ws';
|
|
4
|
+
import { SyncDurableObject } from './durable-object';
|
|
5
|
+
|
|
6
|
+
const staleSocketCloseCode = 1012;
|
|
7
|
+
const staleSocketCloseReason = 'WebSocket session expired; reconnect required';
|
|
8
|
+
|
|
9
|
+
class TestSyncDurableObject extends SyncDurableObject<Record<string, never>> {
|
|
10
|
+
async setup(
|
|
11
|
+
_app: Hono<{ Bindings: Record<string, never> }>,
|
|
12
|
+
_env: Record<string, never>,
|
|
13
|
+
_upgradeWebSocket: UpgradeWebSocket<WebSocket>
|
|
14
|
+
): Promise<void> {}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createSocketTracker(): {
|
|
18
|
+
socket: WebSocket;
|
|
19
|
+
closes: Array<{ code: number | undefined; reason: string | undefined }>;
|
|
20
|
+
} {
|
|
21
|
+
const closes: Array<{
|
|
22
|
+
code: number | undefined;
|
|
23
|
+
reason: string | undefined;
|
|
24
|
+
}> = [];
|
|
25
|
+
const socket = {
|
|
26
|
+
close(code?: number, reason?: string) {
|
|
27
|
+
closes.push({ code, reason });
|
|
28
|
+
},
|
|
29
|
+
} as WebSocket;
|
|
30
|
+
return { socket, closes };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createState(sockets: WebSocket[]): DurableObjectState {
|
|
34
|
+
return {
|
|
35
|
+
acceptWebSocket() {},
|
|
36
|
+
getWebSockets() {
|
|
37
|
+
return sockets;
|
|
38
|
+
},
|
|
39
|
+
} as DurableObjectState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('SyncDurableObject stale websocket handling', () => {
|
|
43
|
+
test('closes untracked sockets on construction (hibernation wake-up path)', () => {
|
|
44
|
+
const tracked = createSocketTracker();
|
|
45
|
+
const state = createState([tracked.socket]);
|
|
46
|
+
|
|
47
|
+
new TestSyncDurableObject(state, {});
|
|
48
|
+
|
|
49
|
+
expect(tracked.closes).toEqual([
|
|
50
|
+
{ code: staleSocketCloseCode, reason: staleSocketCloseReason },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('closes unknown sockets when receiving websocket messages', async () => {
|
|
55
|
+
const state = createState([]);
|
|
56
|
+
const durableObject = new TestSyncDurableObject(state, {});
|
|
57
|
+
const tracked = createSocketTracker();
|
|
58
|
+
|
|
59
|
+
await durableObject.webSocketMessage(tracked.socket, 'hello');
|
|
60
|
+
|
|
61
|
+
expect(tracked.closes).toEqual([
|
|
62
|
+
{ code: staleSocketCloseCode, reason: staleSocketCloseReason },
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/durable-object.ts
CHANGED
|
@@ -47,12 +47,24 @@ interface WebSocketTag {
|
|
|
47
47
|
events: WSEvents<WebSocket>;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
const STALE_SOCKET_CLOSE_CODE = 1012;
|
|
51
|
+
const STALE_SOCKET_CLOSE_REASON =
|
|
52
|
+
'WebSocket session expired; reconnect required';
|
|
53
|
+
|
|
50
54
|
/**
|
|
51
55
|
* WeakMap from server-side WebSocket → tag with event handlers.
|
|
52
56
|
* Populated on upgrade, read in webSocketMessage/webSocketClose.
|
|
53
57
|
*/
|
|
54
58
|
const socketTags = new WeakMap<WebSocket, WebSocketTag>();
|
|
55
59
|
|
|
60
|
+
function closeStaleSocket(ws: WebSocket): void {
|
|
61
|
+
try {
|
|
62
|
+
ws.close(STALE_SOCKET_CLOSE_CODE, STALE_SOCKET_CLOSE_REASON);
|
|
63
|
+
} catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
56
68
|
function createWSContext(ws: WebSocket): WSContext<WebSocket> {
|
|
57
69
|
return new WSContext<WebSocket>({
|
|
58
70
|
send(data) {
|
|
@@ -119,6 +131,7 @@ export abstract class SyncDurableObject<
|
|
|
119
131
|
this.ctx = ctx;
|
|
120
132
|
this.env = env;
|
|
121
133
|
this.doUpgradeWebSocket = createDOUpgradeWebSocket(ctx);
|
|
134
|
+
this.closeUntrackedSockets();
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
/**
|
|
@@ -147,6 +160,14 @@ export abstract class SyncDurableObject<
|
|
|
147
160
|
return this.app!;
|
|
148
161
|
}
|
|
149
162
|
|
|
163
|
+
private closeUntrackedSockets(): void {
|
|
164
|
+
const sockets = this.ctx.getWebSockets();
|
|
165
|
+
for (const ws of sockets) {
|
|
166
|
+
if (socketTags.has(ws)) continue;
|
|
167
|
+
closeStaleSocket(ws);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
/** Handle incoming HTTP requests (and WebSocket upgrades). */
|
|
151
172
|
async fetch(request: Request): Promise<Response> {
|
|
152
173
|
const app = await this.getApp();
|
|
@@ -159,7 +180,10 @@ export abstract class SyncDurableObject<
|
|
|
159
180
|
message: string | ArrayBuffer
|
|
160
181
|
): Promise<void> {
|
|
161
182
|
const tag = socketTags.get(ws);
|
|
162
|
-
if (!tag?.events.onMessage)
|
|
183
|
+
if (!tag?.events.onMessage) {
|
|
184
|
+
closeStaleSocket(ws);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
163
187
|
|
|
164
188
|
const wsCtx = createWSContext(ws);
|
|
165
189
|
const evt = new MessageEvent('message', { data: message });
|
|
@@ -174,7 +198,10 @@ export abstract class SyncDurableObject<
|
|
|
174
198
|
_wasClean: boolean
|
|
175
199
|
): Promise<void> {
|
|
176
200
|
const tag = socketTags.get(ws);
|
|
177
|
-
if (!tag?.events.onClose)
|
|
201
|
+
if (!tag?.events.onClose) {
|
|
202
|
+
socketTags.delete(ws);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
178
205
|
|
|
179
206
|
const wsCtx = createWSContext(ws);
|
|
180
207
|
const evt = new CloseEvent('close', { code, reason });
|
|
@@ -185,7 +212,10 @@ export abstract class SyncDurableObject<
|
|
|
185
212
|
/** Dispatch WebSocket error events to Hono event handlers. */
|
|
186
213
|
async webSocketError(ws: WebSocket, _error: unknown): Promise<void> {
|
|
187
214
|
const tag = socketTags.get(ws);
|
|
188
|
-
if (!tag?.events.onError)
|
|
215
|
+
if (!tag?.events.onError) {
|
|
216
|
+
closeStaleSocket(ws);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
189
219
|
|
|
190
220
|
const wsCtx = createWSContext(ws);
|
|
191
221
|
const evt = new Event('error');
|
package/src/r2.test.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
type BlobStorageAdapter,
|
|
4
|
+
type BlobTokenSigner,
|
|
5
|
+
createR2BlobStorageAdapter,
|
|
6
|
+
} from './r2';
|
|
7
|
+
|
|
8
|
+
function createBodyStream(
|
|
9
|
+
chunks: readonly Uint8Array[]
|
|
10
|
+
): ReadableStream<Uint8Array> {
|
|
11
|
+
return new ReadableStream<Uint8Array>({
|
|
12
|
+
start(controller) {
|
|
13
|
+
for (const chunk of chunks) {
|
|
14
|
+
controller.enqueue(chunk);
|
|
15
|
+
}
|
|
16
|
+
controller.close();
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function consumeStream(
|
|
22
|
+
stream: ReadableStream<Uint8Array>
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const reader = stream.getReader();
|
|
25
|
+
try {
|
|
26
|
+
while (true) {
|
|
27
|
+
const { done } = await reader.read();
|
|
28
|
+
if (done) break;
|
|
29
|
+
}
|
|
30
|
+
} finally {
|
|
31
|
+
reader.releaseLock();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createTokenSigner(): BlobTokenSigner {
|
|
36
|
+
return {
|
|
37
|
+
async sign() {
|
|
38
|
+
return 'token';
|
|
39
|
+
},
|
|
40
|
+
async verify() {
|
|
41
|
+
return null;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createAdapterWithCapturedPuts(
|
|
47
|
+
captured: Array<{ body: unknown; options: unknown }>
|
|
48
|
+
): BlobStorageAdapter {
|
|
49
|
+
const bucket = {
|
|
50
|
+
async put(_key: string, body: unknown, options?: unknown) {
|
|
51
|
+
captured.push({ body, options });
|
|
52
|
+
if (body instanceof ReadableStream) {
|
|
53
|
+
await consumeStream(body as ReadableStream<Uint8Array>);
|
|
54
|
+
}
|
|
55
|
+
return {} as R2Object;
|
|
56
|
+
},
|
|
57
|
+
async get() {
|
|
58
|
+
return null;
|
|
59
|
+
},
|
|
60
|
+
async head() {
|
|
61
|
+
return null;
|
|
62
|
+
},
|
|
63
|
+
async delete() {},
|
|
64
|
+
} as unknown as R2Bucket;
|
|
65
|
+
|
|
66
|
+
return createR2BlobStorageAdapter({
|
|
67
|
+
bucket,
|
|
68
|
+
baseUrl: '/api/sync',
|
|
69
|
+
tokenSigner: createTokenSigner(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe('createR2BlobStorageAdapter.putStream', () => {
|
|
74
|
+
test('buffers unknown-length streams before upload', async () => {
|
|
75
|
+
const puts: Array<{ body: unknown; options: unknown }> = [];
|
|
76
|
+
const adapter = createAdapterWithCapturedPuts(puts);
|
|
77
|
+
|
|
78
|
+
await adapter.putStream?.(
|
|
79
|
+
'sha256:abc123',
|
|
80
|
+
createBodyStream([new Uint8Array([1, 2]), new Uint8Array([3, 4])])
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(puts).toHaveLength(1);
|
|
84
|
+
expect(puts[0]?.body).toBeInstanceOf(Uint8Array);
|
|
85
|
+
expect(Array.from(puts[0]?.body as Uint8Array)).toEqual([1, 2, 3, 4]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('uses fixed-length stream when byteLength is provided', async () => {
|
|
89
|
+
const puts: Array<{ body: unknown; options: unknown }> = [];
|
|
90
|
+
const adapter = createAdapterWithCapturedPuts(puts);
|
|
91
|
+
|
|
92
|
+
await adapter.putStream?.(
|
|
93
|
+
'sha256:def456',
|
|
94
|
+
createBodyStream([new Uint8Array([9, 8, 7, 6])]),
|
|
95
|
+
{ byteLength: 4 }
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(puts).toHaveLength(1);
|
|
99
|
+
if (typeof FixedLengthStream !== 'undefined') {
|
|
100
|
+
expect(puts[0]?.body).toBeInstanceOf(ReadableStream);
|
|
101
|
+
} else {
|
|
102
|
+
expect(puts[0]?.body).toBeInstanceOf(Uint8Array);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('omits checksum when disableChecksum metadata is set', async () => {
|
|
107
|
+
const puts: Array<{ body: unknown; options: unknown }> = [];
|
|
108
|
+
const adapter = createAdapterWithCapturedPuts(puts);
|
|
109
|
+
|
|
110
|
+
await adapter.putStream?.(
|
|
111
|
+
'sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef',
|
|
112
|
+
createBodyStream([new Uint8Array([1, 2, 3])]),
|
|
113
|
+
{ disableChecksum: true }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(puts).toHaveLength(1);
|
|
117
|
+
const options = puts[0]?.options as { sha256?: string };
|
|
118
|
+
expect(options.sha256).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
});
|
package/src/r2.ts
CHANGED
|
@@ -92,10 +92,24 @@ export interface BlobStorageAdapter {
|
|
|
92
92
|
metadata?: Record<string, unknown>
|
|
93
93
|
): Promise<void>;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Store blob data directly from a stream.
|
|
97
|
+
*/
|
|
98
|
+
putStream?(
|
|
99
|
+
hash: string,
|
|
100
|
+
stream: ReadableStream<Uint8Array>,
|
|
101
|
+
metadata?: Record<string, unknown>
|
|
102
|
+
): Promise<void>;
|
|
103
|
+
|
|
95
104
|
/**
|
|
96
105
|
* Get blob data directly (for adapters that support direct retrieval).
|
|
97
106
|
*/
|
|
98
107
|
get?(hash: string): Promise<Uint8Array | null>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get blob data directly as a stream.
|
|
111
|
+
*/
|
|
112
|
+
getStream?(hash: string): Promise<ReadableStream<Uint8Array> | null>;
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
/**
|
|
@@ -228,6 +242,128 @@ export function createR2BlobStorageAdapter(
|
|
|
228
242
|
return `${keyPrefix}${hex}`;
|
|
229
243
|
}
|
|
230
244
|
|
|
245
|
+
function resolveMimeType(metadata?: Record<string, unknown>): string {
|
|
246
|
+
return typeof metadata?.mimeType === 'string'
|
|
247
|
+
? metadata.mimeType
|
|
248
|
+
: 'application/octet-stream';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function resolveChecksum(
|
|
252
|
+
hash: string,
|
|
253
|
+
metadata?: Record<string, unknown>
|
|
254
|
+
): string | undefined {
|
|
255
|
+
if (metadata?.disableChecksum === true) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const explicitChecksum = metadata?.checksumSha256;
|
|
260
|
+
if (
|
|
261
|
+
typeof explicitChecksum === 'string' &&
|
|
262
|
+
/^[0-9a-f]{64}$/i.test(explicitChecksum)
|
|
263
|
+
) {
|
|
264
|
+
return explicitChecksum.toLowerCase();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return hash.startsWith('sha256:') ? hash.slice(7) : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveContentLength(
|
|
271
|
+
metadata?: Record<string, unknown>
|
|
272
|
+
): number | undefined {
|
|
273
|
+
const candidates = [
|
|
274
|
+
metadata?.contentLength,
|
|
275
|
+
metadata?.byteLength,
|
|
276
|
+
metadata?.size,
|
|
277
|
+
];
|
|
278
|
+
for (const candidate of candidates) {
|
|
279
|
+
if (typeof candidate !== 'number') continue;
|
|
280
|
+
if (!Number.isFinite(candidate) || candidate < 0) continue;
|
|
281
|
+
return candidate;
|
|
282
|
+
}
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function streamToBytes(
|
|
287
|
+
stream: ReadableStream<Uint8Array>
|
|
288
|
+
): Promise<Uint8Array> {
|
|
289
|
+
const reader = stream.getReader();
|
|
290
|
+
try {
|
|
291
|
+
const chunks: Uint8Array[] = [];
|
|
292
|
+
let total = 0;
|
|
293
|
+
while (true) {
|
|
294
|
+
const { done, value } = await reader.read();
|
|
295
|
+
if (done) break;
|
|
296
|
+
if (!value) continue;
|
|
297
|
+
chunks.push(value);
|
|
298
|
+
total += value.length;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const output = new Uint8Array(total);
|
|
302
|
+
let offset = 0;
|
|
303
|
+
for (const chunk of chunks) {
|
|
304
|
+
output.set(chunk, offset);
|
|
305
|
+
offset += chunk.length;
|
|
306
|
+
}
|
|
307
|
+
return output;
|
|
308
|
+
} finally {
|
|
309
|
+
reader.releaseLock();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function putStreamInternal(
|
|
314
|
+
hash: string,
|
|
315
|
+
stream: ReadableStream<Uint8Array>,
|
|
316
|
+
metadata?: Record<string, unknown>
|
|
317
|
+
): Promise<void> {
|
|
318
|
+
const key = getKey(hash);
|
|
319
|
+
const mimeType = resolveMimeType(metadata);
|
|
320
|
+
const checksum = resolveChecksum(hash, metadata);
|
|
321
|
+
const contentLength = resolveContentLength(metadata);
|
|
322
|
+
|
|
323
|
+
if (typeof contentLength === 'number' && contentLength >= 0) {
|
|
324
|
+
if (typeof FixedLengthStream !== 'undefined') {
|
|
325
|
+
const fixedLength = new FixedLengthStream(contentLength);
|
|
326
|
+
await Promise.all([
|
|
327
|
+
stream.pipeTo(fixedLength.writable),
|
|
328
|
+
bucket.put(key, fixedLength.readable, {
|
|
329
|
+
httpMetadata: { contentType: mimeType },
|
|
330
|
+
sha256: checksum,
|
|
331
|
+
}),
|
|
332
|
+
]);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const bufferedBody = await streamToBytes(stream);
|
|
337
|
+
if (bufferedBody.byteLength !== contentLength) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Blob content length mismatch: expected ${contentLength}, got ${bufferedBody.byteLength}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
await bucket.put(key, bufferedBody, {
|
|
343
|
+
httpMetadata: { contentType: mimeType },
|
|
344
|
+
sha256: checksum,
|
|
345
|
+
});
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const bufferedBody = await streamToBytes(stream);
|
|
350
|
+
await bucket.put(key, bufferedBody, {
|
|
351
|
+
httpMetadata: {
|
|
352
|
+
contentType: mimeType,
|
|
353
|
+
},
|
|
354
|
+
sha256: checksum,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function getStreamInternal(
|
|
359
|
+
hash: string
|
|
360
|
+
): Promise<ReadableStream<Uint8Array> | null> {
|
|
361
|
+
const key = getKey(hash);
|
|
362
|
+
const object = await bucket.get(key);
|
|
363
|
+
if (!object) return null;
|
|
364
|
+
return object.body as ReadableStream<Uint8Array> | null;
|
|
365
|
+
}
|
|
366
|
+
|
|
231
367
|
return {
|
|
232
368
|
name: 'r2',
|
|
233
369
|
|
|
@@ -291,25 +427,53 @@ export function createR2BlobStorageAdapter(
|
|
|
291
427
|
metadata?: Record<string, unknown>
|
|
292
428
|
): Promise<void> {
|
|
293
429
|
const key = getKey(hash);
|
|
294
|
-
const mimeType =
|
|
295
|
-
|
|
296
|
-
? metadata.mimeType
|
|
297
|
-
: 'application/octet-stream';
|
|
298
|
-
|
|
430
|
+
const mimeType = resolveMimeType(metadata);
|
|
431
|
+
const checksum = resolveChecksum(hash, metadata);
|
|
299
432
|
await bucket.put(key, data, {
|
|
300
433
|
httpMetadata: {
|
|
301
434
|
contentType: mimeType,
|
|
302
435
|
},
|
|
303
|
-
sha256:
|
|
436
|
+
sha256: checksum,
|
|
304
437
|
});
|
|
305
438
|
},
|
|
306
439
|
|
|
440
|
+
async putStream(
|
|
441
|
+
hash: string,
|
|
442
|
+
stream: ReadableStream<Uint8Array>,
|
|
443
|
+
metadata?: Record<string, unknown>
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
await putStreamInternal(hash, stream, metadata);
|
|
446
|
+
},
|
|
447
|
+
|
|
307
448
|
async get(hash: string): Promise<Uint8Array | null> {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
449
|
+
const stream = await getStreamInternal(hash);
|
|
450
|
+
if (!stream) return null;
|
|
451
|
+
|
|
452
|
+
const reader = stream.getReader();
|
|
453
|
+
try {
|
|
454
|
+
const chunks: Uint8Array[] = [];
|
|
455
|
+
let total = 0;
|
|
456
|
+
while (true) {
|
|
457
|
+
const { done, value } = await reader.read();
|
|
458
|
+
if (done) break;
|
|
459
|
+
if (!value) continue;
|
|
460
|
+
chunks.push(value);
|
|
461
|
+
total += value.length;
|
|
462
|
+
}
|
|
463
|
+
const out = new Uint8Array(total);
|
|
464
|
+
let offset = 0;
|
|
465
|
+
for (const chunk of chunks) {
|
|
466
|
+
out.set(chunk, offset);
|
|
467
|
+
offset += chunk.length;
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
} finally {
|
|
471
|
+
reader.releaseLock();
|
|
472
|
+
}
|
|
473
|
+
},
|
|
311
474
|
|
|
312
|
-
|
|
475
|
+
async getStream(hash: string): Promise<ReadableStream<Uint8Array> | null> {
|
|
476
|
+
return getStreamInternal(hash);
|
|
313
477
|
},
|
|
314
478
|
};
|
|
315
479
|
}
|