fivebit-client 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
1
  import { AuthClient } from './AuthClient';
2
+ import { RealtimeClient } from './RealtimeClient';
3
+ import { StorageClient } from './StorageClient';
2
4
  export interface FivebitConfig {
3
5
  url: string;
4
6
  apiKey?: string;
@@ -16,8 +18,9 @@ export interface QueryResult<T> {
16
18
  export declare class FivebitClient {
17
19
  config: FivebitConfig;
18
20
  auth: AuthClient;
21
+ storage: StorageClient;
22
+ realtime: RealtimeClient;
19
23
  private _query;
20
- private _rt;
21
24
  private _session;
22
25
  constructor(config: FivebitConfig);
23
26
  get session(): AuthSession | null;
@@ -28,8 +31,19 @@ export declare class FivebitClient {
28
31
  update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
29
32
  delete(id: number): Promise<boolean>;
30
33
  query(field: string, value: string, limit?: number): Promise<T[]>;
34
+ count(filter?: string): Promise<number>;
35
+ sum(field: string, filter?: string): Promise<number>;
36
+ avg(field: string, filter?: string): Promise<number>;
37
+ min(field: string): Promise<number>;
38
+ max(field: string): Promise<number>;
39
+ };
40
+ onChanges(table: string, fn: (record: any) => void): () => void;
41
+ channel(name: string): {
42
+ on(fn: (payload: any) => void): void;
43
+ broadcast(payload: any): void;
44
+ presence(userId: number, userName: string): void;
45
+ readonly users: any[];
31
46
  };
32
- onChanges(table: string, fn: (record: any) => void): void;
33
47
  close(): void;
34
48
  }
35
49
  export declare function createClient(config: FivebitConfig): FivebitClient;
@@ -5,23 +5,23 @@ exports.createClient = createClient;
5
5
  const AuthClient_1 = require("./AuthClient");
6
6
  const QueryClient_1 = require("./QueryClient");
7
7
  const RealtimeClient_1 = require("./RealtimeClient");
8
+ const StorageClient_1 = require("./StorageClient");
8
9
  class FivebitClient {
9
10
  constructor(config) {
10
11
  this._session = null;
11
12
  this.config = config;
12
13
  this.auth = new AuthClient_1.AuthClient(config.url);
13
- this._query = new QueryClient_1.QueryClient(config.url, () => this._session?.token || '');
14
- this._rt = new RealtimeClient_1.RealtimeClient(config.url);
14
+ const getToken = () => this._session?.token || '';
15
+ this._query = new QueryClient_1.QueryClient(config.url, getToken);
16
+ this.storage = new StorageClient_1.StorageClient(config.url, getToken);
17
+ this.realtime = new RealtimeClient_1.RealtimeClient(config.url);
15
18
  }
16
19
  get session() { return this._session; }
17
20
  setSession(s) { this._session = s; }
18
- from(table) {
19
- return this._query.table(table);
20
- }
21
- onChanges(table, fn) {
22
- this._rt.subscribe(table, fn);
23
- }
24
- close() { this._rt.close(); }
21
+ from(table) { return this._query.table(table); }
22
+ onChanges(table, fn) { return this.realtime.subscribe(table, fn); }
23
+ channel(name) { return this.realtime.channel(name); }
24
+ close() { this.realtime.close(); }
25
25
  }
26
26
  exports.FivebitClient = FivebitClient;
27
27
  function createClient(config) {
@@ -11,5 +11,10 @@ export declare class QueryClient {
11
11
  update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
12
12
  delete(id: number): Promise<boolean>;
13
13
  query(field: string, value: string, limit?: number): Promise<T[]>;
14
+ count(filter?: string): Promise<number>;
15
+ sum(field: string, filter?: string): Promise<number>;
16
+ avg(field: string, filter?: string): Promise<number>;
17
+ min(field: string): Promise<number>;
18
+ max(field: string): Promise<number>;
14
19
  };
15
20
  }
@@ -32,9 +32,7 @@ class QueryClient {
32
32
  return { data, error: null, etag: newEtag };
33
33
  },
34
34
  async insert(record) {
35
- const r = await fetch(`${self.baseUrl}/records`, {
36
- method: 'POST', headers: self.headers(), body: JSON.stringify(record),
37
- });
35
+ const r = await fetch(`${self.baseUrl}/records`, { method: 'POST', headers: self.headers(), body: JSON.stringify(record) });
38
36
  const data = await r.json();
39
37
  return { data, error: r.ok ? null : data.error, etag: '' };
40
38
  },
@@ -43,28 +41,51 @@ class QueryClient {
43
41
  const etag = self.etagCache.get(id);
44
42
  if (etag)
45
43
  h['If-Match'] = `"${etag}"`;
46
- const r = await fetch(`${self.baseUrl}/records/${id}`, {
47
- method: 'PUT', headers: h, body: JSON.stringify(record),
48
- });
44
+ const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'PUT', headers: h, body: JSON.stringify(record) });
49
45
  if (r.status === 412)
50
- return { data: null, error: 'Conflict — record modified', etag: '' };
46
+ return { data: null, error: 'Conflict', etag: '' };
51
47
  const data = await r.json();
52
48
  return { data, error: null, etag: '' };
53
49
  },
54
50
  async delete(id) {
55
- const r = await fetch(`${self.baseUrl}/records/${id}`, {
56
- method: 'DELETE', headers: self.headers(),
57
- });
51
+ const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'DELETE', headers: self.headers() });
58
52
  self.etagCache.delete(id);
59
53
  return r.ok;
60
54
  },
61
55
  async query(field, value, limit = 20) {
62
- const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, {
63
- headers: self.headers(),
64
- });
56
+ const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, { headers: self.headers() });
65
57
  const data = await r.json();
66
58
  return data.results || [];
67
59
  },
60
+ // Aggregates
61
+ async count(filter) {
62
+ const q = filter ? `filter=${filter}&aggregate=count` : 'aggregate=count';
63
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
64
+ const d = await r.json();
65
+ return d.count || 0;
66
+ },
67
+ async sum(field, filter) {
68
+ const q = `aggregate=sum:${field}${filter ? '&filter=' + filter : ''}`;
69
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
70
+ const d = await r.json();
71
+ return d[`sum_${field}`] || 0;
72
+ },
73
+ async avg(field, filter) {
74
+ const q = `aggregate=avg:${field}${filter ? '&filter=' + filter : ''}`;
75
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
76
+ const d = await r.json();
77
+ return d[`avg_${field}`] || 0;
78
+ },
79
+ async min(field) {
80
+ const r = await fetch(`${self.baseUrl}/query?aggregate=min:${field}`, { headers: self.headers() });
81
+ const d = await r.json();
82
+ return d[`min_${field}`] || 0;
83
+ },
84
+ async max(field) {
85
+ const r = await fetch(`${self.baseUrl}/query?aggregate=max:${field}`, { headers: self.headers() });
86
+ const d = await r.json();
87
+ return d[`max_${field}`] || 0;
88
+ },
68
89
  };
69
90
  }
70
91
  }
@@ -1,12 +1,22 @@
1
1
  type ChangeHandler = (record: any) => void;
2
+ type MessageHandler = (payload: any) => void;
2
3
  export declare class RealtimeClient {
3
4
  private baseUrl;
4
- private sources;
5
- private handlers;
5
+ private ws;
6
+ private tableHandlers;
7
+ private channelHandlers;
8
+ private presenceCache;
6
9
  private reconnectMs;
10
+ private wsUrl;
7
11
  constructor(baseUrl: string);
8
- subscribe(table: string, fn: ChangeHandler): () => void;
9
12
  private _connect;
13
+ subscribe(table: string, fn: ChangeHandler): () => void;
14
+ channel(name: string): {
15
+ on(fn: MessageHandler): void;
16
+ broadcast(payload: any): void;
17
+ presence(userId: number, userName: string): void;
18
+ readonly users: any[];
19
+ };
10
20
  close(): void;
11
21
  }
12
22
  export {};
@@ -3,40 +3,78 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RealtimeClient = void 0;
4
4
  class RealtimeClient {
5
5
  constructor(baseUrl) {
6
- this.sources = new Map();
7
- this.handlers = new Map();
6
+ this.ws = null;
7
+ this.tableHandlers = new Map();
8
+ this.channelHandlers = new Map();
9
+ this.presenceCache = [];
8
10
  this.reconnectMs = 3000;
9
11
  this.baseUrl = baseUrl;
12
+ this.wsUrl = baseUrl.replace('http', 'ws') + '/ws';
13
+ this._connect();
10
14
  }
11
- subscribe(table, fn) {
12
- if (!this.handlers.has(table)) {
13
- this.handlers.set(table, new Set());
14
- this._connect(table);
15
- }
16
- this.handlers.get(table).add(fn);
17
- return () => this.handlers.get(table)?.delete(fn);
18
- }
19
- _connect(table) {
20
- const url = `${this.baseUrl}/stream/${table}`;
21
- const es = new EventSource(url);
22
- es.onmessage = (e) => {
15
+ _connect() {
16
+ if (typeof WebSocket === 'undefined')
17
+ return;
18
+ this.ws = new WebSocket(this.wsUrl);
19
+ this.ws.onopen = () => {
20
+ // Re-subscribe to tables
21
+ for (const table of this.tableHandlers.keys()) {
22
+ this.ws.send(JSON.stringify({ type: 'subscribe', table }));
23
+ }
24
+ };
25
+ this.ws.onmessage = (e) => {
23
26
  try {
24
- const record = JSON.parse(e.data);
25
- this.handlers.get(table)?.forEach(fn => fn(record));
27
+ const msg = JSON.parse(e.data);
28
+ if (msg.type === 'change') {
29
+ this.tableHandlers.get(msg.table)?.forEach(fn => fn(msg.event));
30
+ }
31
+ else if (msg.type === 'broadcast') {
32
+ this.channelHandlers.get(msg.channel)?.forEach(fn => fn(msg.payload));
33
+ }
34
+ else if (msg.type === 'presence') {
35
+ this.presenceCache = msg.users || [];
36
+ this.channelHandlers.get(`__presence:${msg.channel || ''}`)?.forEach(fn => fn(msg));
37
+ }
26
38
  }
27
39
  catch { }
28
40
  };
29
- es.onerror = () => {
30
- es.close();
31
- setTimeout(() => this._connect(table), this.reconnectMs);
32
- };
33
- this.sources.set(table, es);
41
+ this.ws.onclose = () => setTimeout(() => this._connect(), this.reconnectMs);
34
42
  }
35
- close() {
36
- for (const es of this.sources.values())
37
- es.close();
38
- this.sources.clear();
39
- this.handlers.clear();
43
+ // Table changes (backward compat + new)
44
+ subscribe(table, fn) {
45
+ if (!this.tableHandlers.has(table))
46
+ this.tableHandlers.set(table, new Set());
47
+ this.tableHandlers.get(table).add(fn);
48
+ if (this.ws?.readyState === 1) {
49
+ this.ws.send(JSON.stringify({ type: 'subscribe', table }));
50
+ }
51
+ return () => this.tableHandlers.get(table)?.delete(fn);
52
+ }
53
+ // Channels (new)
54
+ channel(name) {
55
+ const self = this;
56
+ return {
57
+ on(fn) {
58
+ if (!self.channelHandlers.has(name))
59
+ self.channelHandlers.set(name, new Set());
60
+ self.channelHandlers.get(name).add(fn);
61
+ if (self.ws?.readyState === 1) {
62
+ self.ws.send(JSON.stringify({ type: 'channel', channel: name }));
63
+ }
64
+ },
65
+ broadcast(payload) {
66
+ if (self.ws?.readyState === 1) {
67
+ self.ws.send(JSON.stringify({ type: 'broadcast', channel: name, payload }));
68
+ }
69
+ },
70
+ presence(userId, userName) {
71
+ if (self.ws?.readyState === 1) {
72
+ self.ws.send(JSON.stringify({ type: 'presence', userId, name: userName, channel: name }));
73
+ }
74
+ },
75
+ get users() { return self.presenceCache; },
76
+ };
40
77
  }
78
+ close() { this.ws?.close(); }
41
79
  }
42
80
  exports.RealtimeClient = RealtimeClient;
@@ -0,0 +1,23 @@
1
+ export declare class StorageClient {
2
+ private baseUrl;
3
+ private getToken;
4
+ constructor(baseUrl: string, getToken: () => string);
5
+ bucket(name: string): {
6
+ upload(path: string, data: Blob | Buffer | Uint8Array): Promise<{
7
+ path: string;
8
+ hash: string;
9
+ size: number;
10
+ }>;
11
+ download(path: string): Promise<Blob | null>;
12
+ list(opts?: {
13
+ prefix?: string;
14
+ limit?: number;
15
+ }): Promise<{
16
+ path: string;
17
+ size: number;
18
+ hash: string;
19
+ }[]>;
20
+ remove(path: string): Promise<boolean>;
21
+ };
22
+ private _auth;
23
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StorageClient = void 0;
4
+ class StorageClient {
5
+ constructor(baseUrl, getToken) {
6
+ this.baseUrl = baseUrl;
7
+ this.getToken = getToken;
8
+ }
9
+ bucket(name) {
10
+ const self = this;
11
+ return {
12
+ async upload(path, data) {
13
+ const form = new FormData();
14
+ const blob = data instanceof Blob ? data : new Blob([data]);
15
+ form.append('file', blob, path);
16
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, {
17
+ method: 'POST', headers: self._auth(), body: form,
18
+ });
19
+ return r.json();
20
+ },
21
+ async download(path) {
22
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { headers: self._auth() });
23
+ return r.ok ? r.blob() : null;
24
+ },
25
+ async list(opts) {
26
+ const prefix = opts?.prefix || '';
27
+ const limit = opts?.limit || 100;
28
+ const r = await fetch(`${self.baseUrl}/storage/${name}?prefix=${prefix}&limit=${limit}`, { headers: self._auth() });
29
+ const d = await r.json();
30
+ return d.files || [];
31
+ },
32
+ async remove(path) {
33
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { method: 'DELETE', headers: self._auth() });
34
+ return r.ok;
35
+ },
36
+ };
37
+ }
38
+ _auth() {
39
+ const t = this.getToken();
40
+ return t ? { Authorization: `Bearer ${t}` } : {};
41
+ }
42
+ }
43
+ exports.StorageClient = StorageClient;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fivebit-client",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "5bit client SDK — deterministic, content-addressed database",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,7 @@
1
1
  import { AuthClient } from './AuthClient';
2
2
  import { QueryClient } from './QueryClient';
3
3
  import { RealtimeClient } from './RealtimeClient';
4
+ import { StorageClient } from './StorageClient';
4
5
 
5
6
  export interface FivebitConfig { url: string; apiKey?: string; }
6
7
  export interface AuthSession { userId: number; token: string; expiresAt: number; }
@@ -9,30 +10,27 @@ export interface QueryResult<T> { data: T | null; error: string | null; etag: st
9
10
  export class FivebitClient {
10
11
  config: FivebitConfig;
11
12
  auth: AuthClient;
13
+ storage: StorageClient;
14
+ realtime: RealtimeClient;
12
15
  private _query: QueryClient;
13
- private _rt: RealtimeClient;
14
16
  private _session: AuthSession | null = null;
15
17
 
16
18
  constructor(config: FivebitConfig) {
17
19
  this.config = config;
18
20
  this.auth = new AuthClient(config.url);
19
- this._query = new QueryClient(config.url, () => this._session?.token || '');
20
- this._rt = new RealtimeClient(config.url);
21
+ const getToken = () => this._session?.token || '';
22
+ this._query = new QueryClient(config.url, getToken);
23
+ this.storage = new StorageClient(config.url, getToken);
24
+ this.realtime = new RealtimeClient(config.url);
21
25
  }
22
26
 
23
27
  get session(): AuthSession | null { return this._session; }
24
-
25
28
  setSession(s: AuthSession | null) { this._session = s; }
26
29
 
27
- from<T = any>(table: string) {
28
- return this._query.table<T>(table);
29
- }
30
-
31
- onChanges(table: string, fn: (record: any) => void) {
32
- this._rt.subscribe(table, fn);
33
- }
34
-
35
- close() { this._rt.close(); }
30
+ from<T = any>(table: string) { return this._query.table<T>(table); }
31
+ onChanges(table: string, fn: (record: any) => void) { return this.realtime.subscribe(table, fn); }
32
+ channel(name: string) { return this.realtime.channel(name); }
33
+ close() { this.realtime.close(); }
36
34
  }
37
35
 
38
36
  export function createClient(config: FivebitConfig): FivebitClient {
@@ -11,8 +11,7 @@ export class QueryClient {
11
11
 
12
12
  private headers(): Record<string, string> {
13
13
  const h: Record<string, string> = { 'Content-Type': 'application/json' };
14
- const t = this.getToken();
15
- if (t) h['Authorization'] = `Bearer ${t}`;
14
+ const t = this.getToken(); if (t) h['Authorization'] = `Bearer ${t}`;
16
15
  return h;
17
16
  }
18
17
 
@@ -20,51 +19,56 @@ export class QueryClient {
20
19
  const self = this;
21
20
  return {
22
21
  async getById(id: number): Promise<QueryResult<T>> {
23
- const h = self.headers();
24
- const etag = self.etagCache.get(id);
22
+ const h = self.headers(); const etag = self.etagCache.get(id);
25
23
  if (etag) h['If-None-Match'] = `"${etag}"`;
26
24
  const r = await fetch(`${self.baseUrl}/records/${id}`, { headers: h });
27
25
  if (r.status === 304) return { data: null, error: null, etag: etag || '' };
28
- const data = await r.json();
29
- const newEtag = r.headers.get('ETag')?.replace(/"/g, '') || '';
26
+ const data = await r.json(); const newEtag = r.headers.get('ETag')?.replace(/"/g, '') || '';
30
27
  if (newEtag) self.etagCache.set(id, newEtag);
31
28
  return { data, error: null, etag: newEtag };
32
29
  },
33
-
34
30
  async insert(record: Omit<T, '_id' | '_hash'>): Promise<QueryResult<T>> {
35
- const r = await fetch(`${self.baseUrl}/records`, {
36
- method: 'POST', headers: self.headers(), body: JSON.stringify(record),
37
- });
38
- const data = await r.json();
39
- return { data, error: r.ok ? null : data.error, etag: '' };
31
+ const r = await fetch(`${self.baseUrl}/records`, { method: 'POST', headers: self.headers(), body: JSON.stringify(record) });
32
+ const data = await r.json(); return { data, error: r.ok ? null : data.error, etag: '' };
40
33
  },
41
-
42
34
  async update(id: number, record: Partial<T>): Promise<QueryResult<T>> {
43
- const h = self.headers();
44
- const etag = self.etagCache.get(id);
35
+ const h = self.headers(); const etag = self.etagCache.get(id);
45
36
  if (etag) h['If-Match'] = `"${etag}"`;
46
- const r = await fetch(`${self.baseUrl}/records/${id}`, {
47
- method: 'PUT', headers: h, body: JSON.stringify(record),
48
- });
49
- if (r.status === 412) return { data: null, error: 'Conflict — record modified', etag: '' };
50
- const data = await r.json();
51
- return { data, error: null, etag: '' };
37
+ const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'PUT', headers: h, body: JSON.stringify(record) });
38
+ if (r.status === 412) return { data: null, error: 'Conflict', etag: '' };
39
+ const data = await r.json(); return { data, error: null, etag: '' };
52
40
  },
53
-
54
41
  async delete(id: number): Promise<boolean> {
55
- const r = await fetch(`${self.baseUrl}/records/${id}`, {
56
- method: 'DELETE', headers: self.headers(),
57
- });
58
- self.etagCache.delete(id);
59
- return r.ok;
42
+ const r = await fetch(`${self.baseUrl}/records/${id}`, { method: 'DELETE', headers: self.headers() });
43
+ self.etagCache.delete(id); return r.ok;
60
44
  },
61
-
62
45
  async query(field: string, value: string, limit = 20): Promise<T[]> {
63
- const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, {
64
- headers: self.headers(),
65
- });
66
- const data = await r.json();
67
- return data.results || [];
46
+ const r = await fetch(`${self.baseUrl}/records?field=${field}&value=${value}&limit=${limit}`, { headers: self.headers() });
47
+ const data = await r.json(); return data.results || [];
48
+ },
49
+ // Aggregates
50
+ async count(filter?: string): Promise<number> {
51
+ const q = filter ? `filter=${filter}&aggregate=count` : 'aggregate=count';
52
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
53
+ const d = await r.json(); return d.count || 0;
54
+ },
55
+ async sum(field: string, filter?: string): Promise<number> {
56
+ const q = `aggregate=sum:${field}${filter ? '&filter=' + filter : ''}`;
57
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
58
+ const d = await r.json(); return d[`sum_${field}`] || 0;
59
+ },
60
+ async avg(field: string, filter?: string): Promise<number> {
61
+ const q = `aggregate=avg:${field}${filter ? '&filter=' + filter : ''}`;
62
+ const r = await fetch(`${self.baseUrl}/query?${q}`, { headers: self.headers() });
63
+ const d = await r.json(); return d[`avg_${field}`] || 0;
64
+ },
65
+ async min(field: string): Promise<number> {
66
+ const r = await fetch(`${self.baseUrl}/query?aggregate=min:${field}`, { headers: self.headers() });
67
+ const d = await r.json(); return d[`min_${field}`] || 0;
68
+ },
69
+ async max(field: string): Promise<number> {
70
+ const r = await fetch(`${self.baseUrl}/query?aggregate=max:${field}`, { headers: self.headers() });
71
+ const d = await r.json(); return d[`max_${field}`] || 0;
68
72
  },
69
73
  };
70
74
  }
@@ -1,41 +1,80 @@
1
1
  type ChangeHandler = (record: any) => void;
2
+ type MessageHandler = (payload: any) => void;
2
3
 
3
4
  export class RealtimeClient {
4
5
  private baseUrl: string;
5
- private sources: Map<string, EventSource> = new Map();
6
- private handlers: Map<string, Set<ChangeHandler>> = new Map();
6
+ private ws: WebSocket | null = null;
7
+ private tableHandlers: Map<string, Set<ChangeHandler>> = new Map();
8
+ private channelHandlers: Map<string, Set<MessageHandler>> = new Map();
9
+ private presenceCache: any[] = [];
7
10
  private reconnectMs = 3000;
11
+ private wsUrl: string;
8
12
 
9
- constructor(baseUrl: string) { this.baseUrl = baseUrl; }
10
-
11
- subscribe(table: string, fn: ChangeHandler): () => void {
12
- if (!this.handlers.has(table)) {
13
- this.handlers.set(table, new Set());
14
- this._connect(table);
15
- }
16
- this.handlers.get(table)!.add(fn);
17
- return () => this.handlers.get(table)?.delete(fn);
13
+ constructor(baseUrl: string) {
14
+ this.baseUrl = baseUrl;
15
+ this.wsUrl = baseUrl.replace('http', 'ws') + '/ws';
16
+ this._connect();
18
17
  }
19
18
 
20
- private _connect(table: string): void {
21
- const url = `${this.baseUrl}/stream/${table}`;
22
- const es = new EventSource(url);
23
- es.onmessage = (e) => {
19
+ private _connect(): void {
20
+ if (typeof WebSocket === 'undefined') return;
21
+ this.ws = new WebSocket(this.wsUrl);
22
+ this.ws.onopen = () => {
23
+ // Re-subscribe to tables
24
+ for (const table of this.tableHandlers.keys()) {
25
+ this.ws!.send(JSON.stringify({ type: 'subscribe', table }));
26
+ }
27
+ };
28
+ this.ws.onmessage = (e) => {
24
29
  try {
25
- const record = JSON.parse(e.data);
26
- this.handlers.get(table)?.forEach(fn => fn(record));
30
+ const msg = JSON.parse(e.data);
31
+ if (msg.type === 'change') {
32
+ this.tableHandlers.get(msg.table)?.forEach(fn => fn(msg.event));
33
+ } else if (msg.type === 'broadcast') {
34
+ this.channelHandlers.get(msg.channel)?.forEach(fn => fn(msg.payload));
35
+ } else if (msg.type === 'presence') {
36
+ this.presenceCache = msg.users || [];
37
+ this.channelHandlers.get(`__presence:${msg.channel || ''}`)?.forEach(fn => fn(msg));
38
+ }
27
39
  } catch {}
28
40
  };
29
- es.onerror = () => {
30
- es.close();
31
- setTimeout(() => this._connect(table), this.reconnectMs);
32
- };
33
- this.sources.set(table, es);
41
+ this.ws.onclose = () => setTimeout(() => this._connect(), this.reconnectMs);
34
42
  }
35
43
 
36
- close(): void {
37
- for (const es of this.sources.values()) es.close();
38
- this.sources.clear();
39
- this.handlers.clear();
44
+ // Table changes (backward compat + new)
45
+ subscribe(table: string, fn: ChangeHandler): () => void {
46
+ if (!this.tableHandlers.has(table)) this.tableHandlers.set(table, new Set());
47
+ this.tableHandlers.get(table)!.add(fn);
48
+ if (this.ws?.readyState === 1) {
49
+ this.ws.send(JSON.stringify({ type: 'subscribe', table }));
50
+ }
51
+ return () => this.tableHandlers.get(table)?.delete(fn);
40
52
  }
53
+
54
+ // Channels (new)
55
+ channel(name: string) {
56
+ const self = this;
57
+ return {
58
+ on(fn: MessageHandler) {
59
+ if (!self.channelHandlers.has(name)) self.channelHandlers.set(name, new Set());
60
+ self.channelHandlers.get(name)!.add(fn);
61
+ if (self.ws?.readyState === 1) {
62
+ self.ws.send(JSON.stringify({ type: 'channel', channel: name }));
63
+ }
64
+ },
65
+ broadcast(payload: any) {
66
+ if (self.ws?.readyState === 1) {
67
+ self.ws.send(JSON.stringify({ type: 'broadcast', channel: name, payload }));
68
+ }
69
+ },
70
+ presence(userId: number, userName: string) {
71
+ if (self.ws?.readyState === 1) {
72
+ self.ws.send(JSON.stringify({ type: 'presence', userId, name: userName, channel: name }));
73
+ }
74
+ },
75
+ get users(): any[] { return self.presenceCache; },
76
+ };
77
+ }
78
+
79
+ close(): void { this.ws?.close(); }
41
80
  }
@@ -0,0 +1,39 @@
1
+ export class StorageClient {
2
+ private baseUrl: string; private getToken: () => string;
3
+
4
+ constructor(baseUrl: string, getToken: () => string) {
5
+ this.baseUrl = baseUrl; this.getToken = getToken;
6
+ }
7
+
8
+ bucket(name: string) {
9
+ const self = this;
10
+ return {
11
+ async upload(path: string, data: Blob | Buffer | Uint8Array): Promise<{ path: string; hash: string; size: number }> {
12
+ const form = new FormData();
13
+ const blob = data instanceof Blob ? data : new Blob([data as any]);
14
+ form.append('file', blob, path);
15
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, {
16
+ method: 'POST', headers: self._auth(), body: form,
17
+ });
18
+ return r.json();
19
+ },
20
+ async download(path: string): Promise<Blob | null> {
21
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { headers: self._auth() });
22
+ return r.ok ? r.blob() : null;
23
+ },
24
+ async list(opts?: { prefix?: string; limit?: number }): Promise<{ path: string; size: number; hash: string }[]> {
25
+ const prefix = opts?.prefix || ''; const limit = opts?.limit || 100;
26
+ const r = await fetch(`${self.baseUrl}/storage/${name}?prefix=${prefix}&limit=${limit}`, { headers: self._auth() });
27
+ const d = await r.json(); return d.files || [];
28
+ },
29
+ async remove(path: string): Promise<boolean> {
30
+ const r = await fetch(`${self.baseUrl}/storage/${name}/${path}`, { method: 'DELETE', headers: self._auth() });
31
+ return r.ok;
32
+ },
33
+ };
34
+ }
35
+
36
+ private _auth(): Record<string, string> {
37
+ const t = this.getToken(); return t ? { Authorization: `Bearer ${t}` } : {};
38
+ }
39
+ }