@buley/relay 4.0.0 → 4.2.0
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/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/sync-room.d.ts +17 -0
- package/dist/sync-room.d.ts.map +1 -0
- package/dist/sync-room.js +159 -0
- package/package.json +19 -3
- package/.wrangler/state/v3/do/dash-relay-SyncRoom/5f3be904a1946fd89b96456d4feb9f1e532784e2612d39da90bb9ac1864b516b.sqlite +0 -0
- package/.wrangler/state/v3/r2/dash-backups/blobs/1e21a245f3806f7a14fd8a54bdca8770c80cfd9eac610b042abe42a182df59600000019bee6f5cb7 +0 -1
- package/.wrangler/state/v3/r2/miniflare-R2BucketObject/224f581e474dc581573c5348a37bcc9a8fa368d1b52bf1bac7a634b01eb636ce.sqlite +0 -0
- package/CHANGELOG.md +0 -10
- package/src/index.ts +0 -23
- package/src/sync-room.ts +0 -83
- package/test/index.spec.ts +0 -72
- package/tsconfig.json +0 -12
- package/vitest.config.ts +0 -23
- package/wrangler.toml +0 -22
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SyncRoom } from "./sync-room";
|
|
2
|
+
interface Env {
|
|
3
|
+
SYNC_ROOM: DurableObjectNamespace<SyncRoom>;
|
|
4
|
+
}
|
|
5
|
+
export { SyncRoom };
|
|
6
|
+
declare const _default: {
|
|
7
|
+
fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,UAAU,GAAG;IACX,SAAS,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAC;CAC7C;AAED,OAAO,EAAE,QAAQ,EAAE,CAAC;;mBAGG,OAAO,OAAO,GAAG,OAAO,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;;AADnF,wBAcE"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SyncRoom } from "./sync-room";
|
|
2
|
+
export { SyncRoom };
|
|
3
|
+
export default {
|
|
4
|
+
async fetch(request, env, ctx) {
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
const path = url.pathname.slice(1).split("/"); // ["sync", "roomName"]
|
|
7
|
+
if (path[0] === "sync" && path[1]) {
|
|
8
|
+
const roomName = path[1];
|
|
9
|
+
const id = env.SYNC_ROOM.idFromName(roomName);
|
|
10
|
+
const stub = env.SYNC_ROOM.get(id);
|
|
11
|
+
return stub.fetch(request);
|
|
12
|
+
}
|
|
13
|
+
return new Response("Not Found", { status: 404 });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
interface Env {
|
|
3
|
+
BACKUPS: R2Bucket;
|
|
4
|
+
}
|
|
5
|
+
export declare class SyncRoom extends DurableObject<Env> {
|
|
6
|
+
sql: SqlStorage;
|
|
7
|
+
constructor(ctx: DurableObjectState, env: Env);
|
|
8
|
+
fetch(request: Request): Promise<Response>;
|
|
9
|
+
handleWebTransportSession(session: any): Promise<void>;
|
|
10
|
+
handleStream(stream: any): Promise<void>;
|
|
11
|
+
webSocketMessage(ws: WebSocket, message: ArrayBuffer | string): Promise<void>;
|
|
12
|
+
private broadcast;
|
|
13
|
+
webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void>;
|
|
14
|
+
alarm(): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=sync-room.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-room.d.ts","sourceRoot":"","sources":["../src/sync-room.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,UAAU,GAAG;IACX,OAAO,EAAE,QAAQ,CAAC;CACnB;AAED,qBAAa,QAAS,SAAQ,aAAa,CAAC,GAAG,CAAC;IAC9C,GAAG,EAAE,UAAU,CAAC;gBAEJ,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,GAAG;IAcvC,KAAK,CAAC,OAAO,EAAE,OAAO;IAkDtB,yBAAyB,CAAC,OAAO,EAAE,GAAG;IAgBtC,YAAY,CAAC,MAAM,EAAE,GAAG;IAuCxB,gBAAgB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,WAAW,GAAG,MAAM;IAqBnE,OAAO,CAAC,SAAS;IAaX,cAAc,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO;IAI7E,KAAK;CAgBZ"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { DurableObject } from "cloudflare:workers";
|
|
2
|
+
import * as ucans from 'ucans';
|
|
3
|
+
export class SyncRoom extends DurableObject {
|
|
4
|
+
sql;
|
|
5
|
+
constructor(ctx, env) {
|
|
6
|
+
super(ctx, env);
|
|
7
|
+
this.sql = ctx.storage.sql;
|
|
8
|
+
// Initialize Schema
|
|
9
|
+
this.sql.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
data BLOB,
|
|
13
|
+
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
14
|
+
);
|
|
15
|
+
`);
|
|
16
|
+
}
|
|
17
|
+
async fetch(request) {
|
|
18
|
+
// 0. UCAN Verification
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const token = url.searchParams.get("token");
|
|
21
|
+
if (!token) {
|
|
22
|
+
return new Response("Unauthorized: Missing UCAN", { status: 401 });
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const result = await ucans.verify(token, {
|
|
26
|
+
// In a real system, you would verify against a root DID or specific issuer.
|
|
27
|
+
// For this P2P/Star architecture, we check if the signature is valid
|
|
28
|
+
// and if it grants access to THIS room.
|
|
29
|
+
audience: "did:web:relay.buley.dev",
|
|
30
|
+
requiredCapabilities: [
|
|
31
|
+
{
|
|
32
|
+
capability: {
|
|
33
|
+
with: { scheme: "room", hierPart: `//${url.pathname.split('/').pop()}` },
|
|
34
|
+
can: { namespace: "room", segments: ["write"] }
|
|
35
|
+
},
|
|
36
|
+
rootIssuer: null // Allow self-issued for now (P2P style)
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
if (!result.ok) {
|
|
41
|
+
return new Response(`Unauthorized: Invalid UCAN - ${result.error}`, { status: 403 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
return new Response("Unauthorized: Malformed Token", { status: 403 });
|
|
46
|
+
}
|
|
47
|
+
// 1. WebSocket Upgrade
|
|
48
|
+
if (request.headers.get("Upgrade") === "websocket") {
|
|
49
|
+
const pair = new WebSocketPair();
|
|
50
|
+
const [client, server] = Object.values(pair);
|
|
51
|
+
this.ctx.acceptWebSocket(server);
|
|
52
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
53
|
+
}
|
|
54
|
+
// 2. WebTransport Session
|
|
55
|
+
// Note: Use 'webtransport' npm package or specific Cloudflare types if available.
|
|
56
|
+
// For this snippet, we assume the environment passes a session or we accept it.
|
|
57
|
+
// This logic demonstrates the BYOB loop requested.
|
|
58
|
+
return new Response("Expected WebSocket Upgrade", { status: 426 });
|
|
59
|
+
}
|
|
60
|
+
async handleWebTransportSession(session) {
|
|
61
|
+
// 1. Accept incoming streams
|
|
62
|
+
const reader = session.incomingBidirectionalStreams.getReader();
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const { value: stream, done } = await reader.read();
|
|
66
|
+
if (done)
|
|
67
|
+
break;
|
|
68
|
+
// Handle individual stream with BYOB
|
|
69
|
+
this.handleStream(stream);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
// session closed
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async handleStream(stream) {
|
|
77
|
+
// 2. Zero-Copy Reader (BYOB)
|
|
78
|
+
let reader;
|
|
79
|
+
try {
|
|
80
|
+
reader = stream.readable.getReader({ mode: 'byob' });
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
reader = stream.readable.getReader();
|
|
84
|
+
}
|
|
85
|
+
// Buffer Reuse (Zero Allocation Loop)
|
|
86
|
+
let buffer = new Uint8Array(65536);
|
|
87
|
+
try {
|
|
88
|
+
while (true) {
|
|
89
|
+
let result;
|
|
90
|
+
if (reader.readAtLeast) {
|
|
91
|
+
// Pass view to kernel/runtime
|
|
92
|
+
result = await reader.read(new Uint8Array(buffer.buffer, 0, buffer.byteLength));
|
|
93
|
+
if (result.value)
|
|
94
|
+
buffer = result.value; // Valid buffer returned
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
result = await reader.read();
|
|
98
|
+
}
|
|
99
|
+
if (result.done)
|
|
100
|
+
break;
|
|
101
|
+
if (result.value) {
|
|
102
|
+
// 3. Zero-Copy Insert to Native SQLite
|
|
103
|
+
// The buffer 'result.value' contains the data directly from the network packet
|
|
104
|
+
this.sql.exec("INSERT INTO messages (data) VALUES (?)", result.value);
|
|
105
|
+
// 4. Broadcast (Zero-Copy to outbound buffers?)
|
|
106
|
+
// Cloudflare's WebSocket.send() accepts ArrayBuffer, so no copy needed here either ideally.
|
|
107
|
+
this.broadcast(result.value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// stream error
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async webSocketMessage(ws, message) {
|
|
116
|
+
// 1. Store Delta (Zero Latency)
|
|
117
|
+
// We assume message is a binary CRDT update
|
|
118
|
+
const data = typeof message === 'string' ? new TextEncoder().encode(message) : message;
|
|
119
|
+
this.sql.exec("INSERT INTO messages (data) VALUES (?)", data);
|
|
120
|
+
this.sql.exec("INSERT INTO messages (data) VALUES (?)", data);
|
|
121
|
+
// 2. Broadcast to others
|
|
122
|
+
this.broadcast(message, ws);
|
|
123
|
+
// 3. Set Alarm for Write-back (Debounced)
|
|
124
|
+
// Hibernation Note: The alarm will wake the DO up even if all clients are hibernating.
|
|
125
|
+
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
126
|
+
if (currentAlarm === null) {
|
|
127
|
+
// Set alarm for 10 seconds from now
|
|
128
|
+
await this.ctx.storage.setAlarm(Date.now() + 10000);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
broadcast(message, exclude) {
|
|
132
|
+
// Determine the message to send once
|
|
133
|
+
for (const client of this.ctx.getWebSockets()) {
|
|
134
|
+
if (client !== exclude) {
|
|
135
|
+
try {
|
|
136
|
+
client.send(message);
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
// fast-fail on dead sockets
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async webSocketClose(ws, code, reason, wasClean) {
|
|
145
|
+
// Clean up if needed
|
|
146
|
+
}
|
|
147
|
+
async alarm() {
|
|
148
|
+
// 1. Snapshot SQLite to R2
|
|
149
|
+
// In a real app, you might maintain a separate logical "Document" file
|
|
150
|
+
// Here we just backup the raw messages for safety.
|
|
151
|
+
const messages = this.sql.exec("SELECT * FROM messages").toArray();
|
|
152
|
+
const blob = JSON.stringify(messages); // Simple serialization for backup
|
|
153
|
+
const key = `backups/${this.ctx.id.toString()}/${Date.now()}.json`;
|
|
154
|
+
await this.env.BACKUPS.put(key, blob);
|
|
155
|
+
// Optional: Truncate local history if relying on full-state sync later
|
|
156
|
+
// this.sql.exec("DELETE FROM messages");
|
|
157
|
+
console.log(`Backed up ${messages.length} messages to ${key}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buley/relay",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
5
19
|
"scripts": {
|
|
6
20
|
"dev": "wrangler dev",
|
|
7
21
|
"deploy": "wrangler deploy",
|
|
8
|
-
"build": "wrangler deploy --dry-run --outdir dist",
|
|
22
|
+
"build": "tsc -p tsconfig.build.json && wrangler deploy --dry-run --outdir dist",
|
|
23
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
9
24
|
"test": "NODE_OPTIONS=--experimental-vm-modules vitest"
|
|
10
25
|
},
|
|
11
26
|
"dependencies": {
|
|
12
|
-
"hono": "^3.12.0"
|
|
27
|
+
"hono": "^3.12.0",
|
|
28
|
+
"ucans": "^0.10.0"
|
|
13
29
|
},
|
|
14
30
|
"devDependencies": {
|
|
15
31
|
"@cloudflare/workers-types": "^4.20240117.0",
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
[{"id":1,"data":{},"created_at":1769231824},{"id":2,"data":{},"created_at":1769231825}]
|
|
Binary file
|
package/CHANGELOG.md
DELETED
package/src/index.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { SyncRoom } from "./sync-room";
|
|
2
|
-
|
|
3
|
-
interface Env {
|
|
4
|
-
SYNC_ROOM: DurableObjectNamespace<SyncRoom>;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export { SyncRoom };
|
|
8
|
-
|
|
9
|
-
export default {
|
|
10
|
-
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
11
|
-
const url = new URL(request.url);
|
|
12
|
-
const path = url.pathname.slice(1).split("/"); // ["sync", "roomName"]
|
|
13
|
-
|
|
14
|
-
if (path[0] === "sync" && path[1]) {
|
|
15
|
-
const roomName = path[1];
|
|
16
|
-
const id = env.SYNC_ROOM.idFromName(roomName);
|
|
17
|
-
const stub = env.SYNC_ROOM.get(id);
|
|
18
|
-
return stub.fetch(request);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return new Response("Not Found", { status: 404 });
|
|
22
|
-
},
|
|
23
|
-
};
|
package/src/sync-room.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { DurableObject } from "cloudflare:workers";
|
|
2
|
-
|
|
3
|
-
interface Env {
|
|
4
|
-
BACKUPS: R2Bucket;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class SyncRoom extends DurableObject<Env> {
|
|
8
|
-
sql: SqlStorage;
|
|
9
|
-
|
|
10
|
-
constructor(ctx: DurableObjectState, env: Env) {
|
|
11
|
-
super(ctx, env);
|
|
12
|
-
this.sql = ctx.storage.sql;
|
|
13
|
-
|
|
14
|
-
// Initialize Schema
|
|
15
|
-
this.sql.exec(`
|
|
16
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
17
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
-
data BLOB,
|
|
19
|
-
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
|
20
|
-
);
|
|
21
|
-
`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async fetch(request: Request) {
|
|
25
|
-
// 1. WebSocket Upgrade
|
|
26
|
-
if (request.headers.get("Upgrade") === "websocket") {
|
|
27
|
-
const pair = new WebSocketPair();
|
|
28
|
-
const [client, server] = Object.values(pair);
|
|
29
|
-
|
|
30
|
-
this.ctx.acceptWebSocket(server);
|
|
31
|
-
return new Response(null, { status: 101, webSocket: client });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 2. WebTransport Session (Example - requires parsing Connect)
|
|
35
|
-
// For simplicity in this v1, we focus on the WebSocket 'Hibernate' path
|
|
36
|
-
// accessible via fetch post for high-throughput if needed, or upgrade.
|
|
37
|
-
|
|
38
|
-
return new Response("Expected WebSocket Upgrade", { status: 426 });
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
|
|
42
|
-
// 1. Store Delta (Zero Latency)
|
|
43
|
-
// We assume message is a binary CRDT update
|
|
44
|
-
const data = typeof message === 'string' ? new TextEncoder().encode(message) : message;
|
|
45
|
-
|
|
46
|
-
this.sql.exec("INSERT INTO messages (data) VALUES (?)", data as any);
|
|
47
|
-
|
|
48
|
-
// 2. Broadcast to others
|
|
49
|
-
for (const other of this.ctx.getWebSockets()) {
|
|
50
|
-
if (other !== ws) {
|
|
51
|
-
other.send(message);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 3. Set Alarm for Write-back (Debounced)
|
|
56
|
-
const currentAlarm = await this.ctx.storage.getAlarm();
|
|
57
|
-
if (currentAlarm === null) {
|
|
58
|
-
// Set alarm for 10 seconds from now
|
|
59
|
-
await this.ctx.storage.setAlarm(Date.now() + 10000);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) {
|
|
64
|
-
// Clean up if needed
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async alarm() {
|
|
68
|
-
// 1. Snapshot SQLite to R2
|
|
69
|
-
// In a real app, you might maintain a separate logical "Document" file
|
|
70
|
-
// Here we just backup the raw messages for safety.
|
|
71
|
-
|
|
72
|
-
const messages = this.sql.exec("SELECT * FROM messages").toArray();
|
|
73
|
-
const blob = JSON.stringify(messages); // Simple serialization for backup
|
|
74
|
-
|
|
75
|
-
const key = `backups/${this.ctx.id.toString()}/${Date.now()}.json`;
|
|
76
|
-
await this.env.BACKUPS.put(key, blob);
|
|
77
|
-
|
|
78
|
-
// Optional: Truncate local history if relying on full-state sync later
|
|
79
|
-
// this.sql.exec("DELETE FROM messages");
|
|
80
|
-
|
|
81
|
-
console.log(`Backed up ${messages.length} messages to ${key}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
package/test/index.spec.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// @vitest-environment miniflare
|
|
2
|
-
import { describe, it, expect, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
// In vitest-environment-miniflare, bindings are often exposed as globals directly.
|
|
5
|
-
declare global {
|
|
6
|
-
var SYNC_ROOM: DurableObjectNamespace;
|
|
7
|
-
var BACKUPS: R2Bucket;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe("SyncRoom", () => {
|
|
11
|
-
it("should upgrade to websocket", async () => {
|
|
12
|
-
const id = SYNC_ROOM.idFromName("test-room");
|
|
13
|
-
const stub = SYNC_ROOM.get(id);
|
|
14
|
-
|
|
15
|
-
// Simulate WebSocket Upgrade Request
|
|
16
|
-
const resp = await stub.fetch("http://localhost/sync/test-room", {
|
|
17
|
-
headers: { Upgrade: "websocket" },
|
|
18
|
-
});
|
|
19
|
-
expect(resp.status).toBe(101);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("should store messages and broadcast", async () => {
|
|
23
|
-
const id = SYNC_ROOM.idFromName("room-2");
|
|
24
|
-
const stub = SYNC_ROOM.get(id);
|
|
25
|
-
|
|
26
|
-
const { webSocket: ws1, response: resp1 } = await stub.fetch("http://localhost/sync/room-2", {
|
|
27
|
-
headers: { Upgrade: "websocket" },
|
|
28
|
-
});
|
|
29
|
-
expect(resp1.status).toBe(101);
|
|
30
|
-
|
|
31
|
-
const { webSocket: ws2, response: resp2 } = await stub.fetch("http://localhost/sync/room-2", {
|
|
32
|
-
headers: { Upgrade: "websocket" },
|
|
33
|
-
});
|
|
34
|
-
expect(resp2.status).toBe(101);
|
|
35
|
-
|
|
36
|
-
if (!ws1 || !ws2) throw new Error("Failed to get sockets");
|
|
37
|
-
|
|
38
|
-
ws1.accept();
|
|
39
|
-
ws2.accept();
|
|
40
|
-
|
|
41
|
-
const received: any[] = [];
|
|
42
|
-
ws2.addEventListener("message", (event) => received.push(event.data));
|
|
43
|
-
|
|
44
|
-
// Send Message
|
|
45
|
-
const data = new Uint8Array([1, 2, 3]);
|
|
46
|
-
ws1.send(data);
|
|
47
|
-
|
|
48
|
-
// Wait for broadcast
|
|
49
|
-
await new Promise(r => setTimeout(r, 200));
|
|
50
|
-
|
|
51
|
-
expect(received.length).toBe(1);
|
|
52
|
-
// Note: Miniflare/Workerd might deliver as string or blob depending on config, checking length for now
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("should trigger backup alarm", async () => {
|
|
56
|
-
// Alarm testing is tricky in unit tests without advancing time.
|
|
57
|
-
// We can check if alarm is set.
|
|
58
|
-
const id = SYNC_ROOM.idFromName("room-alarm");
|
|
59
|
-
const stub = SYNC_ROOM.get(id);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const { webSocket: ws1 } = await stub.fetch("http://localhost", { headers: { Upgrade: "websocket" } });
|
|
63
|
-
ws1?.accept();
|
|
64
|
-
ws1?.send("test data");
|
|
65
|
-
|
|
66
|
-
// Wait for write
|
|
67
|
-
await new Promise(r => setTimeout(r, 100));
|
|
68
|
-
|
|
69
|
-
// Unfortunately we can't easily inspect DO internal state from outside unless we add a debug endpoint.
|
|
70
|
-
// For 100% coverage, we trust the integration or add a method to SyncRoom exposed for testing.
|
|
71
|
-
});
|
|
72
|
-
});
|
package/tsconfig.json
DELETED
package/vitest.config.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "vitest/config";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
environment: "miniflare",
|
|
6
|
-
environmentOptions: {
|
|
7
|
-
modules: true,
|
|
8
|
-
scriptPath: "dist/index.js",
|
|
9
|
-
compatibilityDate: "2024-04-01",
|
|
10
|
-
shouldLog: true,
|
|
11
|
-
bindings: { BACKUPS: { type: "R2Bucket" } },
|
|
12
|
-
durableObjects: {
|
|
13
|
-
SYNC_ROOM: "SyncRoom",
|
|
14
|
-
},
|
|
15
|
-
migrations: [
|
|
16
|
-
{
|
|
17
|
-
tag: "v1",
|
|
18
|
-
new_sqlite_classes: ["SyncRoom"],
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
package/wrangler.toml
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
name = "dash-relay"
|
|
2
|
-
main = "src/index.ts"
|
|
3
|
-
compatibility_date = "2024-01-17"
|
|
4
|
-
|
|
5
|
-
# Durable Object Bindings
|
|
6
|
-
[[durable_objects.bindings]]
|
|
7
|
-
name = "SYNC_ROOM"
|
|
8
|
-
class_name = "SyncRoom"
|
|
9
|
-
|
|
10
|
-
# Durable Object Migrations (Required for Native SQLite)
|
|
11
|
-
[[migrations]]
|
|
12
|
-
tag = "v1"
|
|
13
|
-
new_classes = []
|
|
14
|
-
new_sqlite_classes = ["SyncRoom"]
|
|
15
|
-
|
|
16
|
-
# R2 Bucket for Backups (Alarms)
|
|
17
|
-
[[r2_buckets]]
|
|
18
|
-
binding = "BACKUPS"
|
|
19
|
-
bucket_name = "dash-backups"
|
|
20
|
-
|
|
21
|
-
[triggers]
|
|
22
|
-
crons = []
|