fivebit-client 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/AuthClient.d.ts +22 -0
- package/dist/AuthClient.js +36 -0
- package/dist/FivebitClient.d.ts +35 -0
- package/dist/FivebitClient.js +29 -0
- package/dist/QueryClient.d.ts +15 -0
- package/dist/QueryClient.js +71 -0
- package/dist/RealtimeClient.d.ts +12 -0
- package/dist/RealtimeClient.js +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/package.json +15 -0
- package/src/AuthClient.ts +37 -0
- package/src/FivebitClient.ts +40 -0
- package/src/QueryClient.ts +71 -0
- package/src/RealtimeClient.ts +41 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface AuthSession {
|
|
2
|
+
userId: number;
|
|
3
|
+
token: string;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export declare class AuthClient {
|
|
7
|
+
private baseUrl;
|
|
8
|
+
private _token;
|
|
9
|
+
constructor(baseUrl: string);
|
|
10
|
+
signUp(email: string, password: string, name?: string): Promise<{
|
|
11
|
+
userId: number;
|
|
12
|
+
} | {
|
|
13
|
+
error: string;
|
|
14
|
+
}>;
|
|
15
|
+
signIn(email: string, password: string): Promise<{
|
|
16
|
+
session: AuthSession;
|
|
17
|
+
} | {
|
|
18
|
+
error: string;
|
|
19
|
+
}>;
|
|
20
|
+
signOut(): Promise<void>;
|
|
21
|
+
get token(): string | null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthClient = void 0;
|
|
4
|
+
class AuthClient {
|
|
5
|
+
constructor(baseUrl) {
|
|
6
|
+
this._token = null;
|
|
7
|
+
this.baseUrl = baseUrl;
|
|
8
|
+
}
|
|
9
|
+
async signUp(email, password, name) {
|
|
10
|
+
const r = await fetch(`${this.baseUrl}/api/auth/signup`, {
|
|
11
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({ email, password, name }),
|
|
13
|
+
});
|
|
14
|
+
return r.json();
|
|
15
|
+
}
|
|
16
|
+
async signIn(email, password) {
|
|
17
|
+
const r = await fetch(`${this.baseUrl}/api/auth/login`, {
|
|
18
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({ email, password }),
|
|
20
|
+
});
|
|
21
|
+
const data = await r.json();
|
|
22
|
+
if ('session' in data)
|
|
23
|
+
this._token = data.session.token;
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
async signOut() {
|
|
27
|
+
if (this._token) {
|
|
28
|
+
await fetch(`${this.baseUrl}/api/auth/logout`, {
|
|
29
|
+
method: 'POST', headers: { 'Authorization': `Bearer ${this._token}` },
|
|
30
|
+
});
|
|
31
|
+
this._token = null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
get token() { return this._token; }
|
|
35
|
+
}
|
|
36
|
+
exports.AuthClient = AuthClient;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AuthClient } from './AuthClient';
|
|
2
|
+
export interface FivebitConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AuthSession {
|
|
7
|
+
userId: number;
|
|
8
|
+
token: string;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
}
|
|
11
|
+
export interface QueryResult<T> {
|
|
12
|
+
data: T | null;
|
|
13
|
+
error: string | null;
|
|
14
|
+
etag: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class FivebitClient {
|
|
17
|
+
config: FivebitConfig;
|
|
18
|
+
auth: AuthClient;
|
|
19
|
+
private _query;
|
|
20
|
+
private _rt;
|
|
21
|
+
private _session;
|
|
22
|
+
constructor(config: FivebitConfig);
|
|
23
|
+
get session(): AuthSession | null;
|
|
24
|
+
setSession(s: AuthSession | null): void;
|
|
25
|
+
from<T = any>(table: string): {
|
|
26
|
+
getById(id: number): Promise<QueryResult<T>>;
|
|
27
|
+
insert(record: Omit<T, "_id" | "_hash">): Promise<QueryResult<T>>;
|
|
28
|
+
update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
|
|
29
|
+
delete(id: number): Promise<boolean>;
|
|
30
|
+
query(field: string, value: string, limit?: number): Promise<T[]>;
|
|
31
|
+
};
|
|
32
|
+
onChanges(table: string, fn: (record: any) => void): void;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function createClient(config: FivebitConfig): FivebitClient;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FivebitClient = void 0;
|
|
4
|
+
exports.createClient = createClient;
|
|
5
|
+
const AuthClient_1 = require("./AuthClient");
|
|
6
|
+
const QueryClient_1 = require("./QueryClient");
|
|
7
|
+
const RealtimeClient_1 = require("./RealtimeClient");
|
|
8
|
+
class FivebitClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this._session = null;
|
|
11
|
+
this.config = config;
|
|
12
|
+
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);
|
|
15
|
+
}
|
|
16
|
+
get session() { return this._session; }
|
|
17
|
+
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(); }
|
|
25
|
+
}
|
|
26
|
+
exports.FivebitClient = FivebitClient;
|
|
27
|
+
function createClient(config) {
|
|
28
|
+
return new FivebitClient(config);
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { QueryResult } from './FivebitClient';
|
|
2
|
+
export declare class QueryClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private getToken;
|
|
5
|
+
private etagCache;
|
|
6
|
+
constructor(baseUrl: string, getToken: () => string);
|
|
7
|
+
private headers;
|
|
8
|
+
table<T = any>(name: string): {
|
|
9
|
+
getById(id: number): Promise<QueryResult<T>>;
|
|
10
|
+
insert(record: Omit<T, "_id" | "_hash">): Promise<QueryResult<T>>;
|
|
11
|
+
update(id: number, record: Partial<T>): Promise<QueryResult<T>>;
|
|
12
|
+
delete(id: number): Promise<boolean>;
|
|
13
|
+
query(field: string, value: string, limit?: number): Promise<T[]>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QueryClient = void 0;
|
|
4
|
+
class QueryClient {
|
|
5
|
+
constructor(baseUrl, getToken) {
|
|
6
|
+
this.etagCache = new Map();
|
|
7
|
+
this.baseUrl = baseUrl;
|
|
8
|
+
this.getToken = getToken;
|
|
9
|
+
}
|
|
10
|
+
headers() {
|
|
11
|
+
const h = { 'Content-Type': 'application/json' };
|
|
12
|
+
const t = this.getToken();
|
|
13
|
+
if (t)
|
|
14
|
+
h['Authorization'] = `Bearer ${t}`;
|
|
15
|
+
return h;
|
|
16
|
+
}
|
|
17
|
+
table(name) {
|
|
18
|
+
const self = this;
|
|
19
|
+
return {
|
|
20
|
+
async getById(id) {
|
|
21
|
+
const h = self.headers();
|
|
22
|
+
const etag = self.etagCache.get(id);
|
|
23
|
+
if (etag)
|
|
24
|
+
h['If-None-Match'] = `"${etag}"`;
|
|
25
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { headers: h });
|
|
26
|
+
if (r.status === 304)
|
|
27
|
+
return { data: null, error: null, etag: etag || '' };
|
|
28
|
+
const data = await r.json();
|
|
29
|
+
const newEtag = r.headers.get('ETag')?.replace(/"/g, '') || '';
|
|
30
|
+
if (newEtag)
|
|
31
|
+
self.etagCache.set(id, newEtag);
|
|
32
|
+
return { data, error: null, etag: newEtag };
|
|
33
|
+
},
|
|
34
|
+
async insert(record) {
|
|
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: '' };
|
|
40
|
+
},
|
|
41
|
+
async update(id, record) {
|
|
42
|
+
const h = self.headers();
|
|
43
|
+
const etag = self.etagCache.get(id);
|
|
44
|
+
if (etag)
|
|
45
|
+
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)
|
|
50
|
+
return { data: null, error: 'Conflict — record modified', etag: '' };
|
|
51
|
+
const data = await r.json();
|
|
52
|
+
return { data, error: null, etag: '' };
|
|
53
|
+
},
|
|
54
|
+
async delete(id) {
|
|
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;
|
|
60
|
+
},
|
|
61
|
+
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
|
+
});
|
|
65
|
+
const data = await r.json();
|
|
66
|
+
return data.results || [];
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.QueryClient = QueryClient;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type ChangeHandler = (record: any) => void;
|
|
2
|
+
export declare class RealtimeClient {
|
|
3
|
+
private baseUrl;
|
|
4
|
+
private sources;
|
|
5
|
+
private handlers;
|
|
6
|
+
private reconnectMs;
|
|
7
|
+
constructor(baseUrl: string);
|
|
8
|
+
subscribe(table: string, fn: ChangeHandler): () => void;
|
|
9
|
+
private _connect;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RealtimeClient = void 0;
|
|
4
|
+
class RealtimeClient {
|
|
5
|
+
constructor(baseUrl) {
|
|
6
|
+
this.sources = new Map();
|
|
7
|
+
this.handlers = new Map();
|
|
8
|
+
this.reconnectMs = 3000;
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
}
|
|
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) => {
|
|
23
|
+
try {
|
|
24
|
+
const record = JSON.parse(e.data);
|
|
25
|
+
this.handlers.get(table)?.forEach(fn => fn(record));
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
};
|
|
29
|
+
es.onerror = () => {
|
|
30
|
+
es.close();
|
|
31
|
+
setTimeout(() => this._connect(table), this.reconnectMs);
|
|
32
|
+
};
|
|
33
|
+
this.sources.set(table, es);
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
for (const es of this.sources.values())
|
|
37
|
+
es.close();
|
|
38
|
+
this.sources.clear();
|
|
39
|
+
this.handlers.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.RealtimeClient = RealtimeClient;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FivebitClient = exports.createClient = void 0;
|
|
4
|
+
var FivebitClient_1 = require("./FivebitClient");
|
|
5
|
+
Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return FivebitClient_1.createClient; } });
|
|
6
|
+
Object.defineProperty(exports, "FivebitClient", { enumerable: true, get: function () { return FivebitClient_1.FivebitClient; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fivebit-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "5bit client SDK — deterministic, content-addressed database",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist", "src"],
|
|
8
|
+
"repository": { "type": "git", "url": "https://github.com/Humansales-AI/5bit" },
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": ["5bit", "database", "deterministic", "content-addressed", "realtime"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"prepublishOnly": "tsc"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface AuthSession { userId: number; token: string; expiresAt: number; }
|
|
2
|
+
|
|
3
|
+
export class AuthClient {
|
|
4
|
+
private baseUrl: string;
|
|
5
|
+
private _token: string | null = null;
|
|
6
|
+
|
|
7
|
+
constructor(baseUrl: string) { this.baseUrl = baseUrl; }
|
|
8
|
+
|
|
9
|
+
async signUp(email: string, password: string, name?: string): Promise<{ userId: number } | { error: string }> {
|
|
10
|
+
const r = await fetch(`${this.baseUrl}/api/auth/signup`, {
|
|
11
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({ email, password, name }),
|
|
13
|
+
});
|
|
14
|
+
return r.json();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async signIn(email: string, password: string): Promise<{ session: AuthSession } | { error: string }> {
|
|
18
|
+
const r = await fetch(`${this.baseUrl}/api/auth/login`, {
|
|
19
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ email, password }),
|
|
21
|
+
});
|
|
22
|
+
const data = await r.json();
|
|
23
|
+
if ('session' in data) this._token = data.session.token;
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async signOut(): Promise<void> {
|
|
28
|
+
if (this._token) {
|
|
29
|
+
await fetch(`${this.baseUrl}/api/auth/logout`, {
|
|
30
|
+
method: 'POST', headers: { 'Authorization': `Bearer ${this._token}` },
|
|
31
|
+
});
|
|
32
|
+
this._token = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get token(): string | null { return this._token; }
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AuthClient } from './AuthClient';
|
|
2
|
+
import { QueryClient } from './QueryClient';
|
|
3
|
+
import { RealtimeClient } from './RealtimeClient';
|
|
4
|
+
|
|
5
|
+
export interface FivebitConfig { url: string; apiKey?: string; }
|
|
6
|
+
export interface AuthSession { userId: number; token: string; expiresAt: number; }
|
|
7
|
+
export interface QueryResult<T> { data: T | null; error: string | null; etag: string; }
|
|
8
|
+
|
|
9
|
+
export class FivebitClient {
|
|
10
|
+
config: FivebitConfig;
|
|
11
|
+
auth: AuthClient;
|
|
12
|
+
private _query: QueryClient;
|
|
13
|
+
private _rt: RealtimeClient;
|
|
14
|
+
private _session: AuthSession | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(config: FivebitConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.auth = new AuthClient(config.url);
|
|
19
|
+
this._query = new QueryClient(config.url, () => this._session?.token || '');
|
|
20
|
+
this._rt = new RealtimeClient(config.url);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get session(): AuthSession | null { return this._session; }
|
|
24
|
+
|
|
25
|
+
setSession(s: AuthSession | null) { this._session = s; }
|
|
26
|
+
|
|
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(); }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createClient(config: FivebitConfig): FivebitClient {
|
|
39
|
+
return new FivebitClient(config);
|
|
40
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { QueryResult } from './FivebitClient';
|
|
2
|
+
|
|
3
|
+
export class QueryClient {
|
|
4
|
+
private baseUrl: string;
|
|
5
|
+
private getToken: () => string;
|
|
6
|
+
private etagCache: Map<number, string> = new Map();
|
|
7
|
+
|
|
8
|
+
constructor(baseUrl: string, getToken: () => string) {
|
|
9
|
+
this.baseUrl = baseUrl; this.getToken = getToken;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private headers(): Record<string, string> {
|
|
13
|
+
const h: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
14
|
+
const t = this.getToken();
|
|
15
|
+
if (t) h['Authorization'] = `Bearer ${t}`;
|
|
16
|
+
return h;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
table<T = any>(name: string) {
|
|
20
|
+
const self = this;
|
|
21
|
+
return {
|
|
22
|
+
async getById(id: number): Promise<QueryResult<T>> {
|
|
23
|
+
const h = self.headers();
|
|
24
|
+
const etag = self.etagCache.get(id);
|
|
25
|
+
if (etag) h['If-None-Match'] = `"${etag}"`;
|
|
26
|
+
const r = await fetch(`${self.baseUrl}/records/${id}`, { headers: h });
|
|
27
|
+
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, '') || '';
|
|
30
|
+
if (newEtag) self.etagCache.set(id, newEtag);
|
|
31
|
+
return { data, error: null, etag: newEtag };
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
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: '' };
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async update(id: number, record: Partial<T>): Promise<QueryResult<T>> {
|
|
43
|
+
const h = self.headers();
|
|
44
|
+
const etag = self.etagCache.get(id);
|
|
45
|
+
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: '' };
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
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;
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
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 || [];
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type ChangeHandler = (record: any) => void;
|
|
2
|
+
|
|
3
|
+
export class RealtimeClient {
|
|
4
|
+
private baseUrl: string;
|
|
5
|
+
private sources: Map<string, EventSource> = new Map();
|
|
6
|
+
private handlers: Map<string, Set<ChangeHandler>> = new Map();
|
|
7
|
+
private reconnectMs = 3000;
|
|
8
|
+
|
|
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);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private _connect(table: string): void {
|
|
21
|
+
const url = `${this.baseUrl}/stream/${table}`;
|
|
22
|
+
const es = new EventSource(url);
|
|
23
|
+
es.onmessage = (e) => {
|
|
24
|
+
try {
|
|
25
|
+
const record = JSON.parse(e.data);
|
|
26
|
+
this.handlers.get(table)?.forEach(fn => fn(record));
|
|
27
|
+
} catch {}
|
|
28
|
+
};
|
|
29
|
+
es.onerror = () => {
|
|
30
|
+
es.close();
|
|
31
|
+
setTimeout(() => this._connect(table), this.reconnectMs);
|
|
32
|
+
};
|
|
33
|
+
this.sources.set(table, es);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
close(): void {
|
|
37
|
+
for (const es of this.sources.values()) es.close();
|
|
38
|
+
this.sources.clear();
|
|
39
|
+
this.handlers.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
ADDED