@arcote.tech/arc-host 0.1.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/host.d.ts +22 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6730 -0
- package/dist/sqliteAdapter.d.ts +3 -0
- package/dist/sqliteAdapter.d.ts.map +1 -0
- package/host.ts +268 -0
- package/index.ts +11 -0
- package/package.json +24 -0
- package/sqliteAdapter.ts +32 -0
- package/tsconfig.types.json +16 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqliteAdapter.d.ts","sourceRoot":"","sources":["../sqliteAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,kBAAkB,CAAC;AAoB1B,eAAO,MAAM,oBAAoB,WAAY,MAAM,KAAG,gBAMrD,CAAC"}
|
package/host.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArcCollection,
|
|
3
|
+
MasterDataStorage,
|
|
4
|
+
QueryBuilderContext,
|
|
5
|
+
QueryCache,
|
|
6
|
+
type ArcCollectionAny,
|
|
7
|
+
type ArcContextAny,
|
|
8
|
+
type DatabaseAdapter,
|
|
9
|
+
type DataStorageChanges,
|
|
10
|
+
type MessageClientToHost,
|
|
11
|
+
type MessageHostToClient,
|
|
12
|
+
type RealTimeCommunicationAdapter,
|
|
13
|
+
} from "@arcote.tech/arc";
|
|
14
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
15
|
+
import jwt from "jsonwebtoken";
|
|
16
|
+
|
|
17
|
+
class RTCHost implements RealTimeCommunicationAdapter {
|
|
18
|
+
private server!: Server;
|
|
19
|
+
private dataStore: MasterDataStorage;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private context: ArcContextAny,
|
|
23
|
+
dbAdapter: Promise<DatabaseAdapter>,
|
|
24
|
+
) {
|
|
25
|
+
this.dataStore = new MasterDataStorage(dbAdapter, () => this, context);
|
|
26
|
+
this.setupServer();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
commitChanges(changes: DataStorageChanges[]): void {
|
|
30
|
+
// throw new Error("Method not implemented.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async sync(
|
|
34
|
+
progressCallback: ({
|
|
35
|
+
store,
|
|
36
|
+
size,
|
|
37
|
+
}: {
|
|
38
|
+
store: string;
|
|
39
|
+
size: number;
|
|
40
|
+
}) => void,
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
// throw new Error("Method not implemented.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async verifyToken(token: string) {
|
|
46
|
+
try {
|
|
47
|
+
const secret = process.env.AUTH_SECRET as string;
|
|
48
|
+
if (!secret) {
|
|
49
|
+
throw new Error("AUTH_SECRET is not set");
|
|
50
|
+
}
|
|
51
|
+
const payload = jwt.verify(token, secret);
|
|
52
|
+
return payload;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Token verification failed:", error);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private async handleCommand(req: Request) {
|
|
60
|
+
const url = new URL(req.url);
|
|
61
|
+
const commandName = url.pathname.split("/command/")[1];
|
|
62
|
+
|
|
63
|
+
if (!commandName) {
|
|
64
|
+
return new Response("Command not specified", { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const argument = await req.json();
|
|
68
|
+
// Create command context and execute command
|
|
69
|
+
const queryContext = new QueryBuilderContext(
|
|
70
|
+
new QueryCache(),
|
|
71
|
+
this.dataStore,
|
|
72
|
+
);
|
|
73
|
+
const commands = this.context.commandsClient(
|
|
74
|
+
"server",
|
|
75
|
+
queryContext,
|
|
76
|
+
this.dataStore,
|
|
77
|
+
(error) => console.error("Command error:", error),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const result = await (commands as any)[commandName](argument);
|
|
81
|
+
return new Response(JSON.stringify(result), {
|
|
82
|
+
headers: { "Content-Type": "application/json" },
|
|
83
|
+
status: 200,
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`Error executing command ${commandName}:`, error);
|
|
87
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async handleQuery(req: Request) {
|
|
92
|
+
try {
|
|
93
|
+
const body = await req.json();
|
|
94
|
+
const { query } = body;
|
|
95
|
+
|
|
96
|
+
if (!query || !query.element || !query.queryType) {
|
|
97
|
+
return new Response("Invalid query format", { status: 400 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { element, queryType, params } = query;
|
|
101
|
+
|
|
102
|
+
// Access the model and run the query
|
|
103
|
+
const queryBuilder = this.context.queryBuilder(
|
|
104
|
+
new QueryBuilderContext(new QueryCache(), this.dataStore),
|
|
105
|
+
) as any;
|
|
106
|
+
|
|
107
|
+
// Get the appropriate element from the queryBuilder
|
|
108
|
+
const elementBuilder = queryBuilder[element];
|
|
109
|
+
if (!elementBuilder) {
|
|
110
|
+
return new Response(`Element '${element}' not found`, { status: 400 });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get the query method from the element
|
|
114
|
+
const queryMethod = elementBuilder[queryType];
|
|
115
|
+
if (!queryMethod || typeof queryMethod !== "function") {
|
|
116
|
+
return new Response(
|
|
117
|
+
`Query type '${queryType}' not found on element '${element}'`,
|
|
118
|
+
{ status: 400 },
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Build and execute the query
|
|
123
|
+
const queryParams = params || [];
|
|
124
|
+
const queryObj = queryMethod.apply(elementBuilder, queryParams).toQuery();
|
|
125
|
+
const result = await queryObj.run(this.dataStore);
|
|
126
|
+
|
|
127
|
+
return new Response(JSON.stringify(result), {
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
status: 200,
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error("Error executing query:", error);
|
|
133
|
+
return new Response(`Internal Server Error: ${error}`, {
|
|
134
|
+
status: 500,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private setupServer() {
|
|
140
|
+
this.server = Bun.serve({
|
|
141
|
+
fetch: async (req, server) => {
|
|
142
|
+
const url = new URL(req.url);
|
|
143
|
+
|
|
144
|
+
// const authHeader = req.headers.get("Authorization");
|
|
145
|
+
// const token =
|
|
146
|
+
// authHeader?.replace("Bearer ", "") || url.searchParams.get("token");
|
|
147
|
+
// console.log(url);
|
|
148
|
+
|
|
149
|
+
// if (!token) {
|
|
150
|
+
// return new Response("Unauthorized", { status: 401 });
|
|
151
|
+
// }
|
|
152
|
+
|
|
153
|
+
// const payload = await this.verifyToken(token);
|
|
154
|
+
// if (!payload) {
|
|
155
|
+
// return new Response("Invalid token", { status: 401 });
|
|
156
|
+
// }
|
|
157
|
+
const payload = null;
|
|
158
|
+
|
|
159
|
+
// Handle different endpoints
|
|
160
|
+
if (
|
|
161
|
+
url.pathname === "/ws" &&
|
|
162
|
+
req.headers.get("Upgrade") === "websocket"
|
|
163
|
+
) {
|
|
164
|
+
if (server.upgrade(req, { data: { user: payload } })) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
return new Response("Upgrade failed", { status: 500 });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (url.pathname === "/sync" && req.method === "GET") {
|
|
171
|
+
return await this.handleSync(url.searchParams.get("lastSync"));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (url.pathname.startsWith("/command/") && req.method === "POST") {
|
|
175
|
+
return await this.handleCommand(req);
|
|
176
|
+
}
|
|
177
|
+
console.log(url.pathname);
|
|
178
|
+
|
|
179
|
+
if (url.pathname === "/query" && req.method === "POST") {
|
|
180
|
+
return await this.handleQuery(req);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return new Response("Not Found", { status: 404 });
|
|
184
|
+
},
|
|
185
|
+
websocket: {
|
|
186
|
+
message: this.onMessage.bind(this),
|
|
187
|
+
open(ws) {
|
|
188
|
+
ws.subscribe("sync");
|
|
189
|
+
},
|
|
190
|
+
close(ws, code, message) {},
|
|
191
|
+
perMessageDeflate: true,
|
|
192
|
+
backpressureLimit: 16 * 1024 * 1024,
|
|
193
|
+
},
|
|
194
|
+
port: 5005,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async handleSync(lastDate: string | null) {
|
|
199
|
+
const syncDate = new Date();
|
|
200
|
+
const where = lastDate
|
|
201
|
+
? {
|
|
202
|
+
lastUpdate: {
|
|
203
|
+
$gt: new Date(lastDate),
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
: {};
|
|
207
|
+
|
|
208
|
+
// const client = await this.clientPromise;
|
|
209
|
+
const syncResults: { store: string; items: any[] }[] = [];
|
|
210
|
+
|
|
211
|
+
for (const collection of this.context.elements
|
|
212
|
+
.filter(
|
|
213
|
+
(element) => element instanceof ArcCollection,
|
|
214
|
+
// || element instanceof ArcIndexedCollection,
|
|
215
|
+
)
|
|
216
|
+
.concat([{ name: "state" } as unknown as ArcCollectionAny])) {
|
|
217
|
+
// const result = await client
|
|
218
|
+
// .db(process.env.MONGODB_DB)
|
|
219
|
+
// .collection(collection.name)
|
|
220
|
+
// .find(where)
|
|
221
|
+
// .toArray();
|
|
222
|
+
// const prepareDeleted = result.map((item) => {
|
|
223
|
+
// if (item.deleted) return { _id: item._id, deleted: true };
|
|
224
|
+
// return item;
|
|
225
|
+
// });
|
|
226
|
+
// syncResults.push({
|
|
227
|
+
// store: collection.name,
|
|
228
|
+
// items: prepareDeleted,
|
|
229
|
+
// });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return new Response(
|
|
233
|
+
JSON.stringify({
|
|
234
|
+
results: syncResults,
|
|
235
|
+
syncDate: syncDate.toISOString(),
|
|
236
|
+
}),
|
|
237
|
+
{
|
|
238
|
+
headers: {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private async onMessage(ws: ServerWebSocket, messageAsString: string) {
|
|
246
|
+
const message = JSON.parse(messageAsString) as MessageClientToHost;
|
|
247
|
+
switch (message.type) {
|
|
248
|
+
case "changes-executed":
|
|
249
|
+
await this.dataStore.applyChanges(message.changes);
|
|
250
|
+
this.publishMessage(ws, {
|
|
251
|
+
type: "state-changes",
|
|
252
|
+
changes: message.changes,
|
|
253
|
+
});
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
console.warn(`Message unsupported`, message);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private publishMessage(ws: ServerWebSocket, message: MessageHostToClient) {
|
|
261
|
+
ws.publish("sync", JSON.stringify(message));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const rtcHostFactory =
|
|
266
|
+
(context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>) => () => {
|
|
267
|
+
return new RTCHost(context, dbAdapter);
|
|
268
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ArcContextAny } from "@arcote.tech/arc";
|
|
2
|
+
import { rtcHostFactory } from "./host";
|
|
3
|
+
import { sqliteAdapterFactory } from "./sqliteAdapter";
|
|
4
|
+
|
|
5
|
+
export function hostLiveModel<C extends ArcContextAny>(
|
|
6
|
+
context: C,
|
|
7
|
+
options: { db: string; version: number },
|
|
8
|
+
) {
|
|
9
|
+
const dbAdapterFactory = sqliteAdapterFactory(options.db);
|
|
10
|
+
rtcHostFactory(context, dbAdapterFactory(context))();
|
|
11
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcote.tech/arc-host",
|
|
3
|
+
"module": "index.ts",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"private": false,
|
|
9
|
+
"author": "Przemysław Krasiński [arcote.tech]",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@arcote.tech/arc": "workspace:*",
|
|
12
|
+
"jsonwebtoken": "^9.0.2"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "rm -rf dist && bun build ./index.ts --target=bun --outdir=dist && bun run build:declaration",
|
|
16
|
+
"build:declaration": "tsc --emitDeclarationOnly --declarationMap --project tsconfig.types.json",
|
|
17
|
+
"postbuild": "rimraf tsconfig.types.tsbuildinfo",
|
|
18
|
+
"type-check": "tsc",
|
|
19
|
+
"dev": "nodemon --ignore dist -e ts,tsx --exec 'bun run build'"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/jsonwebtoken": "^9.0.7"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/sqliteAdapter.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSQLiteAdapterFactory,
|
|
3
|
+
type ArcContextAny,
|
|
4
|
+
type DBAdapterFactory,
|
|
5
|
+
type SQLiteDatabase,
|
|
6
|
+
} from "@arcote.tech/arc";
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
class BunSQLiteDatabase implements SQLiteDatabase {
|
|
10
|
+
constructor(private db: Database) {}
|
|
11
|
+
|
|
12
|
+
async exec(sql: string, params?: any[]): Promise<any> {
|
|
13
|
+
try {
|
|
14
|
+
if (params && params.length > 0) {
|
|
15
|
+
const stmt = this.db.prepare(sql);
|
|
16
|
+
return stmt.all(...params);
|
|
17
|
+
}
|
|
18
|
+
return this.db.query(sql).all();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error("SQLite error:", error);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const sqliteAdapterFactory = (dbName: string): DBAdapterFactory => {
|
|
27
|
+
return async (context: ArcContextAny) => {
|
|
28
|
+
const db = new Database(dbName);
|
|
29
|
+
const sqliteDb = new BunSQLiteDatabase(db);
|
|
30
|
+
return createSQLiteAdapterFactory(sqliteDb)(context);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsxdev",
|
|
5
|
+
"noEmit": false,
|
|
6
|
+
"emitDeclarationOnly": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"composite": true,
|
|
12
|
+
"preserveSymlinks": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["**/*.ts"],
|
|
15
|
+
"exclude": ["./build.ts", "node_modules", "dist"]
|
|
16
|
+
}
|