silosdk 0.0.0 → 0.0.1
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 +3 -1
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/cli/d1.cjs +93 -0
- package/dist/cli/d1.mjs +92 -0
- package/dist/cli/index.cjs +93 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +94 -0
- package/dist/cli/init.cjs +134 -0
- package/dist/cli/init.mjs +133 -0
- package/dist/cli/kv.cjs +63 -0
- package/dist/cli/kv.mjs +60 -0
- package/dist/cli/r2.cjs +83 -0
- package/dist/cli/r2.mjs +82 -0
- package/dist/cli/wrangler.cjs +93 -0
- package/dist/cli/wrangler.mjs +89 -0
- package/dist/local/adapters/cloudflare.cjs +200 -0
- package/dist/local/adapters/cloudflare.d.cts +50 -0
- package/dist/local/adapters/cloudflare.d.mts +50 -0
- package/dist/local/adapters/cloudflare.mjs +200 -0
- package/dist/local/auth-context.cjs +14 -0
- package/dist/local/auth-context.d.cts +7 -0
- package/dist/local/auth-context.d.mts +7 -0
- package/dist/local/auth-context.mjs +12 -0
- package/dist/local/auth.cjs +109 -0
- package/dist/local/auth.d.cts +26 -0
- package/dist/local/auth.d.mts +26 -0
- package/dist/local/auth.mjs +99 -0
- package/dist/local/commit.cjs +350 -0
- package/dist/local/commit.d.cts +59 -0
- package/dist/local/commit.d.mts +59 -0
- package/dist/local/commit.mjs +349 -0
- package/dist/local/config.cjs +17 -0
- package/dist/local/config.mjs +15 -0
- package/dist/local/index.cjs +16 -0
- package/dist/local/index.d.cts +10 -0
- package/dist/local/index.d.mts +10 -0
- package/dist/local/index.mjs +9 -0
- package/dist/local/provider.cjs +204 -0
- package/dist/local/provider.d.cts +25 -0
- package/dist/local/provider.d.mts +25 -0
- package/dist/local/provider.mjs +203 -0
- package/dist/local/query-store.cjs +276 -0
- package/dist/local/query-store.mjs +274 -0
- package/dist/local/storage.cjs +71 -0
- package/dist/local/storage.d.cts +7 -0
- package/dist/local/storage.d.mts +7 -0
- package/dist/local/storage.mjs +68 -0
- package/dist/local/sync.cjs +124 -0
- package/dist/local/sync.d.cts +36 -0
- package/dist/local/sync.d.mts +36 -0
- package/dist/local/sync.mjs +122 -0
- package/dist/local/view.cjs +257 -0
- package/dist/local/view.d.cts +24 -0
- package/dist/local/view.d.mts +24 -0
- package/dist/local/view.mjs +254 -0
- package/dist/package.cjs +11 -0
- package/dist/package.mjs +5 -0
- package/dist/schema/index.cjs +276 -0
- package/dist/schema/index.d.cts +207 -0
- package/dist/schema/index.d.mts +207 -0
- package/dist/schema/index.mjs +265 -0
- package/dist/server/auth.cjs +132 -0
- package/dist/server/auth.d.cts +49 -0
- package/dist/server/auth.d.mts +49 -0
- package/dist/server/auth.mjs +122 -0
- package/dist/server/d1.cjs +120 -0
- package/dist/server/d1.mjs +116 -0
- package/dist/server/do.cjs +132 -0
- package/dist/server/do.d.cts +21 -0
- package/dist/server/do.d.mts +21 -0
- package/dist/server/do.mjs +131 -0
- package/dist/server/index.cjs +355 -0
- package/dist/server/index.d.cts +65 -0
- package/dist/server/index.d.mts +65 -0
- package/dist/server/index.mjs +348 -0
- package/dist/server/protect.cjs +34 -0
- package/dist/server/protect.d.cts +32 -0
- package/dist/server/protect.d.mts +32 -0
- package/dist/server/protect.mjs +33 -0
- package/dist/server/r2.cjs +58 -0
- package/dist/server/r2.d.cts +4 -0
- package/dist/server/r2.d.mts +4 -0
- package/dist/server/r2.mjs +53 -0
- package/package.json +55 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/server/d1.ts
|
|
3
|
+
function jex(field) {
|
|
4
|
+
return `json_extract(data, '$.${field}')`;
|
|
5
|
+
}
|
|
6
|
+
function compileWhere(where) {
|
|
7
|
+
const params = [];
|
|
8
|
+
const compileObject = (obj) => {
|
|
9
|
+
const parts = [];
|
|
10
|
+
for (const field of Object.keys(obj)) {
|
|
11
|
+
const value = obj[field];
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
14
|
+
parts.push(`${jex(field)} IN (${placeholders})`);
|
|
15
|
+
params.push(...value);
|
|
16
|
+
} else if (value && typeof value === "object") for (const op of Object.keys(value)) {
|
|
17
|
+
parts.push(`${jex(field)} ${op} ?`);
|
|
18
|
+
params.push(value[op]);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
parts.push(`${jex(field)} = ?`);
|
|
22
|
+
params.push(value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return parts.join(" AND ");
|
|
26
|
+
};
|
|
27
|
+
if (Array.isArray(where)) return {
|
|
28
|
+
clause: where.map((o) => `(${compileObject(o)})`).join(" OR "),
|
|
29
|
+
params
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
clause: compileObject(where),
|
|
33
|
+
params
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async function d1GetRow(db, viewName, options) {
|
|
37
|
+
const row = await db.prepare(`SELECT id, view, data, createdAt, updatedAt, version
|
|
38
|
+
FROM views WHERE view = ? AND id = ?`).bind(viewName, options.id).first();
|
|
39
|
+
if (!row) return null;
|
|
40
|
+
return {
|
|
41
|
+
...row,
|
|
42
|
+
data: JSON.parse(row.data)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function d1GetRows(db, viewName, options) {
|
|
46
|
+
const parentView = options.parentView;
|
|
47
|
+
const parentId = options.parentId;
|
|
48
|
+
const childView = options.childView;
|
|
49
|
+
const childId = options.childId;
|
|
50
|
+
let statement = `SELECT id, view, data, createdAt, updatedAt, version FROM views WHERE view = ?`;
|
|
51
|
+
const params = [viewName];
|
|
52
|
+
if (parentView && parentId) {
|
|
53
|
+
statement += ` AND id IN (SELECT cid FROM relations WHERE child = ? AND parent = ? AND pid = ?)`;
|
|
54
|
+
params.push(viewName, parentView, parentId);
|
|
55
|
+
}
|
|
56
|
+
if (childView && childId) {
|
|
57
|
+
statement += ` AND id IN (SELECT pid FROM relations WHERE parent = ? AND child = ? AND cid = ?)`;
|
|
58
|
+
params.push(viewName, childView, childId);
|
|
59
|
+
}
|
|
60
|
+
if (options.where) {
|
|
61
|
+
const { clause, params: wp } = compileWhere(options.where);
|
|
62
|
+
statement += ` AND (${clause})`;
|
|
63
|
+
params.push(...wp);
|
|
64
|
+
}
|
|
65
|
+
if (options.order) {
|
|
66
|
+
const orderClauses = [];
|
|
67
|
+
for (const [field, direction] of Object.entries(options.order)) orderClauses.push(`${jex(field)} ${String(direction).toUpperCase()}`);
|
|
68
|
+
if (orderClauses.length > 0) statement += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
69
|
+
}
|
|
70
|
+
if (options.take !== void 0) {
|
|
71
|
+
statement += ` LIMIT ?`;
|
|
72
|
+
params.push(options.take);
|
|
73
|
+
}
|
|
74
|
+
return ((await db.prepare(statement).bind(...params).all()).results ?? []).map((row) => ({
|
|
75
|
+
...row,
|
|
76
|
+
data: JSON.parse(row.data)
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
async function d1ApplyOps(db, ops) {
|
|
80
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
81
|
+
const stmts = [];
|
|
82
|
+
for (const op of ops) if (op.kind === "add") stmts.push(db.prepare(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
84
|
+
ON CONFLICT(id) DO NOTHING`).bind(op.id, op.view.name, JSON.stringify(op.value), timestamp, timestamp, 1));
|
|
85
|
+
else if (op.kind === "update") stmts.push(db.prepare(`UPDATE views
|
|
86
|
+
SET data = json_patch(data, ?), updatedAt = ?, version = version + 1
|
|
87
|
+
WHERE id = ? AND view = ?`).bind(JSON.stringify(op.value), timestamp, op.id, op.view.name));
|
|
88
|
+
else if (op.kind === "remove") {
|
|
89
|
+
stmts.push(db.prepare(`DELETE FROM views WHERE id = ? AND view = ?`).bind(op.id, op.view.name));
|
|
90
|
+
stmts.push(db.prepare(`DELETE FROM relations
|
|
91
|
+
WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`).bind(op.view.name, op.id, op.view.name, op.id));
|
|
92
|
+
} else if (op.kind === "link") stmts.push(db.prepare(`INSERT INTO relations (parent, pid, child, cid)
|
|
93
|
+
VALUES (?, ?, ?, ?)
|
|
94
|
+
ON CONFLICT DO NOTHING`).bind(op.parent.view.name, op.parent.id, op.child.view.name, op.child.id));
|
|
95
|
+
else if (op.kind === "unlink") stmts.push(db.prepare(`DELETE FROM relations
|
|
96
|
+
WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`).bind(op.parent.view.name, op.parent.id, op.child.view.name, op.child.id));
|
|
97
|
+
if (stmts.length > 0) await db.batch(stmts);
|
|
98
|
+
}
|
|
99
|
+
async function d1EnsureSchema(db) {
|
|
100
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS views (
|
|
101
|
+
id TEXT PRIMARY KEY,
|
|
102
|
+
view TEXT NOT NULL,
|
|
103
|
+
data BLOB NOT NULL,
|
|
104
|
+
createdAt TEXT NOT NULL,
|
|
105
|
+
updatedAt TEXT NOT NULL,
|
|
106
|
+
version INTEGER DEFAULT 0
|
|
107
|
+
)`).run();
|
|
108
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS relations (
|
|
109
|
+
parent TEXT NOT NULL,
|
|
110
|
+
pid TEXT NOT NULL,
|
|
111
|
+
child TEXT NOT NULL,
|
|
112
|
+
cid TEXT NOT NULL
|
|
113
|
+
)`).run();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
exports.d1ApplyOps = d1ApplyOps;
|
|
118
|
+
exports.d1EnsureSchema = d1EnsureSchema;
|
|
119
|
+
exports.d1GetRow = d1GetRow;
|
|
120
|
+
exports.d1GetRows = d1GetRows;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/server/d1.ts
|
|
2
|
+
function jex(field) {
|
|
3
|
+
return `json_extract(data, '$.${field}')`;
|
|
4
|
+
}
|
|
5
|
+
function compileWhere(where) {
|
|
6
|
+
const params = [];
|
|
7
|
+
const compileObject = (obj) => {
|
|
8
|
+
const parts = [];
|
|
9
|
+
for (const field of Object.keys(obj)) {
|
|
10
|
+
const value = obj[field];
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
const placeholders = value.map(() => "?").join(", ");
|
|
13
|
+
parts.push(`${jex(field)} IN (${placeholders})`);
|
|
14
|
+
params.push(...value);
|
|
15
|
+
} else if (value && typeof value === "object") for (const op of Object.keys(value)) {
|
|
16
|
+
parts.push(`${jex(field)} ${op} ?`);
|
|
17
|
+
params.push(value[op]);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
parts.push(`${jex(field)} = ?`);
|
|
21
|
+
params.push(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return parts.join(" AND ");
|
|
25
|
+
};
|
|
26
|
+
if (Array.isArray(where)) return {
|
|
27
|
+
clause: where.map((o) => `(${compileObject(o)})`).join(" OR "),
|
|
28
|
+
params
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
clause: compileObject(where),
|
|
32
|
+
params
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function d1GetRow(db, viewName, options) {
|
|
36
|
+
const row = await db.prepare(`SELECT id, view, data, createdAt, updatedAt, version
|
|
37
|
+
FROM views WHERE view = ? AND id = ?`).bind(viewName, options.id).first();
|
|
38
|
+
if (!row) return null;
|
|
39
|
+
return {
|
|
40
|
+
...row,
|
|
41
|
+
data: JSON.parse(row.data)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function d1GetRows(db, viewName, options) {
|
|
45
|
+
const parentView = options.parentView;
|
|
46
|
+
const parentId = options.parentId;
|
|
47
|
+
const childView = options.childView;
|
|
48
|
+
const childId = options.childId;
|
|
49
|
+
let statement = `SELECT id, view, data, createdAt, updatedAt, version FROM views WHERE view = ?`;
|
|
50
|
+
const params = [viewName];
|
|
51
|
+
if (parentView && parentId) {
|
|
52
|
+
statement += ` AND id IN (SELECT cid FROM relations WHERE child = ? AND parent = ? AND pid = ?)`;
|
|
53
|
+
params.push(viewName, parentView, parentId);
|
|
54
|
+
}
|
|
55
|
+
if (childView && childId) {
|
|
56
|
+
statement += ` AND id IN (SELECT pid FROM relations WHERE parent = ? AND child = ? AND cid = ?)`;
|
|
57
|
+
params.push(viewName, childView, childId);
|
|
58
|
+
}
|
|
59
|
+
if (options.where) {
|
|
60
|
+
const { clause, params: wp } = compileWhere(options.where);
|
|
61
|
+
statement += ` AND (${clause})`;
|
|
62
|
+
params.push(...wp);
|
|
63
|
+
}
|
|
64
|
+
if (options.order) {
|
|
65
|
+
const orderClauses = [];
|
|
66
|
+
for (const [field, direction] of Object.entries(options.order)) orderClauses.push(`${jex(field)} ${String(direction).toUpperCase()}`);
|
|
67
|
+
if (orderClauses.length > 0) statement += ` ORDER BY ${orderClauses.join(", ")}`;
|
|
68
|
+
}
|
|
69
|
+
if (options.take !== void 0) {
|
|
70
|
+
statement += ` LIMIT ?`;
|
|
71
|
+
params.push(options.take);
|
|
72
|
+
}
|
|
73
|
+
return ((await db.prepare(statement).bind(...params).all()).results ?? []).map((row) => ({
|
|
74
|
+
...row,
|
|
75
|
+
data: JSON.parse(row.data)
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
async function d1ApplyOps(db, ops) {
|
|
79
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
80
|
+
const stmts = [];
|
|
81
|
+
for (const op of ops) if (op.kind === "add") stmts.push(db.prepare(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
|
|
82
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
83
|
+
ON CONFLICT(id) DO NOTHING`).bind(op.id, op.view.name, JSON.stringify(op.value), timestamp, timestamp, 1));
|
|
84
|
+
else if (op.kind === "update") stmts.push(db.prepare(`UPDATE views
|
|
85
|
+
SET data = json_patch(data, ?), updatedAt = ?, version = version + 1
|
|
86
|
+
WHERE id = ? AND view = ?`).bind(JSON.stringify(op.value), timestamp, op.id, op.view.name));
|
|
87
|
+
else if (op.kind === "remove") {
|
|
88
|
+
stmts.push(db.prepare(`DELETE FROM views WHERE id = ? AND view = ?`).bind(op.id, op.view.name));
|
|
89
|
+
stmts.push(db.prepare(`DELETE FROM relations
|
|
90
|
+
WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`).bind(op.view.name, op.id, op.view.name, op.id));
|
|
91
|
+
} else if (op.kind === "link") stmts.push(db.prepare(`INSERT INTO relations (parent, pid, child, cid)
|
|
92
|
+
VALUES (?, ?, ?, ?)
|
|
93
|
+
ON CONFLICT DO NOTHING`).bind(op.parent.view.name, op.parent.id, op.child.view.name, op.child.id));
|
|
94
|
+
else if (op.kind === "unlink") stmts.push(db.prepare(`DELETE FROM relations
|
|
95
|
+
WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`).bind(op.parent.view.name, op.parent.id, op.child.view.name, op.child.id));
|
|
96
|
+
if (stmts.length > 0) await db.batch(stmts);
|
|
97
|
+
}
|
|
98
|
+
async function d1EnsureSchema(db) {
|
|
99
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS views (
|
|
100
|
+
id TEXT PRIMARY KEY,
|
|
101
|
+
view TEXT NOT NULL,
|
|
102
|
+
data BLOB NOT NULL,
|
|
103
|
+
createdAt TEXT NOT NULL,
|
|
104
|
+
updatedAt TEXT NOT NULL,
|
|
105
|
+
version INTEGER DEFAULT 0
|
|
106
|
+
)`).run();
|
|
107
|
+
await db.prepare(`CREATE TABLE IF NOT EXISTS relations (
|
|
108
|
+
parent TEXT NOT NULL,
|
|
109
|
+
pid TEXT NOT NULL,
|
|
110
|
+
child TEXT NOT NULL,
|
|
111
|
+
cid TEXT NOT NULL
|
|
112
|
+
)`).run();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { d1ApplyOps, d1EnsureSchema, d1GetRow, d1GetRows };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/server/do.ts
|
|
3
|
+
var SyncObject = class {
|
|
4
|
+
constructor(state, env) {
|
|
5
|
+
this.state = state;
|
|
6
|
+
this.env = env;
|
|
7
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
8
|
+
this.sql = state.storage.sql;
|
|
9
|
+
this.sql.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS views (
|
|
11
|
+
id TEXT PRIMARY KEY,
|
|
12
|
+
view TEXT NOT NULL,
|
|
13
|
+
data BLOB NOT NULL,
|
|
14
|
+
createdAt TEXT NOT NULL,
|
|
15
|
+
updatedAt TEXT NOT NULL,
|
|
16
|
+
version INTEGER DEFAULT 0
|
|
17
|
+
);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
19
|
+
parent TEXT NOT NULL,
|
|
20
|
+
pid TEXT NOT NULL,
|
|
21
|
+
child TEXT NOT NULL,
|
|
22
|
+
cid TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
async fetch(request) {
|
|
27
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
28
|
+
if (!upgradeHeader || upgradeHeader !== "websocket") return new Response("Expected WebSocket", { status: 426 });
|
|
29
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
30
|
+
this.state.acceptWebSocket(server);
|
|
31
|
+
return new Response(null, {
|
|
32
|
+
status: 101,
|
|
33
|
+
webSocket: client
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async webSocketMessage(ws, message) {
|
|
37
|
+
let msg;
|
|
38
|
+
try {
|
|
39
|
+
msg = JSON.parse(typeof message === "string" ? message : "");
|
|
40
|
+
} catch {
|
|
41
|
+
this.send(ws, {
|
|
42
|
+
type: "error",
|
|
43
|
+
message: "Invalid JSON"
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (msg.type === "subscribe") await this.handleSubscribe(ws, msg.views);
|
|
48
|
+
else if (msg.type === "commit") await this.handleCommit(ws, msg.ops, msg.requestId);
|
|
49
|
+
}
|
|
50
|
+
async webSocketClose(ws) {
|
|
51
|
+
this.sessions.delete(ws);
|
|
52
|
+
}
|
|
53
|
+
async webSocketError(ws) {
|
|
54
|
+
this.sessions.delete(ws);
|
|
55
|
+
ws.close();
|
|
56
|
+
}
|
|
57
|
+
async handleSubscribe(ws, views) {
|
|
58
|
+
this.sessions.set(ws, { views });
|
|
59
|
+
for (const viewName of views) {
|
|
60
|
+
const parsed = this.sql.exec(`SELECT id, view, data, createdAt, updatedAt, version
|
|
61
|
+
FROM views WHERE view = ?`, viewName).toArray().map((row) => ({
|
|
62
|
+
...row,
|
|
63
|
+
data: JSON.parse(row.data)
|
|
64
|
+
}));
|
|
65
|
+
this.send(ws, {
|
|
66
|
+
type: "hydrate",
|
|
67
|
+
view: viewName,
|
|
68
|
+
rows: parsed
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async handleCommit(ws, ops, requestId) {
|
|
73
|
+
try {
|
|
74
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
this.sql.exec("BEGIN");
|
|
76
|
+
try {
|
|
77
|
+
for (const op of ops) if (op.kind === "add") this.sql.exec(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
|
|
78
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
79
|
+
ON CONFLICT(id) DO NOTHING`, op.id, op.view.name, JSON.stringify(op.value), timestamp, timestamp, 1);
|
|
80
|
+
else if (op.kind === "update") this.sql.exec(`UPDATE views
|
|
81
|
+
SET data = json_patch(data, ?), updatedAt = ?, version = version + 1
|
|
82
|
+
WHERE id = ? AND view = ?`, JSON.stringify(op.value), timestamp, op.id, op.view.name);
|
|
83
|
+
else if (op.kind === "remove") {
|
|
84
|
+
this.sql.exec(`DELETE FROM views WHERE id = ? AND view = ?`, op.id, op.view.name);
|
|
85
|
+
this.sql.exec(`DELETE FROM relations
|
|
86
|
+
WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`, op.view.name, op.id, op.view.name, op.id);
|
|
87
|
+
} else if (op.kind === "link") this.sql.exec(`INSERT OR IGNORE INTO relations (parent, pid, child, cid)
|
|
88
|
+
VALUES (?, ?, ?, ?)`, op.parent.view.name, op.parent.id, op.child.view.name, op.child.id);
|
|
89
|
+
else if (op.kind === "unlink") this.sql.exec(`DELETE FROM relations
|
|
90
|
+
WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, op.parent.view.name, op.parent.id, op.child.view.name, op.child.id);
|
|
91
|
+
this.sql.exec("COMMIT");
|
|
92
|
+
} catch (err) {
|
|
93
|
+
this.sql.exec("ROLLBACK");
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
this.send(ws, {
|
|
97
|
+
type: "committed",
|
|
98
|
+
requestId,
|
|
99
|
+
ops
|
|
100
|
+
});
|
|
101
|
+
const touchedViews = new Set(ops.map((op) => op.kind === "link" || op.kind === "unlink" ? op.parent.view.name : op.view.name));
|
|
102
|
+
for (const [otherWs, session] of this.sessions) {
|
|
103
|
+
if (otherWs === ws) continue;
|
|
104
|
+
if (session.views.some((v) => touchedViews.has(v))) this.send(otherWs, {
|
|
105
|
+
type: "push",
|
|
106
|
+
ops
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (const otherWs of this.state.getWebSockets()) {
|
|
110
|
+
if (otherWs === ws) continue;
|
|
111
|
+
if (!this.sessions.has(otherWs)) this.send(otherWs, {
|
|
112
|
+
type: "push",
|
|
113
|
+
ops
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
this.send(ws, {
|
|
118
|
+
type: "rejected",
|
|
119
|
+
requestId,
|
|
120
|
+
reason: err instanceof Error ? err.message : "Unknown error"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
send(ws, msg) {
|
|
125
|
+
try {
|
|
126
|
+
ws.send(JSON.stringify(msg));
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
exports.SyncObject = SyncObject;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/server/do.d.ts
|
|
2
|
+
interface Env {
|
|
3
|
+
SILO_SYNC: DurableObjectNamespace;
|
|
4
|
+
SILO_DB: D1Database;
|
|
5
|
+
}
|
|
6
|
+
declare class SyncObject implements DurableObject {
|
|
7
|
+
private readonly state;
|
|
8
|
+
private readonly env;
|
|
9
|
+
private sql;
|
|
10
|
+
private sessions;
|
|
11
|
+
constructor(state: DurableObjectState, env: Env);
|
|
12
|
+
fetch(request: Request): Promise<Response>;
|
|
13
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
14
|
+
webSocketClose(ws: WebSocket): Promise<void>;
|
|
15
|
+
webSocketError(ws: WebSocket): Promise<void>;
|
|
16
|
+
private handleSubscribe;
|
|
17
|
+
private handleCommit;
|
|
18
|
+
private send;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { SyncObject };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/server/do.d.ts
|
|
2
|
+
interface Env {
|
|
3
|
+
SILO_SYNC: DurableObjectNamespace;
|
|
4
|
+
SILO_DB: D1Database;
|
|
5
|
+
}
|
|
6
|
+
declare class SyncObject implements DurableObject {
|
|
7
|
+
private readonly state;
|
|
8
|
+
private readonly env;
|
|
9
|
+
private sql;
|
|
10
|
+
private sessions;
|
|
11
|
+
constructor(state: DurableObjectState, env: Env);
|
|
12
|
+
fetch(request: Request): Promise<Response>;
|
|
13
|
+
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise<void>;
|
|
14
|
+
webSocketClose(ws: WebSocket): Promise<void>;
|
|
15
|
+
webSocketError(ws: WebSocket): Promise<void>;
|
|
16
|
+
private handleSubscribe;
|
|
17
|
+
private handleCommit;
|
|
18
|
+
private send;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { SyncObject };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
//#region src/server/do.ts
|
|
2
|
+
var SyncObject = class {
|
|
3
|
+
constructor(state, env) {
|
|
4
|
+
this.state = state;
|
|
5
|
+
this.env = env;
|
|
6
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
7
|
+
this.sql = state.storage.sql;
|
|
8
|
+
this.sql.exec(`
|
|
9
|
+
CREATE TABLE IF NOT EXISTS views (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
view TEXT NOT NULL,
|
|
12
|
+
data BLOB NOT NULL,
|
|
13
|
+
createdAt TEXT NOT NULL,
|
|
14
|
+
updatedAt TEXT NOT NULL,
|
|
15
|
+
version INTEGER DEFAULT 0
|
|
16
|
+
);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
18
|
+
parent TEXT NOT NULL,
|
|
19
|
+
pid TEXT NOT NULL,
|
|
20
|
+
child TEXT NOT NULL,
|
|
21
|
+
cid TEXT NOT NULL
|
|
22
|
+
);
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
async fetch(request) {
|
|
26
|
+
const upgradeHeader = request.headers.get("Upgrade");
|
|
27
|
+
if (!upgradeHeader || upgradeHeader !== "websocket") return new Response("Expected WebSocket", { status: 426 });
|
|
28
|
+
const { 0: client, 1: server } = new WebSocketPair();
|
|
29
|
+
this.state.acceptWebSocket(server);
|
|
30
|
+
return new Response(null, {
|
|
31
|
+
status: 101,
|
|
32
|
+
webSocket: client
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async webSocketMessage(ws, message) {
|
|
36
|
+
let msg;
|
|
37
|
+
try {
|
|
38
|
+
msg = JSON.parse(typeof message === "string" ? message : "");
|
|
39
|
+
} catch {
|
|
40
|
+
this.send(ws, {
|
|
41
|
+
type: "error",
|
|
42
|
+
message: "Invalid JSON"
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (msg.type === "subscribe") await this.handleSubscribe(ws, msg.views);
|
|
47
|
+
else if (msg.type === "commit") await this.handleCommit(ws, msg.ops, msg.requestId);
|
|
48
|
+
}
|
|
49
|
+
async webSocketClose(ws) {
|
|
50
|
+
this.sessions.delete(ws);
|
|
51
|
+
}
|
|
52
|
+
async webSocketError(ws) {
|
|
53
|
+
this.sessions.delete(ws);
|
|
54
|
+
ws.close();
|
|
55
|
+
}
|
|
56
|
+
async handleSubscribe(ws, views) {
|
|
57
|
+
this.sessions.set(ws, { views });
|
|
58
|
+
for (const viewName of views) {
|
|
59
|
+
const parsed = this.sql.exec(`SELECT id, view, data, createdAt, updatedAt, version
|
|
60
|
+
FROM views WHERE view = ?`, viewName).toArray().map((row) => ({
|
|
61
|
+
...row,
|
|
62
|
+
data: JSON.parse(row.data)
|
|
63
|
+
}));
|
|
64
|
+
this.send(ws, {
|
|
65
|
+
type: "hydrate",
|
|
66
|
+
view: viewName,
|
|
67
|
+
rows: parsed
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async handleCommit(ws, ops, requestId) {
|
|
72
|
+
try {
|
|
73
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
74
|
+
this.sql.exec("BEGIN");
|
|
75
|
+
try {
|
|
76
|
+
for (const op of ops) if (op.kind === "add") this.sql.exec(`INSERT INTO views (id, view, data, createdAt, updatedAt, version)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
78
|
+
ON CONFLICT(id) DO NOTHING`, op.id, op.view.name, JSON.stringify(op.value), timestamp, timestamp, 1);
|
|
79
|
+
else if (op.kind === "update") this.sql.exec(`UPDATE views
|
|
80
|
+
SET data = json_patch(data, ?), updatedAt = ?, version = version + 1
|
|
81
|
+
WHERE id = ? AND view = ?`, JSON.stringify(op.value), timestamp, op.id, op.view.name);
|
|
82
|
+
else if (op.kind === "remove") {
|
|
83
|
+
this.sql.exec(`DELETE FROM views WHERE id = ? AND view = ?`, op.id, op.view.name);
|
|
84
|
+
this.sql.exec(`DELETE FROM relations
|
|
85
|
+
WHERE (parent = ? AND pid = ?) OR (child = ? AND cid = ?)`, op.view.name, op.id, op.view.name, op.id);
|
|
86
|
+
} else if (op.kind === "link") this.sql.exec(`INSERT OR IGNORE INTO relations (parent, pid, child, cid)
|
|
87
|
+
VALUES (?, ?, ?, ?)`, op.parent.view.name, op.parent.id, op.child.view.name, op.child.id);
|
|
88
|
+
else if (op.kind === "unlink") this.sql.exec(`DELETE FROM relations
|
|
89
|
+
WHERE parent = ? AND pid = ? AND child = ? AND cid = ?`, op.parent.view.name, op.parent.id, op.child.view.name, op.child.id);
|
|
90
|
+
this.sql.exec("COMMIT");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.sql.exec("ROLLBACK");
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
this.send(ws, {
|
|
96
|
+
type: "committed",
|
|
97
|
+
requestId,
|
|
98
|
+
ops
|
|
99
|
+
});
|
|
100
|
+
const touchedViews = new Set(ops.map((op) => op.kind === "link" || op.kind === "unlink" ? op.parent.view.name : op.view.name));
|
|
101
|
+
for (const [otherWs, session] of this.sessions) {
|
|
102
|
+
if (otherWs === ws) continue;
|
|
103
|
+
if (session.views.some((v) => touchedViews.has(v))) this.send(otherWs, {
|
|
104
|
+
type: "push",
|
|
105
|
+
ops
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
for (const otherWs of this.state.getWebSockets()) {
|
|
109
|
+
if (otherWs === ws) continue;
|
|
110
|
+
if (!this.sessions.has(otherWs)) this.send(otherWs, {
|
|
111
|
+
type: "push",
|
|
112
|
+
ops
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this.send(ws, {
|
|
117
|
+
type: "rejected",
|
|
118
|
+
requestId,
|
|
119
|
+
reason: err instanceof Error ? err.message : "Unknown error"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
send(ws, msg) {
|
|
124
|
+
try {
|
|
125
|
+
ws.send(JSON.stringify(msg));
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { SyncObject };
|