@syncular/server-cloudflare 0.0.1-100
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/dist/durable-object.d.ts +89 -0
- package/dist/durable-object.d.ts.map +1 -0
- package/dist/durable-object.js +195 -0
- package/dist/durable-object.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/r2.d.ts +148 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +219 -0
- package/dist/r2.js.map +1 -0
- package/dist/worker.d.ts +42 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +59 -0
- package/dist/worker.js.map +1 -0
- package/package.json +77 -0
- package/src/durable-object.test.ts +65 -0
- package/src/durable-object.ts +271 -0
- package/src/index.ts +18 -0
- package/src/r2.ts +422 -0
- package/src/worker.ts +73 -0
package/dist/r2.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R2 blob storage adapter using native R2Bucket binding.
|
|
3
|
+
*
|
|
4
|
+
* This adapter stores blobs in Cloudflare R2 using the native binding,
|
|
5
|
+
* without requiring the AWS SDK. Since R2 bindings don't support presigned URLs,
|
|
6
|
+
* this adapter generates signed tokens that allow uploads/downloads through
|
|
7
|
+
* the Worker's blob routes (similar to the database adapter).
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Create a simple HMAC-based token signer.
|
|
11
|
+
*/
|
|
12
|
+
export function createHmacTokenSigner(secret) {
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
async function hmacSign(data) {
|
|
15
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
16
|
+
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
|
|
17
|
+
return bufferToHex(new Uint8Array(signature));
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
async sign(payload, _expiresIn) {
|
|
21
|
+
const data = JSON.stringify(payload);
|
|
22
|
+
const dataB64 = btoa(data);
|
|
23
|
+
const sig = await hmacSign(dataB64);
|
|
24
|
+
return `${dataB64}.${sig}`;
|
|
25
|
+
},
|
|
26
|
+
async verify(token) {
|
|
27
|
+
const [dataB64, sig] = token.split('.');
|
|
28
|
+
if (!dataB64 || !sig)
|
|
29
|
+
return null;
|
|
30
|
+
const expectedSig = await hmacSign(dataB64);
|
|
31
|
+
if (sig !== expectedSig)
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(atob(dataB64));
|
|
35
|
+
if (Date.now() > data.expiresAt)
|
|
36
|
+
return null;
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function bufferToHex(buffer) {
|
|
46
|
+
return Array.from(buffer)
|
|
47
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
48
|
+
.join('');
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create an R2 blob storage adapter using native R2Bucket binding.
|
|
52
|
+
*
|
|
53
|
+
* Since R2 bindings don't support presigned URLs, this adapter generates
|
|
54
|
+
* signed tokens and uses Worker-proxied uploads/downloads.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { createR2BlobStorageAdapter, createHmacTokenSigner } from '@syncular/server-cloudflare/r2';
|
|
59
|
+
*
|
|
60
|
+
* type Env = { BLOBS: R2Bucket };
|
|
61
|
+
*
|
|
62
|
+
* const adapter = createR2BlobStorageAdapter({
|
|
63
|
+
* bucket: env.BLOBS,
|
|
64
|
+
* baseUrl: 'https://api.example.com/sync',
|
|
65
|
+
* tokenSigner: createHmacTokenSigner(env.BLOB_SECRET),
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export function createR2BlobStorageAdapter(options) {
|
|
70
|
+
const { bucket, keyPrefix = '', baseUrl, tokenSigner } = options;
|
|
71
|
+
// Normalize base URL (remove trailing slash)
|
|
72
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, '');
|
|
73
|
+
function getKey(hash) {
|
|
74
|
+
// Remove "sha256:" prefix and use hex as key
|
|
75
|
+
const hex = hash.startsWith('sha256:') ? hash.slice(7) : hash;
|
|
76
|
+
return `${keyPrefix}${hex}`;
|
|
77
|
+
}
|
|
78
|
+
function resolveMimeType(metadata) {
|
|
79
|
+
return typeof metadata?.mimeType === 'string'
|
|
80
|
+
? metadata.mimeType
|
|
81
|
+
: 'application/octet-stream';
|
|
82
|
+
}
|
|
83
|
+
function resolveChecksum(hash) {
|
|
84
|
+
return hash.startsWith('sha256:') ? hash.slice(7) : undefined;
|
|
85
|
+
}
|
|
86
|
+
function resolveContentLength(metadata) {
|
|
87
|
+
const candidates = [
|
|
88
|
+
metadata?.contentLength,
|
|
89
|
+
metadata?.byteLength,
|
|
90
|
+
metadata?.size,
|
|
91
|
+
];
|
|
92
|
+
for (const candidate of candidates) {
|
|
93
|
+
if (typeof candidate !== 'number')
|
|
94
|
+
continue;
|
|
95
|
+
if (!Number.isFinite(candidate) || candidate < 0)
|
|
96
|
+
continue;
|
|
97
|
+
return candidate;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
async function putStreamInternal(hash, stream, metadata) {
|
|
102
|
+
const key = getKey(hash);
|
|
103
|
+
const mimeType = resolveMimeType(metadata);
|
|
104
|
+
const checksum = resolveChecksum(hash);
|
|
105
|
+
const contentLength = resolveContentLength(metadata);
|
|
106
|
+
if (typeof contentLength === 'number' && contentLength >= 0) {
|
|
107
|
+
const fixedLength = new FixedLengthStream(contentLength);
|
|
108
|
+
await Promise.all([
|
|
109
|
+
stream.pipeTo(fixedLength.writable),
|
|
110
|
+
bucket.put(key, fixedLength.readable, {
|
|
111
|
+
httpMetadata: { contentType: mimeType },
|
|
112
|
+
sha256: checksum,
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await bucket.put(key, stream, {
|
|
118
|
+
httpMetadata: {
|
|
119
|
+
contentType: mimeType,
|
|
120
|
+
},
|
|
121
|
+
sha256: checksum,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function getStreamInternal(hash) {
|
|
125
|
+
const key = getKey(hash);
|
|
126
|
+
const object = await bucket.get(key);
|
|
127
|
+
if (!object)
|
|
128
|
+
return null;
|
|
129
|
+
return object.body;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
name: 'r2',
|
|
133
|
+
async signUpload(opts) {
|
|
134
|
+
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
135
|
+
const token = await tokenSigner.sign({ hash: opts.hash, action: 'upload', expiresAt }, opts.expiresIn);
|
|
136
|
+
// URL points to server's blob upload endpoint
|
|
137
|
+
const url = `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/upload?token=${encodeURIComponent(token)}`;
|
|
138
|
+
return {
|
|
139
|
+
url,
|
|
140
|
+
method: 'PUT',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': opts.mimeType,
|
|
143
|
+
'Content-Length': String(opts.size),
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
async signDownload(opts) {
|
|
148
|
+
const expiresAt = Date.now() + opts.expiresIn * 1000;
|
|
149
|
+
const token = await tokenSigner.sign({ hash: opts.hash, action: 'download', expiresAt }, opts.expiresIn);
|
|
150
|
+
return `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`;
|
|
151
|
+
},
|
|
152
|
+
async exists(hash) {
|
|
153
|
+
const key = getKey(hash);
|
|
154
|
+
const head = await bucket.head(key);
|
|
155
|
+
return head !== null;
|
|
156
|
+
},
|
|
157
|
+
async delete(hash) {
|
|
158
|
+
const key = getKey(hash);
|
|
159
|
+
await bucket.delete(key);
|
|
160
|
+
},
|
|
161
|
+
async getMetadata(hash) {
|
|
162
|
+
const key = getKey(hash);
|
|
163
|
+
const head = await bucket.head(key);
|
|
164
|
+
if (!head)
|
|
165
|
+
return null;
|
|
166
|
+
return {
|
|
167
|
+
size: head.size,
|
|
168
|
+
mimeType: head.httpMetadata?.contentType,
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
async put(hash, data, metadata) {
|
|
172
|
+
const key = getKey(hash);
|
|
173
|
+
const mimeType = resolveMimeType(metadata);
|
|
174
|
+
const checksum = resolveChecksum(hash);
|
|
175
|
+
await bucket.put(key, data, {
|
|
176
|
+
httpMetadata: {
|
|
177
|
+
contentType: mimeType,
|
|
178
|
+
},
|
|
179
|
+
sha256: checksum,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
async putStream(hash, stream, metadata) {
|
|
183
|
+
await putStreamInternal(hash, stream, metadata);
|
|
184
|
+
},
|
|
185
|
+
async get(hash) {
|
|
186
|
+
const stream = await getStreamInternal(hash);
|
|
187
|
+
if (!stream)
|
|
188
|
+
return null;
|
|
189
|
+
const reader = stream.getReader();
|
|
190
|
+
try {
|
|
191
|
+
const chunks = [];
|
|
192
|
+
let total = 0;
|
|
193
|
+
while (true) {
|
|
194
|
+
const { done, value } = await reader.read();
|
|
195
|
+
if (done)
|
|
196
|
+
break;
|
|
197
|
+
if (!value)
|
|
198
|
+
continue;
|
|
199
|
+
chunks.push(value);
|
|
200
|
+
total += value.length;
|
|
201
|
+
}
|
|
202
|
+
const out = new Uint8Array(total);
|
|
203
|
+
let offset = 0;
|
|
204
|
+
for (const chunk of chunks) {
|
|
205
|
+
out.set(chunk, offset);
|
|
206
|
+
offset += chunk.length;
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
reader.releaseLock();
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
async getStream(hash) {
|
|
215
|
+
return getStreamInternal(hash);
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=r2.js.map
|
package/dist/r2.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2.js","sourceRoot":"","sources":["../src/r2.ts"],"names":[],"mappings":"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"}
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-cloudflare - Worker handler (polling only)
|
|
3
|
+
*
|
|
4
|
+
* Creates a stateless Cloudflare Worker that serves sync routes via Hono.
|
|
5
|
+
* No WebSocket support — use the Durable Object adapter for realtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createSyncWorker } from '@syncular/server-cloudflare/worker';
|
|
10
|
+
* import { createD1Db } from '@syncular/dialect-d1';
|
|
11
|
+
* import { createSqliteServerDialect } from '@syncular/server-dialect-sqlite';
|
|
12
|
+
* import { ensureSyncSchema } from '@syncular/server';
|
|
13
|
+
* import { createSyncServer } from '@syncular/server-hono';
|
|
14
|
+
*
|
|
15
|
+
* type Env = { DB: D1Database };
|
|
16
|
+
*
|
|
17
|
+
* export default createSyncWorker<Env>((app, env) => {
|
|
18
|
+
* const db = createD1Db(env.DB);
|
|
19
|
+
* const dialect = createSqliteServerDialect();
|
|
20
|
+
* const { syncRoutes, consoleRoutes } = createSyncServer({
|
|
21
|
+
* db, dialect,
|
|
22
|
+
* handlers: [tasksHandler],
|
|
23
|
+
* authenticate: async (c) => ({ actorId: c.req.header('x-user-id')! }),
|
|
24
|
+
* });
|
|
25
|
+
* app.route('/sync', syncRoutes);
|
|
26
|
+
* if (consoleRoutes) app.route('/console', consoleRoutes);
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { Hono } from 'hono';
|
|
31
|
+
type SyncWorkerSetup<B extends object> = (app: Hono<{
|
|
32
|
+
Bindings: B;
|
|
33
|
+
}>, env: B) => void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Create a Cloudflare Worker export that lazily initializes a Hono app.
|
|
36
|
+
*
|
|
37
|
+
* The `setup` callback is called once per isolate on the first request.
|
|
38
|
+
* It receives a fresh Hono app and the Worker env bindings.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createSyncWorker<Bindings extends object = Record<string, unknown>>(setup: SyncWorkerSetup<Bindings>): ExportedHandler<Bindings>;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM,IAAI,CACvC,GAAG,EAAE,IAAI,CAAC;IAAE,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,EAC1B,GAAG,EAAE,CAAC,KACH,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjD,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,GAAG,eAAe,CAAC,QAAQ,CAAC,CA2B7D"}
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server-cloudflare - Worker handler (polling only)
|
|
3
|
+
*
|
|
4
|
+
* Creates a stateless Cloudflare Worker that serves sync routes via Hono.
|
|
5
|
+
* No WebSocket support — use the Durable Object adapter for realtime.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createSyncWorker } from '@syncular/server-cloudflare/worker';
|
|
10
|
+
* import { createD1Db } from '@syncular/dialect-d1';
|
|
11
|
+
* import { createSqliteServerDialect } from '@syncular/server-dialect-sqlite';
|
|
12
|
+
* import { ensureSyncSchema } from '@syncular/server';
|
|
13
|
+
* import { createSyncServer } from '@syncular/server-hono';
|
|
14
|
+
*
|
|
15
|
+
* type Env = { DB: D1Database };
|
|
16
|
+
*
|
|
17
|
+
* export default createSyncWorker<Env>((app, env) => {
|
|
18
|
+
* const db = createD1Db(env.DB);
|
|
19
|
+
* const dialect = createSqliteServerDialect();
|
|
20
|
+
* const { syncRoutes, consoleRoutes } = createSyncServer({
|
|
21
|
+
* db, dialect,
|
|
22
|
+
* handlers: [tasksHandler],
|
|
23
|
+
* authenticate: async (c) => ({ actorId: c.req.header('x-user-id')! }),
|
|
24
|
+
* });
|
|
25
|
+
* app.route('/sync', syncRoutes);
|
|
26
|
+
* if (consoleRoutes) app.route('/console', consoleRoutes);
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { Hono } from 'hono';
|
|
31
|
+
/**
|
|
32
|
+
* Create a Cloudflare Worker export that lazily initializes a Hono app.
|
|
33
|
+
*
|
|
34
|
+
* The `setup` callback is called once per isolate on the first request.
|
|
35
|
+
* It receives a fresh Hono app and the Worker env bindings.
|
|
36
|
+
*/
|
|
37
|
+
export function createSyncWorker(setup) {
|
|
38
|
+
let app = null;
|
|
39
|
+
let initPromise = null;
|
|
40
|
+
async function getApp(env) {
|
|
41
|
+
if (app)
|
|
42
|
+
return app;
|
|
43
|
+
if (!initPromise) {
|
|
44
|
+
const honoApp = new Hono();
|
|
45
|
+
initPromise = Promise.resolve(setup(honoApp, env)).then(() => {
|
|
46
|
+
app = honoApp;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
await initPromise;
|
|
50
|
+
return app;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async fetch(request, env, ctx) {
|
|
54
|
+
const honoApp = await getApp(env);
|
|
55
|
+
return honoApp.fetch(request, env, ctx);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAO5B;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAE9B,KAAgC,EAA6B;IAE7D,IAAI,GAAG,GAAmB,IAAI,CAAC;IAC/B,IAAI,WAAW,GAAyB,IAAI,CAAC;IAE7C,KAAK,UAAU,MAAM,CAAC,GAAa,EAAoB;QACrD,IAAI,GAAG;YAAE,OAAO,GAAG,CAAC;QACpB,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,IAAI,IAAI,EAAK,CAAC;YAC9B,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC5D,GAAG,GAAG,OAAO,CAAC;YAAA,CACf,CAAC,CAAC;QACL,CAAC;QACD,MAAM,WAAW,CAAC;QAClB,OAAO,GAAI,CAAC;IAAA,CACb;IAED,OAAO;QACL,KAAK,CAAC,KAAK,CACT,OAAgB,EAChB,GAAa,EACb,GAAqB,EACF;YACnB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YAClC,OAAO,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QAAA,CACzC;KACF,CAAC;AAAA,CACH"}
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@syncular/server-cloudflare",
|
|
3
|
+
"version": "0.0.1-100",
|
|
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
|
+
],
|
|
26
|
+
"private": false,
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"type": "module",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"bun": "./src/index.ts",
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./worker": {
|
|
40
|
+
"bun": "./src/worker.ts",
|
|
41
|
+
"import": {
|
|
42
|
+
"types": "./dist/worker.d.ts",
|
|
43
|
+
"default": "./dist/worker.js"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"./durable-object": {
|
|
47
|
+
"bun": "./src/durable-object.ts",
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/durable-object.d.ts",
|
|
50
|
+
"default": "./dist/durable-object.js"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./r2": {
|
|
54
|
+
"bun": "./src/r2.ts",
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/r2.d.ts",
|
|
57
|
+
"default": "./dist/r2.js"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"tsgo": "tsgo --noEmit",
|
|
63
|
+
"build": "tsgo",
|
|
64
|
+
"release": "bunx syncular-publish"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"hono": "^4.0.0"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@cloudflare/workers-types": "*",
|
|
71
|
+
"@syncular/config": "0.0.0"
|
|
72
|
+
},
|
|
73
|
+
"files": [
|
|
74
|
+
"dist",
|
|
75
|
+
"src"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
@@ -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
|
+
});
|