@bod.ee/db 0.7.0 → 0.8.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.
@@ -5,11 +5,18 @@ import type { ClientMessage } from '../shared/protocol.ts';
5
5
  import { Errors } from '../shared/errors.ts';
6
6
  import { normalizePath, pathKey } from '../shared/pathUtils.ts';
7
7
 
8
- interface WsData {
8
+ interface VfsTransfer {
9
+ path: string;
10
+ mime?: string;
11
+ chunks: string[];
12
+ }
13
+
14
+ export interface WsData {
9
15
  auth: Record<string, unknown> | null;
10
16
  valueSubs: Map<string, () => void>;
11
17
  childSubs: Map<string, () => void>;
12
18
  streamSubs: Map<string, () => void>;
19
+ vfsTransfers: Map<string, VfsTransfer>;
13
20
  }
14
21
 
15
22
  export class TransportOptions {
@@ -33,437 +40,647 @@ export class Transport {
33
40
  this.options = { ...new TransportOptions(), ...options };
34
41
  }
35
42
 
36
- start(): Server {
37
- const self = this;
38
- const extractAuth = async (req: Request): Promise<Record<string, unknown> | null> => {
39
- const header = req.headers.get('authorization');
40
- if (!header?.startsWith('Bearer ')) return null;
41
- const token = header.slice(7);
42
- if (self.options.auth) return await self.options.auth(token);
43
- return { token };
44
- };
45
- this.server = Bun.serve({
46
- port: this.options.port,
47
- fetch(req, server) {
48
- const url = new URL(req.url);
43
+ private async extractAuth(req: Request): Promise<Record<string, unknown> | null> {
44
+ const header = req.headers.get('authorization');
45
+ if (!header?.startsWith('Bearer ')) return null;
46
+ const token = header.slice(7);
47
+ if (this.options.auth) return await this.options.auth(token);
48
+ return { token };
49
+ }
49
50
 
50
- // WebSocket upgrade (only if client actually requests it)
51
- if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
52
- if (server.upgrade(req, { data: { auth: null, valueSubs: new Map(), childSubs: new Map(), streamSubs: new Map() } as WsData })) {
53
- return;
54
- }
55
- }
51
+ /** Create fresh WsData for a new WebSocket connection */
52
+ newWsData(): WsData {
53
+ return { auth: null, valueSubs: new Map(), childSubs: new Map(), streamSubs: new Map(), vfsTransfers: new Map() };
54
+ }
56
55
 
57
- // REST: GET /path → get
58
- if (req.method === 'GET' && url.pathname.startsWith('/db/')) {
59
- return (async () => {
60
- const path = normalizePath(url.pathname.slice(4));
61
- const auth = await extractAuth(req);
62
- if (self.rules && !self.rules.check('read', path, auth)) {
63
- return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
64
- }
65
- const data = self.db.get(path);
66
- return Response.json({ ok: true, data });
67
- })();
56
+ /**
57
+ * Handle an HTTP request. Returns a Response, or undefined if the request
58
+ * was upgraded to WebSocket (caller should return immediately in that case).
59
+ * Pass `server` to enable WebSocket upgrades.
60
+ */
61
+ handleFetch(req: Request, server?: Server): Response | Promise<Response | undefined> | undefined {
62
+ const url = new URL(req.url);
63
+
64
+ // WebSocket upgrade (only if server provided and client requests it)
65
+ if (server && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
66
+ if (server.upgrade(req, { data: this.newWsData() })) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ // REST: GET /db/<path> → get
72
+ if (req.method === 'GET' && url.pathname.startsWith('/db/')) {
73
+ return (async () => {
74
+ const path = normalizePath(url.pathname.slice(4));
75
+ const auth = await this.extractAuth(req);
76
+ if (this.rules && !this.rules.check('read', path, auth)) {
77
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
68
78
  }
69
-
70
- // REST: PUT /path set
71
- if (req.method === 'PUT' && url.pathname.startsWith('/db/')) {
72
- return (async () => {
73
- const path = normalizePath(url.pathname.slice(4));
74
- const auth = await extractAuth(req);
75
- if (self.rules && !self.rules.check('write', path, auth)) {
76
- return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
77
- }
79
+ const meta = this.db.storage.getWithMeta(path);
80
+ return Response.json({ ok: true, data: meta?.data ?? null, updatedAt: meta?.updatedAt });
81
+ })();
82
+ }
83
+
84
+ // REST: PUT /db/<path> → set
85
+ if (req.method === 'PUT' && url.pathname.startsWith('/db/')) {
86
+ return (async () => {
87
+ const path = normalizePath(url.pathname.slice(4));
88
+ if (this.db.replication?.isReplica) {
89
+ try {
78
90
  const body = await req.json();
79
- self.db.set(path, body);
91
+ await this.db.replication.proxyWrite({ op: 'set', path, value: body });
80
92
  return Response.json({ ok: true });
81
- })();
93
+ } catch (e: any) {
94
+ return Response.json({ ok: false, error: e.message, code: Errors.INTERNAL }, { status: 500 });
95
+ }
82
96
  }
83
-
84
- // REST: DELETE /path delete
85
- if (req.method === 'DELETE' && url.pathname.startsWith('/db/')) {
86
- return (async () => {
87
- const path = normalizePath(url.pathname.slice(4));
88
- const auth = await extractAuth(req);
89
- if (self.rules && !self.rules.check('write', path, auth)) {
90
- return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
91
- }
92
- self.db.delete(path);
97
+ const auth = await this.extractAuth(req);
98
+ if (this.rules && !this.rules.check('write', path, auth)) {
99
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
100
+ }
101
+ const body = await req.json();
102
+ this.db.set(path, body);
103
+ return Response.json({ ok: true });
104
+ })();
105
+ }
106
+
107
+ // REST: DELETE /db/<path> → delete
108
+ if (req.method === 'DELETE' && url.pathname.startsWith('/db/')) {
109
+ return (async () => {
110
+ const path = normalizePath(url.pathname.slice(4));
111
+ if (this.db.replication?.isReplica) {
112
+ try {
113
+ await this.db.replication.proxyWrite({ op: 'delete', path });
93
114
  return Response.json({ ok: true });
94
- })();
115
+ } catch (e: any) {
116
+ return Response.json({ ok: false, error: e.message, code: Errors.INTERNAL }, { status: 500 });
117
+ }
118
+ }
119
+ const auth = await this.extractAuth(req);
120
+ if (this.rules && !this.rules.check('write', path, auth)) {
121
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
122
+ }
123
+ this.db.delete(path);
124
+ return Response.json({ ok: true });
125
+ })();
126
+ }
127
+
128
+ // SSE: GET /sse/<path>?event=value|child
129
+ if (req.method === 'GET' && url.pathname.startsWith('/sse/')) {
130
+ return (async () => {
131
+ const path = normalizePath(url.pathname.slice(5));
132
+ const event = url.searchParams.get('event') || 'value';
133
+ const auth = await this.extractAuth(req);
134
+
135
+ if (this.rules && !this.rules.check('read', path, auth)) {
136
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
95
137
  }
96
138
 
97
- // SSE: GET /sse/path?event=value|child
98
- if (req.method === 'GET' && url.pathname.startsWith('/sse/')) {
99
- return (async () => {
100
- const path = normalizePath(url.pathname.slice(5));
101
- const event = url.searchParams.get('event') || 'value';
102
- const auth = await extractAuth(req);
103
-
104
- if (self.rules && !self.rules.check('read', path, auth)) {
105
- return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
106
- }
107
-
108
- let cleanup: (() => void) | null = null;
109
- const stream = new ReadableStream({
110
- start(controller) {
111
- const encoder = new TextEncoder();
112
- // Initial SSE comment to establish the stream
113
- controller.enqueue(encoder.encode(': ok\n\n'));
114
- const send = (data: string) => {
115
- try { controller.enqueue(encoder.encode(`data: ${data}\n\n`)); } catch {}
116
- };
117
-
118
- if (event === 'child') {
119
- cleanup = self.db.onChild(path, (e) => {
120
- send(JSON.stringify({ type: 'child', path, key: e.key, data: e.val(), event: e.type }));
121
- });
122
- } else {
123
- cleanup = self.db.on(path, (snap) => {
124
- send(JSON.stringify({ type: 'value', path: snap.path, data: snap.val() }));
125
- });
126
- }
127
- },
128
- cancel() {
129
- cleanup?.();
130
- },
131
- });
132
-
133
- return new Response(stream, {
134
- headers: {
135
- 'Content-Type': 'text/event-stream',
136
- 'Cache-Control': 'no-cache',
137
- 'Connection': 'keep-alive',
138
- },
139
+ let cleanup: (() => void) | null = null;
140
+ const stream = new ReadableStream({
141
+ start: (controller) => {
142
+ const encoder = new TextEncoder();
143
+ controller.enqueue(encoder.encode(': ok\n\n'));
144
+ const send = (data: string) => {
145
+ try { controller.enqueue(encoder.encode(`data: ${data}\n\n`)); } catch {}
146
+ };
147
+
148
+ if (event === 'child') {
149
+ cleanup = this.db.onChild(path, (e) => {
150
+ send(JSON.stringify({ type: 'child', path, key: e.key, data: e.val(), event: e.type }));
139
151
  });
140
- })();
152
+ } else {
153
+ cleanup = this.db.on(path, (snap) => {
154
+ send(JSON.stringify({ type: 'value', path: snap.path, data: snap.val() }));
155
+ });
156
+ }
157
+ },
158
+ cancel() {
159
+ cleanup?.();
160
+ },
161
+ });
162
+
163
+ return new Response(stream, {
164
+ headers: {
165
+ 'Content-Type': 'text/event-stream',
166
+ 'Cache-Control': 'no-cache',
167
+ 'Connection': 'keep-alive',
168
+ },
169
+ });
170
+ })();
171
+ }
172
+
173
+ // VFS REST routes
174
+ if (this.db.vfs && url.pathname.startsWith('/files/')) {
175
+ const vfsPath = normalizePath(url.pathname.slice(7));
176
+ return (async () => {
177
+ const auth = await this.extractAuth(req);
178
+
179
+ if (req.method === 'GET' && url.searchParams.has('stat')) {
180
+ if (this.rules && !this.rules.check('read', `_vfs/${vfsPath}`, auth)) {
181
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
182
+ }
183
+ const stat = this.db.vfs!.stat(vfsPath);
184
+ if (!stat) return Response.json({ ok: false, error: 'Not found', code: Errors.NOT_FOUND }, { status: 404 });
185
+ return Response.json({ ok: true, data: stat });
141
186
  }
142
187
 
143
- // Static routes
144
- if (self.options.staticRoutes) {
145
- const filePath = self.options.staticRoutes[url.pathname];
146
- if (filePath) return new Response(Bun.file(filePath));
188
+ if (req.method === 'GET' && url.searchParams.has('list')) {
189
+ if (this.rules && !this.rules.check('read', `_vfs/${vfsPath}`, auth)) {
190
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
191
+ }
192
+ return Response.json({ ok: true, data: this.db.vfs!.list(vfsPath) });
147
193
  }
148
194
 
149
- return new Response('Not Found', { status: 404 });
150
- },
151
- websocket: {
152
- open(ws: ServerWebSocket<WsData>) { self._clients.add(ws); },
153
- close(ws: ServerWebSocket<WsData>) {
154
- self._clients.delete(ws);
155
- // Cleanup all subscriptions
156
- for (const off of ws.data.valueSubs.values()) off();
157
- for (const off of ws.data.childSubs.values()) off();
158
- for (const off of ws.data.streamSubs.values()) off();
159
- ws.data.valueSubs.clear();
160
- ws.data.childSubs.clear();
161
- ws.data.streamSubs.clear();
162
- },
163
- async message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
164
- let msg: ClientMessage;
195
+ if (req.method === 'GET') {
196
+ if (this.rules && !this.rules.check('read', `_vfs/${vfsPath}`, auth)) {
197
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
198
+ }
165
199
  try {
166
- msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
200
+ const data = await this.db.vfs!.read(vfsPath);
201
+ const stat = this.db.vfs!.stat(vfsPath);
202
+ return new Response(data, {
203
+ headers: { 'Content-Type': stat?.mime || 'application/octet-stream' },
204
+ });
167
205
  } catch {
168
- ws.send(JSON.stringify({ id: '?', ok: false, error: 'Invalid JSON', code: Errors.INTERNAL }));
169
- return;
206
+ return Response.json({ ok: false, error: 'Not found', code: Errors.NOT_FOUND }, { status: 404 });
170
207
  }
208
+ }
171
209
 
172
- const { id } = msg;
173
- const reply = (data: unknown) => ws.send(JSON.stringify({ id, ok: true, data }));
174
- const error = (err: string, code: string) => ws.send(JSON.stringify({ id, ok: false, error: err, code }));
210
+ if (req.method === 'POST' && url.searchParams.has('mkdir')) {
211
+ if (this.rules && !this.rules.check('write', `_vfs/${vfsPath}`, auth)) {
212
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
213
+ }
214
+ const stat = this.db.vfs!.mkdir(vfsPath);
215
+ return Response.json({ ok: true, data: stat });
216
+ }
175
217
 
176
- try {
177
- switch (msg.op) {
178
- case 'auth': {
179
- if (!self.options.auth) {
180
- ws.data.auth = { token: msg.token };
181
- } else {
182
- const result = await self.options.auth(msg.token);
183
- if (!result) return error('Authentication failed', Errors.AUTH_REQUIRED);
184
- ws.data.auth = result;
185
- }
186
- return reply(null);
218
+ if (req.method === 'POST') {
219
+ if (this.rules && !this.rules.check('write', `_vfs/${vfsPath}`, auth)) {
220
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
221
+ }
222
+ const buf = await req.arrayBuffer();
223
+ const mime = req.headers.get('content-type') || undefined;
224
+ const stat = await this.db.vfs!.write(vfsPath, new Uint8Array(buf), mime);
225
+ return Response.json({ ok: true, data: stat });
226
+ }
227
+
228
+ if (req.method === 'PUT' && url.searchParams.has('move')) {
229
+ if (this.rules && !this.rules.check('write', `_vfs/${vfsPath}`, auth)) {
230
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
231
+ }
232
+ const dst = url.searchParams.get('move')!;
233
+ const stat = await this.db.vfs!.move(vfsPath, dst);
234
+ return Response.json({ ok: true, data: stat });
235
+ }
236
+
237
+ if (req.method === 'DELETE') {
238
+ if (this.rules && !this.rules.check('write', `_vfs/${vfsPath}`, auth)) {
239
+ return Response.json({ ok: false, error: 'Permission denied', code: Errors.PERMISSION_DENIED }, { status: 403 });
240
+ }
241
+ await this.db.vfs!.remove(vfsPath);
242
+ return Response.json({ ok: true });
243
+ }
244
+
245
+ return new Response('Method not allowed', { status: 405 });
246
+ })();
247
+ }
248
+
249
+ // Static routes
250
+ if (this.options.staticRoutes) {
251
+ const filePath = this.options.staticRoutes[url.pathname];
252
+ if (filePath) return new Response(Bun.file(filePath));
253
+ }
254
+
255
+ return new Response('Not Found', { status: 404 });
256
+ }
257
+
258
+ /** WebSocket handler config for Bun.serve() */
259
+ get websocketConfig() {
260
+ const self = this;
261
+ return {
262
+ open(ws: ServerWebSocket<WsData>) { self._clients.add(ws); },
263
+ close(ws: ServerWebSocket<WsData>) {
264
+ self._clients.delete(ws);
265
+ for (const off of ws.data.valueSubs.values()) off();
266
+ for (const off of ws.data.childSubs.values()) off();
267
+ for (const off of ws.data.streamSubs.values()) off();
268
+ ws.data.valueSubs.clear();
269
+ ws.data.childSubs.clear();
270
+ ws.data.streamSubs.clear();
271
+ },
272
+ async message(ws: ServerWebSocket<WsData>, raw: string | Buffer) {
273
+ let msg: ClientMessage;
274
+ try {
275
+ msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
276
+ } catch {
277
+ ws.send(JSON.stringify({ id: '?', ok: false, error: 'Invalid JSON', code: Errors.INTERNAL }));
278
+ return;
279
+ }
280
+
281
+ const { id } = msg;
282
+ const reply = (data: unknown, extra?: Record<string, unknown>) => ws.send(JSON.stringify({ id, ok: true, data, ...extra }));
283
+ const error = (err: string, code: string) => ws.send(JSON.stringify({ id, ok: false, error: err, code }));
284
+
285
+ try {
286
+ switch (msg.op) {
287
+ case 'auth': {
288
+ if (!self.options.auth) {
289
+ ws.data.auth = { token: msg.token };
290
+ } else {
291
+ const result = await self.options.auth(msg.token);
292
+ if (!result) return error('Authentication failed', Errors.AUTH_REQUIRED);
293
+ ws.data.auth = result;
187
294
  }
295
+ return reply(null);
296
+ }
188
297
 
189
- case 'get': {
190
- if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
191
- return error('Permission denied', Errors.PERMISSION_DENIED);
192
- }
193
- if (msg.shallow) return reply(self.db.getShallow(msg.path || undefined));
194
- return reply(self.db.get(msg.path));
298
+ case 'get': {
299
+ if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
300
+ return error('Permission denied', Errors.PERMISSION_DENIED);
195
301
  }
302
+ if (msg.shallow) return reply(self.db.getShallow(msg.path || undefined));
303
+ const meta = self.db.storage.getWithMeta(msg.path);
304
+ return reply(meta?.data ?? null, { updatedAt: meta?.updatedAt });
305
+ }
196
306
 
197
- case 'set': {
198
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth, self.db.get(msg.path), msg.value)) {
199
- return error('Permission denied', Errors.PERMISSION_DENIED);
200
- }
201
- self.db.set(msg.path, msg.value, msg.ttl ? { ttl: msg.ttl } : undefined);
202
- return reply(null);
307
+ case 'set': {
308
+ if (self.db.replication?.isReplica) {
309
+ try { return reply(await self.db.replication.proxyWrite(msg)); }
310
+ catch (e: any) { return error(e.message, Errors.INTERNAL); }
203
311
  }
312
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth, self.db.get(msg.path), msg.value)) {
313
+ return error('Permission denied', Errors.PERMISSION_DENIED);
314
+ }
315
+ self.db.set(msg.path, msg.value, msg.ttl ? { ttl: msg.ttl } : undefined);
316
+ return reply(null);
317
+ }
204
318
 
205
- case 'update': {
206
- for (const path of Object.keys(msg.updates)) {
207
- if (self.rules && !self.rules.check('write', path, ws.data.auth)) {
208
- return error(`Permission denied for ${path}`, Errors.PERMISSION_DENIED);
209
- }
319
+ case 'update': {
320
+ if (self.db.replication?.isReplica) {
321
+ try { return reply(await self.db.replication.proxyWrite(msg)); }
322
+ catch (e: any) { return error(e.message, Errors.INTERNAL); }
323
+ }
324
+ for (const path of Object.keys(msg.updates)) {
325
+ if (self.rules && !self.rules.check('write', path, ws.data.auth)) {
326
+ return error(`Permission denied for ${path}`, Errors.PERMISSION_DENIED);
210
327
  }
211
- self.db.update(msg.updates);
212
- return reply(null);
213
328
  }
329
+ self.db.update(msg.updates);
330
+ return reply(null);
331
+ }
214
332
 
215
- case 'delete': {
216
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
217
- return error('Permission denied', Errors.PERMISSION_DENIED);
218
- }
219
- self.db.delete(msg.path);
220
- return reply(null);
333
+ case 'delete': {
334
+ if (self.db.replication?.isReplica) {
335
+ try { return reply(await self.db.replication.proxyWrite(msg)); }
336
+ catch (e: any) { return error(e.message, Errors.INTERNAL); }
337
+ }
338
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
339
+ return error('Permission denied', Errors.PERMISSION_DENIED);
221
340
  }
341
+ self.db.delete(msg.path);
342
+ return reply(null);
343
+ }
222
344
 
223
- case 'query': {
224
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
225
- return error('Permission denied', Errors.PERMISSION_DENIED);
226
- }
227
- let q = self.db.query(msg.path);
228
- if (msg.filters) for (const f of msg.filters) q = q.where(f.field, f.op, f.value);
229
- if (msg.order) q = q.order(msg.order.field, msg.order.dir);
230
- if (msg.limit) q = q.limit(msg.limit);
231
- if (msg.offset) q = q.offset(msg.offset);
232
- return reply(q.get());
345
+ case 'query': {
346
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
347
+ return error('Permission denied', Errors.PERMISSION_DENIED);
233
348
  }
349
+ let q = self.db.query(msg.path);
350
+ if (msg.filters) for (const f of msg.filters) q = q.where(f.field, f.op, f.value);
351
+ if (msg.order) q = q.order(msg.order.field, msg.order.dir);
352
+ if (msg.limit) q = q.limit(msg.limit);
353
+ if (msg.offset) q = q.offset(msg.offset);
354
+ return reply(q.get());
355
+ }
234
356
 
235
- case 'sub': {
236
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
237
- return error('Permission denied', Errors.PERMISSION_DENIED);
238
- }
239
- const subKey = `${msg.event}:${msg.path}`;
240
- if (msg.event === 'value') {
241
- if (ws.data.valueSubs.has(subKey)) return reply(null); // already subscribed
242
- const off = self.db.on(msg.path, (snap) => {
243
- ws.send(JSON.stringify({ type: 'value', path: snap.path, data: snap.val() }));
244
- });
245
- ws.data.valueSubs.set(subKey, off);
246
- } else if (msg.event === 'child') {
247
- if (ws.data.childSubs.has(subKey)) return reply(null);
248
- const subPath = msg.path;
249
- const off = self.db.onChild(subPath, (event) => {
250
- ws.send(JSON.stringify({ type: 'child', path: subPath, key: event.key, data: event.val(), event: event.type }));
251
- });
252
- ws.data.childSubs.set(subKey, off);
253
- }
254
- return reply(null);
357
+ case 'sub': {
358
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
359
+ return error('Permission denied', Errors.PERMISSION_DENIED);
255
360
  }
361
+ const subKey = `${msg.event}:${msg.path}`;
362
+ if (msg.event === 'value') {
363
+ if (ws.data.valueSubs.has(subKey)) return reply(null);
364
+ const off = self.db.on(msg.path, (snap) => {
365
+ const meta = self.db.storage.getWithMeta(snap.path);
366
+ ws.send(JSON.stringify({ type: 'value', path: snap.path, data: snap.val(), updatedAt: meta?.updatedAt }));
367
+ });
368
+ ws.data.valueSubs.set(subKey, off);
369
+ } else if (msg.event === 'child') {
370
+ if (ws.data.childSubs.has(subKey)) return reply(null);
371
+ const subPath = msg.path;
372
+ const off = self.db.onChild(subPath, (event) => {
373
+ ws.send(JSON.stringify({ type: 'child', path: subPath, key: event.key, data: event.val(), event: event.type }));
374
+ });
375
+ ws.data.childSubs.set(subKey, off);
376
+ }
377
+ return reply(null);
378
+ }
256
379
 
257
- case 'unsub': {
258
- const unsubKey = `${msg.event}:${msg.path}`;
259
- if (msg.event === 'value') {
260
- ws.data.valueSubs.get(unsubKey)?.();
261
- ws.data.valueSubs.delete(unsubKey);
262
- } else if (msg.event === 'child') {
263
- ws.data.childSubs.get(unsubKey)?.();
264
- ws.data.childSubs.delete(unsubKey);
265
- }
266
- return reply(null);
380
+ case 'unsub': {
381
+ const unsubKey = `${msg.event}:${msg.path}`;
382
+ if (msg.event === 'value') {
383
+ ws.data.valueSubs.get(unsubKey)?.();
384
+ ws.data.valueSubs.delete(unsubKey);
385
+ } else if (msg.event === 'child') {
386
+ ws.data.childSubs.get(unsubKey)?.();
387
+ ws.data.childSubs.delete(unsubKey);
267
388
  }
389
+ return reply(null);
390
+ }
268
391
 
269
- case 'batch': {
270
- const results: unknown[] = [];
271
- self.db.transaction((tx) => {
272
- for (const batchOp of msg.operations) {
273
- switch (batchOp.op) {
274
- case 'set':
275
- if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
276
- throw new Error(`Permission denied for ${batchOp.path}`);
277
- }
278
- tx.set(batchOp.path, batchOp.value);
279
- break;
280
- case 'update':
281
- for (const path of Object.keys(batchOp.updates)) {
282
- if (self.rules && !self.rules.check('write', path, ws.data.auth)) {
283
- throw new Error(`Permission denied for ${path}`);
284
- }
285
- }
286
- tx.update(batchOp.updates);
287
- break;
288
- case 'delete':
289
- if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
290
- throw new Error(`Permission denied for ${batchOp.path}`);
291
- }
292
- tx.delete(batchOp.path);
293
- break;
294
- case 'push': {
295
- if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
296
- throw new Error(`Permission denied for ${batchOp.path}`);
392
+ case 'batch': {
393
+ if (self.db.replication?.isReplica) {
394
+ try { return reply(await self.db.replication.proxyWrite(msg)); }
395
+ catch (e: any) { return error(e.message, Errors.INTERNAL); }
396
+ }
397
+ const results: unknown[] = [];
398
+ self.db.transaction((tx) => {
399
+ for (const batchOp of msg.operations) {
400
+ switch (batchOp.op) {
401
+ case 'set':
402
+ if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
403
+ throw new Error(`Permission denied for ${batchOp.path}`);
404
+ }
405
+ tx.set(batchOp.path, batchOp.value);
406
+ break;
407
+ case 'update':
408
+ for (const path of Object.keys(batchOp.updates)) {
409
+ if (self.rules && !self.rules.check('write', path, ws.data.auth)) {
410
+ throw new Error(`Permission denied for ${path}`);
297
411
  }
298
- const key = self.db.push(batchOp.path, batchOp.value);
299
- results.push(key);
300
- break;
301
412
  }
413
+ tx.update(batchOp.updates);
414
+ break;
415
+ case 'delete':
416
+ if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
417
+ throw new Error(`Permission denied for ${batchOp.path}`);
418
+ }
419
+ tx.delete(batchOp.path);
420
+ break;
421
+ case 'push': {
422
+ if (self.rules && !self.rules.check('write', batchOp.path, ws.data.auth)) {
423
+ throw new Error(`Permission denied for ${batchOp.path}`);
424
+ }
425
+ const key = self.db.push(batchOp.path, batchOp.value);
426
+ results.push(key);
427
+ break;
302
428
  }
303
429
  }
304
- });
305
- return reply(results.length ? results : null);
306
- }
307
-
308
- case 'push': {
309
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
310
- return error('Permission denied', Errors.PERMISSION_DENIED);
311
430
  }
312
- const key = self.db.push(msg.path, msg.value, msg.idempotencyKey ? { idempotencyKey: msg.idempotencyKey } : undefined);
313
- return reply(key);
314
- }
431
+ });
432
+ return reply(results.length ? results : null);
433
+ }
315
434
 
316
- case 'stream-read': {
317
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
318
- return error('Permission denied', Errors.PERMISSION_DENIED);
319
- }
320
- const events = self.db.stream.read(msg.path, msg.groupId, msg.limit);
321
- return reply(events);
435
+ case 'push': {
436
+ if (self.db.replication?.isReplica) {
437
+ try { return reply(await self.db.replication.proxyWrite(msg)); }
438
+ catch (e: any) { return error(e.message, Errors.INTERNAL); }
439
+ }
440
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
441
+ return error('Permission denied', Errors.PERMISSION_DENIED);
322
442
  }
443
+ const key = self.db.push(msg.path, msg.value, msg.idempotencyKey ? { idempotencyKey: msg.idempotencyKey } : undefined);
444
+ return reply(key);
445
+ }
323
446
 
324
- case 'stream-ack': {
325
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
326
- return error('Permission denied', Errors.PERMISSION_DENIED);
327
- }
328
- self.db.stream.ack(msg.path, msg.groupId, msg.key);
329
- return reply(null);
447
+ case 'stream-read': {
448
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
449
+ return error('Permission denied', Errors.PERMISSION_DENIED);
330
450
  }
451
+ const events = self.db.stream.read(msg.path, msg.groupId, msg.limit);
452
+ return reply(events);
453
+ }
331
454
 
332
- case 'stream-sub': {
333
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
334
- return error('Permission denied', Errors.PERMISSION_DENIED);
335
- }
336
- const streamKey = `${msg.path}:${msg.groupId}`;
337
- // Unsub existing if any
338
- ws.data.streamSubs.get(streamKey)?.();
339
- const unsub = self.db.stream.subscribe(msg.path, msg.groupId, (events) => {
340
- ws.send(JSON.stringify({ type: 'stream', path: msg.path, groupId: msg.groupId, events }));
341
- });
342
- ws.data.streamSubs.set(streamKey, unsub);
343
- return reply(null);
455
+ case 'stream-ack': {
456
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
457
+ return error('Permission denied', Errors.PERMISSION_DENIED);
344
458
  }
459
+ self.db.stream.ack(msg.path, msg.groupId, msg.key);
460
+ return reply(null);
461
+ }
345
462
 
346
- case 'stream-unsub': {
347
- const streamUnsubKey = `${msg.path}:${msg.groupId}`;
348
- ws.data.streamSubs.get(streamUnsubKey)?.();
349
- ws.data.streamSubs.delete(streamUnsubKey);
350
- return reply(null);
463
+ case 'stream-sub': {
464
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
465
+ return error('Permission denied', Errors.PERMISSION_DENIED);
351
466
  }
467
+ const streamKey = `${msg.path}:${msg.groupId}`;
468
+ ws.data.streamSubs.get(streamKey)?.();
469
+ const unsub = self.db.stream.subscribe(msg.path, msg.groupId, (events) => {
470
+ ws.send(JSON.stringify({ type: 'stream', path: msg.path, groupId: msg.groupId, events }));
471
+ });
472
+ ws.data.streamSubs.set(streamKey, unsub);
473
+ return reply(null);
474
+ }
352
475
 
353
- // MQ ops
354
- case 'mq-push': {
355
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
356
- return error('Permission denied', Errors.PERMISSION_DENIED);
357
- }
358
- const mqKey = self.db.mq.push(msg.path, msg.value, msg.idempotencyKey ? { idempotencyKey: msg.idempotencyKey } : undefined);
359
- return reply(mqKey);
476
+ case 'stream-unsub': {
477
+ const streamUnsubKey = `${msg.path}:${msg.groupId}`;
478
+ ws.data.streamSubs.get(streamUnsubKey)?.();
479
+ ws.data.streamSubs.delete(streamUnsubKey);
480
+ return reply(null);
481
+ }
482
+
483
+ case 'mq-push': {
484
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
485
+ return error('Permission denied', Errors.PERMISSION_DENIED);
360
486
  }
361
- case 'mq-fetch': {
362
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
363
- return error('Permission denied', Errors.PERMISSION_DENIED);
364
- }
365
- return reply(self.db.mq.fetch(msg.path, msg.count));
487
+ const mqKey = self.db.mq.push(msg.path, msg.value, msg.idempotencyKey ? { idempotencyKey: msg.idempotencyKey } : undefined);
488
+ return reply(mqKey);
489
+ }
490
+ case 'mq-fetch': {
491
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
492
+ return error('Permission denied', Errors.PERMISSION_DENIED);
366
493
  }
367
- case 'mq-ack': {
368
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
369
- return error('Permission denied', Errors.PERMISSION_DENIED);
370
- }
371
- self.db.mq.ack(msg.path, msg.key);
372
- return reply(null);
494
+ return reply(self.db.mq.fetch(msg.path, msg.count));
495
+ }
496
+ case 'mq-ack': {
497
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
498
+ return error('Permission denied', Errors.PERMISSION_DENIED);
373
499
  }
374
- case 'mq-nack': {
375
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
376
- return error('Permission denied', Errors.PERMISSION_DENIED);
377
- }
378
- self.db.mq.nack(msg.path, msg.key);
379
- return reply(null);
500
+ self.db.mq.ack(msg.path, msg.key);
501
+ return reply(null);
502
+ }
503
+ case 'mq-nack': {
504
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
505
+ return error('Permission denied', Errors.PERMISSION_DENIED);
380
506
  }
381
- case 'mq-peek': {
382
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
383
- return error('Permission denied', Errors.PERMISSION_DENIED);
384
- }
385
- return reply(self.db.mq.peek(msg.path, msg.count));
507
+ self.db.mq.nack(msg.path, msg.key);
508
+ return reply(null);
509
+ }
510
+ case 'mq-peek': {
511
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
512
+ return error('Permission denied', Errors.PERMISSION_DENIED);
386
513
  }
387
- case 'mq-dlq': {
388
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
389
- return error('Permission denied', Errors.PERMISSION_DENIED);
390
- }
391
- return reply(self.db.mq.dlq(msg.path));
514
+ return reply(self.db.mq.peek(msg.path, msg.count));
515
+ }
516
+ case 'mq-dlq': {
517
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
518
+ return error('Permission denied', Errors.PERMISSION_DENIED);
392
519
  }
393
- case 'mq-purge': {
394
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
395
- return error('Permission denied', Errors.PERMISSION_DENIED);
396
- }
397
- return reply(self.db.mq.purge(msg.path, { all: msg.all }));
520
+ return reply(self.db.mq.dlq(msg.path));
521
+ }
522
+ case 'mq-purge': {
523
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
524
+ return error('Permission denied', Errors.PERMISSION_DENIED);
398
525
  }
526
+ return reply(self.db.mq.purge(msg.path, { all: msg.all }));
527
+ }
399
528
 
400
- // FTS ops
401
- case 'fts-search': {
402
- if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
403
- return error('Permission denied', Errors.PERMISSION_DENIED);
404
- }
405
- return reply(self.db.search({ text: msg.text, path: msg.path, limit: msg.limit }));
529
+ case 'fts-search': {
530
+ if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
531
+ return error('Permission denied', Errors.PERMISSION_DENIED);
406
532
  }
407
- case 'fts-index': {
408
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
409
- return error('Permission denied', Errors.PERMISSION_DENIED);
410
- }
411
- if (msg.fields) self.db.index(msg.path, msg.fields);
412
- else if (msg.content) self.db.index(msg.path, msg.content);
413
- return reply(null);
533
+ return reply(self.db.search({ text: msg.text, path: msg.path, limit: msg.limit }));
534
+ }
535
+ case 'fts-index': {
536
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
537
+ return error('Permission denied', Errors.PERMISSION_DENIED);
414
538
  }
539
+ if (msg.fields) self.db.index(msg.path, msg.fields);
540
+ else if (msg.content) self.db.index(msg.path, msg.content);
541
+ return reply(null);
542
+ }
415
543
 
416
- // Vector ops
417
- case 'vector-search': {
418
- if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
419
- return error('Permission denied', Errors.PERMISSION_DENIED);
420
- }
421
- return reply(self.db.vectorSearch({ query: msg.query, path: msg.path, limit: msg.limit, threshold: msg.threshold }));
544
+ case 'vector-search': {
545
+ if (self.rules && !self.rules.check('read', msg.path || '', ws.data.auth)) {
546
+ return error('Permission denied', Errors.PERMISSION_DENIED);
422
547
  }
423
- case 'vector-store': {
424
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
425
- return error('Permission denied', Errors.PERMISSION_DENIED);
426
- }
427
- self.db.vectors!.store(msg.path, msg.embedding);
428
- return reply(null);
548
+ return reply(self.db.vectorSearch({ query: msg.query, path: msg.path, limit: msg.limit, threshold: msg.threshold }));
549
+ }
550
+ case 'vector-store': {
551
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
552
+ return error('Permission denied', Errors.PERMISSION_DENIED);
429
553
  }
554
+ self.db.vectors!.store(msg.path, msg.embedding);
555
+ return reply(null);
556
+ }
430
557
 
431
- // Stream extended ops
432
- case 'stream-snapshot': {
433
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
434
- return error('Permission denied', Errors.PERMISSION_DENIED);
435
- }
436
- return reply(self.db.stream.snapshot(msg.path));
558
+ case 'stream-snapshot': {
559
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
560
+ return error('Permission denied', Errors.PERMISSION_DENIED);
437
561
  }
438
- case 'stream-materialize': {
439
- if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
440
- return error('Permission denied', Errors.PERMISSION_DENIED);
441
- }
442
- return reply(self.db.stream.materialize(msg.path, msg.keepKey ? { keepKey: msg.keepKey } : undefined));
562
+ return reply(self.db.stream.snapshot(msg.path));
563
+ }
564
+ case 'stream-materialize': {
565
+ if (self.rules && !self.rules.check('read', msg.path, ws.data.auth)) {
566
+ return error('Permission denied', Errors.PERMISSION_DENIED);
443
567
  }
444
- case 'stream-compact': {
445
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
446
- return error('Permission denied', Errors.PERMISSION_DENIED);
447
- }
448
- return reply(self.db.stream.compact(msg.path, { maxAge: msg.maxAge, maxCount: msg.maxCount, keepKey: msg.keepKey }));
568
+ return reply(self.db.stream.materialize(msg.path, msg.keepKey ? { keepKey: msg.keepKey } : undefined));
569
+ }
570
+ case 'stream-compact': {
571
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
572
+ return error('Permission denied', Errors.PERMISSION_DENIED);
449
573
  }
450
- case 'stream-reset': {
451
- if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
452
- return error('Permission denied', Errors.PERMISSION_DENIED);
453
- }
454
- self.db.stream.reset(msg.path);
455
- return reply(null);
574
+ return reply(self.db.stream.compact(msg.path, { maxAge: msg.maxAge, maxCount: msg.maxCount, keepKey: msg.keepKey }));
575
+ }
576
+ case 'stream-reset': {
577
+ if (self.rules && !self.rules.check('write', msg.path, ws.data.auth)) {
578
+ return error('Permission denied', Errors.PERMISSION_DENIED);
456
579
  }
580
+ self.db.stream.reset(msg.path);
581
+ return reply(null);
582
+ }
457
583
 
458
- default:
459
- return error('Unknown operation', Errors.INTERNAL);
584
+ case 'vfs-upload-init': {
585
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
586
+ if (self.rules && !self.rules.check('write', `_vfs/${msg.path}`, ws.data.auth)) {
587
+ return error('Permission denied', Errors.PERMISSION_DENIED);
588
+ }
589
+ const transferId = String(Date.now()) + Math.random().toString(36).slice(2, 6);
590
+ ws.data.vfsTransfers.set(transferId, { path: msg.path, mime: msg.mime, chunks: [] });
591
+ return reply(transferId);
592
+ }
593
+ case 'vfs-upload-chunk': {
594
+ const transfer = ws.data.vfsTransfers.get(msg.transferId);
595
+ if (!transfer) return error('Invalid transfer', Errors.INTERNAL);
596
+ transfer.chunks[msg.seq] = msg.data;
597
+ return reply(null);
598
+ }
599
+ case 'vfs-upload-done': {
600
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
601
+ const t = ws.data.vfsTransfers.get(msg.transferId);
602
+ if (!t) return error('Invalid transfer', Errors.INTERNAL);
603
+ ws.data.vfsTransfers.delete(msg.transferId);
604
+ const combined = Buffer.from(t.chunks.join(''), 'base64');
605
+ const stat = await self.db.vfs.write(t.path, new Uint8Array(combined), t.mime);
606
+ return reply(stat);
607
+ }
608
+ case 'vfs-download-init': {
609
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
610
+ if (self.rules && !self.rules.check('read', `_vfs/${msg.path}`, ws.data.auth)) {
611
+ return error('Permission denied', Errors.PERMISSION_DENIED);
612
+ }
613
+ try {
614
+ const data = await self.db.vfs.read(msg.path);
615
+ const b64 = Buffer.from(data).toString('base64');
616
+ const CHUNK_SIZE = 48000;
617
+ const totalChunks = Math.ceil(b64.length / CHUNK_SIZE) || 1;
618
+ const transferId = String(Date.now()) + Math.random().toString(36).slice(2, 6);
619
+ reply({ transferId, totalChunks });
620
+ for (let i = 0; i < totalChunks; i++) {
621
+ const chunk = b64.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
622
+ ws.send(JSON.stringify({ type: 'vfs-download-chunk', transferId, data: chunk, seq: i, done: i === totalChunks - 1 }));
623
+ }
624
+ } catch {
625
+ return error('File not found', Errors.INTERNAL);
626
+ }
627
+ return;
628
+ }
629
+ case 'vfs-stat': {
630
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
631
+ if (self.rules && !self.rules.check('read', `_vfs/${msg.path}`, ws.data.auth)) {
632
+ return error('Permission denied', Errors.PERMISSION_DENIED);
633
+ }
634
+ return reply(self.db.vfs.stat(msg.path));
635
+ }
636
+ case 'vfs-list': {
637
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
638
+ if (self.rules && !self.rules.check('read', `_vfs/${msg.path}`, ws.data.auth)) {
639
+ return error('Permission denied', Errors.PERMISSION_DENIED);
640
+ }
641
+ return reply(self.db.vfs.list(msg.path));
642
+ }
643
+ case 'vfs-delete': {
644
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
645
+ if (self.rules && !self.rules.check('write', `_vfs/${msg.path}`, ws.data.auth)) {
646
+ return error('Permission denied', Errors.PERMISSION_DENIED);
647
+ }
648
+ await self.db.vfs.remove(msg.path);
649
+ return reply(null);
650
+ }
651
+ case 'vfs-mkdir': {
652
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
653
+ if (self.rules && !self.rules.check('write', `_vfs/${msg.path}`, ws.data.auth)) {
654
+ return error('Permission denied', Errors.PERMISSION_DENIED);
655
+ }
656
+ return reply(self.db.vfs.mkdir(msg.path));
657
+ }
658
+ case 'vfs-move': {
659
+ if (!self.db.vfs) return error('VFS not configured', Errors.INTERNAL);
660
+ if (self.rules && !self.rules.check('write', `_vfs/${msg.path}`, ws.data.auth)) {
661
+ return error('Permission denied', Errors.PERMISSION_DENIED);
662
+ }
663
+ const moved = await self.db.vfs.move(msg.path, msg.dst);
664
+ return reply(moved);
460
665
  }
461
- } catch (e: unknown) {
462
- const message = e instanceof Error ? e.message : 'Internal error';
463
- return error(message, Errors.INTERNAL);
666
+
667
+ default:
668
+ return error('Unknown operation', Errors.INTERNAL);
464
669
  }
465
- },
670
+ } catch (e: unknown) {
671
+ const message = e instanceof Error ? e.message : 'Internal error';
672
+ return error(message, Errors.INTERNAL);
673
+ }
466
674
  },
675
+ };
676
+ }
677
+
678
+ /** Start standalone Bun HTTP+WS server */
679
+ start(): Server {
680
+ this.server = Bun.serve({
681
+ port: this.options.port,
682
+ fetch: (req, server) => this.handleFetch(req, server),
683
+ websocket: this.websocketConfig,
467
684
  });
468
685
  return this.server;
469
686
  }