@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.
@@ -0,0 +1,3 @@
1
+ import { type DBAdapterFactory } from "@arcote.tech/arc";
2
+ export declare const sqliteAdapterFactory: (dbName: string) => DBAdapterFactory;
3
+ //# sourceMappingURL=sqliteAdapter.d.ts.map
@@ -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
+ }
@@ -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
+ }