reflectdb 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/LICENSE +21 -0
- package/README.md +1260 -0
- package/dist/cjs/client/index.cjs +1252 -0
- package/dist/cjs/client/index.d.cts +629 -0
- package/dist/cjs/client/storage/indexeddb.cjs +253 -0
- package/dist/cjs/client/storage/indexeddb.d.cts +85 -0
- package/dist/cjs/core/index.cjs +202 -0
- package/dist/cjs/core/index.d.cts +473 -0
- package/dist/cjs/react/index.cjs +1512 -0
- package/dist/cjs/react/index.d.cts +748 -0
- package/dist/cjs/server/drizzle.cjs +413 -0
- package/dist/cjs/server/drizzle.d.cts +233 -0
- package/dist/cjs/server/index.cjs +4486 -0
- package/dist/cjs/server/index.d.cts +1988 -0
- package/dist/cjs/svelte/index.cjs +1414 -0
- package/dist/cjs/svelte/index.d.cts +645 -0
- package/dist/cjs/transport/bun-ws.cjs +244 -0
- package/dist/cjs/transport/bun-ws.d.cts +215 -0
- package/dist/cjs/transport/polling.cjs +285 -0
- package/dist/cjs/transport/polling.d.cts +210 -0
- package/dist/cjs/transport/sse.cjs +312 -0
- package/dist/cjs/transport/sse.d.cts +205 -0
- package/dist/cjs/transport/ws.cjs +330 -0
- package/dist/cjs/transport/ws.d.cts +236 -0
- package/dist/cjs/vanilla/index.cjs +1430 -0
- package/dist/cjs/vanilla/index.d.cts +657 -0
- package/dist/client/index.d.ts +629 -0
- package/dist/client/index.js +106 -0
- package/dist/client/storage/indexeddb.d.ts +85 -0
- package/dist/client/storage/indexeddb.js +213 -0
- package/dist/core/index.d.ts +473 -0
- package/dist/core/index.js +56 -0
- package/dist/react/index.d.ts +748 -0
- package/dist/react/index.js +366 -0
- package/dist/server/drizzle.d.ts +233 -0
- package/dist/server/drizzle.js +9 -0
- package/dist/server/index.d.ts +1988 -0
- package/dist/server/index.js +3959 -0
- package/dist/shared/esm-3tkwvysa.js +54 -0
- package/dist/shared/esm-b7xs9cde.js +4 -0
- package/dist/shared/esm-rw7jjtrv.js +58 -0
- package/dist/shared/esm-wkwx6bd9.js +25 -0
- package/dist/shared/esm-ytrd3hbq.js +1007 -0
- package/dist/shared/esm-z1xse19c.js +369 -0
- package/dist/svelte/index.d.ts +645 -0
- package/dist/svelte/index.js +262 -0
- package/dist/transport/bun-ws.d.ts +215 -0
- package/dist/transport/bun-ws.js +140 -0
- package/dist/transport/polling.d.ts +210 -0
- package/dist/transport/polling.js +181 -0
- package/dist/transport/sse.d.ts +205 -0
- package/dist/transport/sse.js +208 -0
- package/dist/transport/ws.d.ts +236 -0
- package/dist/transport/ws.js +226 -0
- package/dist/vanilla/index.d.ts +657 -0
- package/dist/vanilla/index.js +278 -0
- package/package.json +253 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
type OpType = "insert" | "update" | "delete";
|
|
2
|
+
type ErrorReason = "outside_shape" | "readonly_query" | "rate_limited" | "buffer_full" | "server_conflict" | "custom_conflict" | "merge_stale" | "clock_drift" | "replay" | "batch_too_large" | "schema_outdated" | "auth_revoked" | "compacted" | "mutation_rejected" | "server_error" | "unknown_query";
|
|
3
|
+
interface HelloMessage {
|
|
4
|
+
type: "hello";
|
|
5
|
+
/** Highest protocol version the client prefers. Retained for back-compat. */
|
|
6
|
+
protocolVersion: number;
|
|
7
|
+
/** All versions the client can speak. Omit for legacy single-version clients. */
|
|
8
|
+
supportedVersions?: readonly number[];
|
|
9
|
+
clientId: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
interface BootstrapMessage {
|
|
13
|
+
type: "bootstrap";
|
|
14
|
+
syncParams?: Record<string, Record<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
interface ResumeMessage {
|
|
17
|
+
type: "resume";
|
|
18
|
+
since: string;
|
|
19
|
+
}
|
|
20
|
+
interface OpsMessage {
|
|
21
|
+
type: "ops";
|
|
22
|
+
ops: ClientOp[];
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
25
|
+
interface ClientOp {
|
|
26
|
+
id: string;
|
|
27
|
+
table: string;
|
|
28
|
+
op: OpType;
|
|
29
|
+
rowId: string;
|
|
30
|
+
payload: Record<string, unknown> | null;
|
|
31
|
+
hlc: string;
|
|
32
|
+
batchId?: string;
|
|
33
|
+
batchSize?: number;
|
|
34
|
+
batchSeq?: number;
|
|
35
|
+
}
|
|
36
|
+
interface SyncDeclareMessage {
|
|
37
|
+
type: "sync_declare";
|
|
38
|
+
table: string;
|
|
39
|
+
params?: Record<string, unknown>;
|
|
40
|
+
window?: number;
|
|
41
|
+
}
|
|
42
|
+
interface LoadMoreMessage {
|
|
43
|
+
type: "load_more";
|
|
44
|
+
table: string;
|
|
45
|
+
count: number;
|
|
46
|
+
}
|
|
47
|
+
interface UnsyncMessage {
|
|
48
|
+
type: "unsync";
|
|
49
|
+
table: string;
|
|
50
|
+
}
|
|
51
|
+
interface AuthMessage {
|
|
52
|
+
type: "auth";
|
|
53
|
+
token: string;
|
|
54
|
+
}
|
|
55
|
+
interface EphemeralMessage {
|
|
56
|
+
type: "ephemeral";
|
|
57
|
+
key: string;
|
|
58
|
+
userId: string;
|
|
59
|
+
data: Record<string, unknown>;
|
|
60
|
+
ttlMs?: number;
|
|
61
|
+
}
|
|
62
|
+
interface HelloAckMessage {
|
|
63
|
+
type: "hello_ack";
|
|
64
|
+
protocolVersion: number;
|
|
65
|
+
serverId: string;
|
|
66
|
+
}
|
|
67
|
+
interface HelloRejectMessage {
|
|
68
|
+
type: "hello_reject";
|
|
69
|
+
reason: string;
|
|
70
|
+
supported: number[];
|
|
71
|
+
}
|
|
72
|
+
interface SnapshotMessage {
|
|
73
|
+
type: "snapshot";
|
|
74
|
+
table: string;
|
|
75
|
+
rows: Record<string, unknown>[];
|
|
76
|
+
colClocks: Record<string, Record<string, string>>;
|
|
77
|
+
append?: boolean;
|
|
78
|
+
totalCount?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Primary key column name for this table. Self-contained per message so the
|
|
81
|
+
* client doesn't depend on `bootstrap_complete.tableMeta` arriving first.
|
|
82
|
+
* When omitted (older servers), the client falls back to tableMeta, then "id".
|
|
83
|
+
*/
|
|
84
|
+
pk?: string;
|
|
85
|
+
}
|
|
86
|
+
interface BootstrapCompleteMessage {
|
|
87
|
+
type: "bootstrap_complete";
|
|
88
|
+
serverHlc: string;
|
|
89
|
+
tableMeta: Record<string, TableMeta>;
|
|
90
|
+
}
|
|
91
|
+
interface TableMeta {
|
|
92
|
+
serverSet: string[];
|
|
93
|
+
readonly: string[];
|
|
94
|
+
broadcast: "consistent" | "eager" | "eager-durable";
|
|
95
|
+
/** Primary key column name. Defaults to "id" when omitted (older servers). */
|
|
96
|
+
pk?: string;
|
|
97
|
+
}
|
|
98
|
+
interface DeltaMessage {
|
|
99
|
+
type: "delta";
|
|
100
|
+
table: string;
|
|
101
|
+
op: OpType;
|
|
102
|
+
rowId: string;
|
|
103
|
+
payload: Record<string, unknown> | null;
|
|
104
|
+
hlc: string;
|
|
105
|
+
colClocks?: Record<string, string>;
|
|
106
|
+
}
|
|
107
|
+
interface AckMessage {
|
|
108
|
+
type: "ack";
|
|
109
|
+
opIds: string[];
|
|
110
|
+
}
|
|
111
|
+
interface RejectMessage {
|
|
112
|
+
type: "reject";
|
|
113
|
+
opId?: string;
|
|
114
|
+
batchId?: string;
|
|
115
|
+
reason: ErrorReason;
|
|
116
|
+
serverRow?: Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
interface ResumeCompleteMessage {
|
|
119
|
+
type: "resume_complete";
|
|
120
|
+
serverHlc: string;
|
|
121
|
+
}
|
|
122
|
+
interface ResumeRejectedMessage {
|
|
123
|
+
type: "resume_rejected";
|
|
124
|
+
reason: string;
|
|
125
|
+
serverHlc: string;
|
|
126
|
+
}
|
|
127
|
+
interface ShapeChangedMessage {
|
|
128
|
+
type: "shape_changed";
|
|
129
|
+
table: string;
|
|
130
|
+
reason: "auth_changed" | "params_changed" | "server_policy";
|
|
131
|
+
}
|
|
132
|
+
interface DisconnectMessage {
|
|
133
|
+
type: "disconnect";
|
|
134
|
+
reason: string;
|
|
135
|
+
}
|
|
136
|
+
interface ReauthMessage {
|
|
137
|
+
type: "reauth";
|
|
138
|
+
}
|
|
139
|
+
interface CountChangedMessage {
|
|
140
|
+
type: "count_changed";
|
|
141
|
+
table: string;
|
|
142
|
+
totalCount: number;
|
|
143
|
+
}
|
|
144
|
+
interface EphemeralEvent {
|
|
145
|
+
type: "ephemeral";
|
|
146
|
+
key: string;
|
|
147
|
+
clientId: string;
|
|
148
|
+
userId: string;
|
|
149
|
+
data: Record<string, unknown>;
|
|
150
|
+
ttlMs?: number;
|
|
151
|
+
}
|
|
152
|
+
type ClientMessage = HelloMessage | BootstrapMessage | ResumeMessage | OpsMessage | SyncDeclareMessage | LoadMoreMessage | UnsyncMessage | AuthMessage | EphemeralMessage;
|
|
153
|
+
type ServerMessage = HelloAckMessage | HelloRejectMessage | SnapshotMessage | BootstrapCompleteMessage | DeltaMessage | AckMessage | RejectMessage | ResumeCompleteMessage | ResumeRejectedMessage | ShapeChangedMessage | DisconnectMessage | ReauthMessage | EphemeralEvent | CountChangedMessage;
|
|
154
|
+
interface ServerTransport {
|
|
155
|
+
/**
|
|
156
|
+
* Deliver a message to one client.
|
|
157
|
+
*
|
|
158
|
+
* MUST reject (ideally with `TransportSendError`) when the frame could not
|
|
159
|
+
* be handed to the peer — unknown/closed socket, full outbound queue, or a
|
|
160
|
+
* throw from the underlying socket. Resolving on a dropped frame makes the
|
|
161
|
+
* broadcast engine commit result-cache state the client never received.
|
|
162
|
+
*/
|
|
163
|
+
send(clientId: string, message: ServerMessage): Promise<void>;
|
|
164
|
+
broadcast(roomId: string, message: ServerMessage, exclude?: string): Promise<void>;
|
|
165
|
+
onMessage(handler: (clientId: string, message: ClientMessage) => void): void;
|
|
166
|
+
onConnect(handler: (clientId: string, req: Request) => void): void;
|
|
167
|
+
onDisconnect(handler: (clientId: string) => void): void;
|
|
168
|
+
close(): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Force-disconnect a single client. Optional — if absent, the handler relies
|
|
171
|
+
* on the client honoring a `disconnect` server-message. Required for any
|
|
172
|
+
* transport that carries authentication state, since an adversarial client
|
|
173
|
+
* can otherwise ignore the message and continue using a revoked session.
|
|
174
|
+
*/
|
|
175
|
+
disconnect?(clientId: string): Promise<void> | void;
|
|
176
|
+
}
|
|
177
|
+
interface ClientTransport {
|
|
178
|
+
send(message: ClientMessage): Promise<void>;
|
|
179
|
+
subscribe(handler: (message: ServerMessage) => void): void;
|
|
180
|
+
close(): Promise<void>;
|
|
181
|
+
}
|
|
182
|
+
interface PollingServerConfig {
|
|
183
|
+
/** Max queued messages per client. Older messages dropped when exceeded. Default: 1000. */
|
|
184
|
+
maxQueueLen?: number;
|
|
185
|
+
/** Idle timeout in ms — clients with no poll in this window are reaped. Default: 60_000. */
|
|
186
|
+
idleTimeoutMs?: number;
|
|
187
|
+
/** Reaper interval in ms. Default: 30_000. */
|
|
188
|
+
reaperIntervalMs?: number;
|
|
189
|
+
/** Max inbound JSON message bytes. Default: 1 MB. */
|
|
190
|
+
maxMessageBytes?: number;
|
|
191
|
+
}
|
|
192
|
+
/** Caller validates body size against this before parsing JSON; exported for convenience. */
|
|
193
|
+
declare function pollingBodyTooLarge(body: string, limit?: number): boolean;
|
|
194
|
+
declare function createPollingServerTransport(cfg?: PollingServerConfig): ServerTransport & {
|
|
195
|
+
handleConnect(clientId: string): void;
|
|
196
|
+
handleMessage(clientId: string, message: ClientMessage): void;
|
|
197
|
+
handleDisconnect(clientId: string): void;
|
|
198
|
+
handlePoll(clientId: string): ServerMessage[];
|
|
199
|
+
};
|
|
200
|
+
interface PollingClientConfig {
|
|
201
|
+
/** GET endpoint that returns `{ messages: ServerMessage[] }` */
|
|
202
|
+
pollUrl: string;
|
|
203
|
+
/** POST endpoint for sending client messages */
|
|
204
|
+
messageUrl: string;
|
|
205
|
+
/** Poll interval in ms (default: 1000) */
|
|
206
|
+
interval?: number;
|
|
207
|
+
headers?: Record<string, string>;
|
|
208
|
+
}
|
|
209
|
+
declare function createPollingClientTransport(config: PollingClientConfig): ClientTransport;
|
|
210
|
+
export { pollingBodyTooLarge, createPollingServerTransport, createPollingClientTransport, PollingServerConfig, PollingClientConfig };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TransportSendError
|
|
3
|
+
} from "../shared/esm-3tkwvysa.js";
|
|
4
|
+
import"../shared/esm-b7xs9cde.js";
|
|
5
|
+
|
|
6
|
+
// src/transport/polling.ts
|
|
7
|
+
function pollingBodyTooLarge(body, limit = 1e6) {
|
|
8
|
+
return body.length > limit;
|
|
9
|
+
}
|
|
10
|
+
function createPollingServerTransport(cfg = {}) {
|
|
11
|
+
const queues = new Map;
|
|
12
|
+
const lastPoll = new Map;
|
|
13
|
+
const maxQueueLen = cfg.maxQueueLen ?? 1000;
|
|
14
|
+
const idleTimeoutMs = cfg.idleTimeoutMs ?? 60000;
|
|
15
|
+
const reaperIntervalMs = cfg.reaperIntervalMs ?? 30000;
|
|
16
|
+
const maxMessageBytes = cfg.maxMessageBytes ?? 1e6;
|
|
17
|
+
let messageHandler = null;
|
|
18
|
+
let connectHandler = null;
|
|
19
|
+
let disconnectHandler = null;
|
|
20
|
+
const reaper = setInterval(() => {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
for (const [clientId, last] of lastPoll) {
|
|
23
|
+
if (now - last > idleTimeoutMs) {
|
|
24
|
+
queues.delete(clientId);
|
|
25
|
+
lastPoll.delete(clientId);
|
|
26
|
+
disconnectHandler?.(clientId);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}, reaperIntervalMs);
|
|
30
|
+
if (typeof reaper.unref === "function") {
|
|
31
|
+
reaper.unref();
|
|
32
|
+
}
|
|
33
|
+
function enqueue(queue, message) {
|
|
34
|
+
queue.push(message);
|
|
35
|
+
let dropped = false;
|
|
36
|
+
while (queue.length > maxQueueLen) {
|
|
37
|
+
queue.shift();
|
|
38
|
+
dropped = true;
|
|
39
|
+
}
|
|
40
|
+
return !dropped;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
handleConnect(clientId) {
|
|
44
|
+
if (!queues.has(clientId)) {
|
|
45
|
+
queues.set(clientId, []);
|
|
46
|
+
}
|
|
47
|
+
lastPoll.set(clientId, Date.now());
|
|
48
|
+
connectHandler?.(clientId, new Request("https://poll-connect"));
|
|
49
|
+
},
|
|
50
|
+
handleMessage(clientId, message) {
|
|
51
|
+
lastPoll.set(clientId, Date.now());
|
|
52
|
+
let byteLen;
|
|
53
|
+
try {
|
|
54
|
+
const stringified = JSON.stringify(message);
|
|
55
|
+
byteLen = typeof Buffer !== "undefined" ? Buffer.byteLength(stringified, "utf8") : new Blob([stringified]).size;
|
|
56
|
+
} catch {
|
|
57
|
+
byteLen = 0;
|
|
58
|
+
}
|
|
59
|
+
if (byteLen > maxMessageBytes) {
|
|
60
|
+
console.warn(`[reflectdb] polling: message from ${clientId} exceeds ${maxMessageBytes} bytes (${byteLen}), dropping`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
messageHandler?.(clientId, message);
|
|
64
|
+
},
|
|
65
|
+
handleDisconnect(clientId) {
|
|
66
|
+
queues.delete(clientId);
|
|
67
|
+
lastPoll.delete(clientId);
|
|
68
|
+
disconnectHandler?.(clientId);
|
|
69
|
+
},
|
|
70
|
+
handlePoll(clientId) {
|
|
71
|
+
lastPoll.set(clientId, Date.now());
|
|
72
|
+
const queue = queues.get(clientId);
|
|
73
|
+
if (!queue || queue.length === 0)
|
|
74
|
+
return [];
|
|
75
|
+
const messages = [...queue];
|
|
76
|
+
queue.length = 0;
|
|
77
|
+
return messages;
|
|
78
|
+
},
|
|
79
|
+
async send(clientId, message) {
|
|
80
|
+
const queue = queues.get(clientId);
|
|
81
|
+
if (!queue) {
|
|
82
|
+
throw new TransportSendError(clientId, "no queue registered for client");
|
|
83
|
+
}
|
|
84
|
+
if (!enqueue(queue, message)) {
|
|
85
|
+
throw new TransportSendError(clientId, `polling queue overflow (max ${maxQueueLen}); oldest message dropped undelivered`);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async broadcast(_roomId, message, exclude) {
|
|
89
|
+
for (const [clientId, queue] of queues) {
|
|
90
|
+
if (clientId !== exclude) {
|
|
91
|
+
enqueue(queue, message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
onMessage(handler) {
|
|
96
|
+
messageHandler = handler;
|
|
97
|
+
},
|
|
98
|
+
onConnect(handler) {
|
|
99
|
+
connectHandler = handler;
|
|
100
|
+
},
|
|
101
|
+
onDisconnect(handler) {
|
|
102
|
+
disconnectHandler = handler;
|
|
103
|
+
},
|
|
104
|
+
async close() {
|
|
105
|
+
clearInterval(reaper);
|
|
106
|
+
queues.clear();
|
|
107
|
+
lastPoll.clear();
|
|
108
|
+
},
|
|
109
|
+
disconnect(clientId) {
|
|
110
|
+
queues.delete(clientId);
|
|
111
|
+
lastPoll.delete(clientId);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function createPollingClientTransport(config) {
|
|
116
|
+
let handler = null;
|
|
117
|
+
let intervalId = null;
|
|
118
|
+
let intentionalClose = false;
|
|
119
|
+
const abortController = new AbortController;
|
|
120
|
+
async function poll() {
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(config.pollUrl, {
|
|
123
|
+
headers: config.headers,
|
|
124
|
+
signal: abortController.signal
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
console.warn(`[reflectdb] Poll returned ${response.status}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const data = await response.json();
|
|
131
|
+
for (const msg of data.messages) {
|
|
132
|
+
try {
|
|
133
|
+
handler?.(msg);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error("[reflectdb] Polling message handler error:", err);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (intentionalClose || err.name === "AbortError")
|
|
140
|
+
return;
|
|
141
|
+
handler?.({ type: "disconnect", reason: "poll_error" });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
async send(message) {
|
|
146
|
+
const res = await fetch(config.messageUrl, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
...config.headers
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify(message),
|
|
153
|
+
signal: abortController.signal
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
throw new Error(`Polling send failed: ${res.status} ${res.statusText}`);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
subscribe(h) {
|
|
160
|
+
handler = h;
|
|
161
|
+
if (intervalId !== null)
|
|
162
|
+
return;
|
|
163
|
+
poll();
|
|
164
|
+
const ms = config.interval ?? 1000;
|
|
165
|
+
intervalId = setInterval(poll, ms);
|
|
166
|
+
},
|
|
167
|
+
async close() {
|
|
168
|
+
intentionalClose = true;
|
|
169
|
+
if (intervalId !== null) {
|
|
170
|
+
clearInterval(intervalId);
|
|
171
|
+
intervalId = null;
|
|
172
|
+
}
|
|
173
|
+
abortController.abort();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
pollingBodyTooLarge,
|
|
179
|
+
createPollingServerTransport,
|
|
180
|
+
createPollingClientTransport
|
|
181
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
type OpType = "insert" | "update" | "delete";
|
|
2
|
+
type ErrorReason = "outside_shape" | "readonly_query" | "rate_limited" | "buffer_full" | "server_conflict" | "custom_conflict" | "merge_stale" | "clock_drift" | "replay" | "batch_too_large" | "schema_outdated" | "auth_revoked" | "compacted" | "mutation_rejected" | "server_error" | "unknown_query";
|
|
3
|
+
interface HelloMessage {
|
|
4
|
+
type: "hello";
|
|
5
|
+
/** Highest protocol version the client prefers. Retained for back-compat. */
|
|
6
|
+
protocolVersion: number;
|
|
7
|
+
/** All versions the client can speak. Omit for legacy single-version clients. */
|
|
8
|
+
supportedVersions?: readonly number[];
|
|
9
|
+
clientId: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
interface BootstrapMessage {
|
|
13
|
+
type: "bootstrap";
|
|
14
|
+
syncParams?: Record<string, Record<string, unknown>>;
|
|
15
|
+
}
|
|
16
|
+
interface ResumeMessage {
|
|
17
|
+
type: "resume";
|
|
18
|
+
since: string;
|
|
19
|
+
}
|
|
20
|
+
interface OpsMessage {
|
|
21
|
+
type: "ops";
|
|
22
|
+
ops: ClientOp[];
|
|
23
|
+
token: string;
|
|
24
|
+
}
|
|
25
|
+
interface ClientOp {
|
|
26
|
+
id: string;
|
|
27
|
+
table: string;
|
|
28
|
+
op: OpType;
|
|
29
|
+
rowId: string;
|
|
30
|
+
payload: Record<string, unknown> | null;
|
|
31
|
+
hlc: string;
|
|
32
|
+
batchId?: string;
|
|
33
|
+
batchSize?: number;
|
|
34
|
+
batchSeq?: number;
|
|
35
|
+
}
|
|
36
|
+
interface SyncDeclareMessage {
|
|
37
|
+
type: "sync_declare";
|
|
38
|
+
table: string;
|
|
39
|
+
params?: Record<string, unknown>;
|
|
40
|
+
window?: number;
|
|
41
|
+
}
|
|
42
|
+
interface LoadMoreMessage {
|
|
43
|
+
type: "load_more";
|
|
44
|
+
table: string;
|
|
45
|
+
count: number;
|
|
46
|
+
}
|
|
47
|
+
interface UnsyncMessage {
|
|
48
|
+
type: "unsync";
|
|
49
|
+
table: string;
|
|
50
|
+
}
|
|
51
|
+
interface AuthMessage {
|
|
52
|
+
type: "auth";
|
|
53
|
+
token: string;
|
|
54
|
+
}
|
|
55
|
+
interface EphemeralMessage {
|
|
56
|
+
type: "ephemeral";
|
|
57
|
+
key: string;
|
|
58
|
+
userId: string;
|
|
59
|
+
data: Record<string, unknown>;
|
|
60
|
+
ttlMs?: number;
|
|
61
|
+
}
|
|
62
|
+
interface HelloAckMessage {
|
|
63
|
+
type: "hello_ack";
|
|
64
|
+
protocolVersion: number;
|
|
65
|
+
serverId: string;
|
|
66
|
+
}
|
|
67
|
+
interface HelloRejectMessage {
|
|
68
|
+
type: "hello_reject";
|
|
69
|
+
reason: string;
|
|
70
|
+
supported: number[];
|
|
71
|
+
}
|
|
72
|
+
interface SnapshotMessage {
|
|
73
|
+
type: "snapshot";
|
|
74
|
+
table: string;
|
|
75
|
+
rows: Record<string, unknown>[];
|
|
76
|
+
colClocks: Record<string, Record<string, string>>;
|
|
77
|
+
append?: boolean;
|
|
78
|
+
totalCount?: number;
|
|
79
|
+
/**
|
|
80
|
+
* Primary key column name for this table. Self-contained per message so the
|
|
81
|
+
* client doesn't depend on `bootstrap_complete.tableMeta` arriving first.
|
|
82
|
+
* When omitted (older servers), the client falls back to tableMeta, then "id".
|
|
83
|
+
*/
|
|
84
|
+
pk?: string;
|
|
85
|
+
}
|
|
86
|
+
interface BootstrapCompleteMessage {
|
|
87
|
+
type: "bootstrap_complete";
|
|
88
|
+
serverHlc: string;
|
|
89
|
+
tableMeta: Record<string, TableMeta>;
|
|
90
|
+
}
|
|
91
|
+
interface TableMeta {
|
|
92
|
+
serverSet: string[];
|
|
93
|
+
readonly: string[];
|
|
94
|
+
broadcast: "consistent" | "eager" | "eager-durable";
|
|
95
|
+
/** Primary key column name. Defaults to "id" when omitted (older servers). */
|
|
96
|
+
pk?: string;
|
|
97
|
+
}
|
|
98
|
+
interface DeltaMessage {
|
|
99
|
+
type: "delta";
|
|
100
|
+
table: string;
|
|
101
|
+
op: OpType;
|
|
102
|
+
rowId: string;
|
|
103
|
+
payload: Record<string, unknown> | null;
|
|
104
|
+
hlc: string;
|
|
105
|
+
colClocks?: Record<string, string>;
|
|
106
|
+
}
|
|
107
|
+
interface AckMessage {
|
|
108
|
+
type: "ack";
|
|
109
|
+
opIds: string[];
|
|
110
|
+
}
|
|
111
|
+
interface RejectMessage {
|
|
112
|
+
type: "reject";
|
|
113
|
+
opId?: string;
|
|
114
|
+
batchId?: string;
|
|
115
|
+
reason: ErrorReason;
|
|
116
|
+
serverRow?: Record<string, unknown>;
|
|
117
|
+
}
|
|
118
|
+
interface ResumeCompleteMessage {
|
|
119
|
+
type: "resume_complete";
|
|
120
|
+
serverHlc: string;
|
|
121
|
+
}
|
|
122
|
+
interface ResumeRejectedMessage {
|
|
123
|
+
type: "resume_rejected";
|
|
124
|
+
reason: string;
|
|
125
|
+
serverHlc: string;
|
|
126
|
+
}
|
|
127
|
+
interface ShapeChangedMessage {
|
|
128
|
+
type: "shape_changed";
|
|
129
|
+
table: string;
|
|
130
|
+
reason: "auth_changed" | "params_changed" | "server_policy";
|
|
131
|
+
}
|
|
132
|
+
interface DisconnectMessage {
|
|
133
|
+
type: "disconnect";
|
|
134
|
+
reason: string;
|
|
135
|
+
}
|
|
136
|
+
interface ReauthMessage {
|
|
137
|
+
type: "reauth";
|
|
138
|
+
}
|
|
139
|
+
interface CountChangedMessage {
|
|
140
|
+
type: "count_changed";
|
|
141
|
+
table: string;
|
|
142
|
+
totalCount: number;
|
|
143
|
+
}
|
|
144
|
+
interface EphemeralEvent {
|
|
145
|
+
type: "ephemeral";
|
|
146
|
+
key: string;
|
|
147
|
+
clientId: string;
|
|
148
|
+
userId: string;
|
|
149
|
+
data: Record<string, unknown>;
|
|
150
|
+
ttlMs?: number;
|
|
151
|
+
}
|
|
152
|
+
type ClientMessage = HelloMessage | BootstrapMessage | ResumeMessage | OpsMessage | SyncDeclareMessage | LoadMoreMessage | UnsyncMessage | AuthMessage | EphemeralMessage;
|
|
153
|
+
type ServerMessage = HelloAckMessage | HelloRejectMessage | SnapshotMessage | BootstrapCompleteMessage | DeltaMessage | AckMessage | RejectMessage | ResumeCompleteMessage | ResumeRejectedMessage | ShapeChangedMessage | DisconnectMessage | ReauthMessage | EphemeralEvent | CountChangedMessage;
|
|
154
|
+
interface ServerTransport {
|
|
155
|
+
/**
|
|
156
|
+
* Deliver a message to one client.
|
|
157
|
+
*
|
|
158
|
+
* MUST reject (ideally with `TransportSendError`) when the frame could not
|
|
159
|
+
* be handed to the peer — unknown/closed socket, full outbound queue, or a
|
|
160
|
+
* throw from the underlying socket. Resolving on a dropped frame makes the
|
|
161
|
+
* broadcast engine commit result-cache state the client never received.
|
|
162
|
+
*/
|
|
163
|
+
send(clientId: string, message: ServerMessage): Promise<void>;
|
|
164
|
+
broadcast(roomId: string, message: ServerMessage, exclude?: string): Promise<void>;
|
|
165
|
+
onMessage(handler: (clientId: string, message: ClientMessage) => void): void;
|
|
166
|
+
onConnect(handler: (clientId: string, req: Request) => void): void;
|
|
167
|
+
onDisconnect(handler: (clientId: string) => void): void;
|
|
168
|
+
close(): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* Force-disconnect a single client. Optional — if absent, the handler relies
|
|
171
|
+
* on the client honoring a `disconnect` server-message. Required for any
|
|
172
|
+
* transport that carries authentication state, since an adversarial client
|
|
173
|
+
* can otherwise ignore the message and continue using a revoked session.
|
|
174
|
+
*/
|
|
175
|
+
disconnect?(clientId: string): Promise<void> | void;
|
|
176
|
+
}
|
|
177
|
+
interface ClientTransport {
|
|
178
|
+
send(message: ClientMessage): Promise<void>;
|
|
179
|
+
subscribe(handler: (message: ServerMessage) => void): void;
|
|
180
|
+
close(): Promise<void>;
|
|
181
|
+
}
|
|
182
|
+
interface SseServerConfig {
|
|
183
|
+
/** Max replay buffer size per client. Default: 256. */
|
|
184
|
+
replayBufferSize?: number;
|
|
185
|
+
/**
|
|
186
|
+
* Defensive cap on inbound message JSON byte length. The HTTP POST handler
|
|
187
|
+
* should size-gate the request body before parsing, but this catches a
|
|
188
|
+
* caller that forgets — preferable to silently accepting unbounded JSON.
|
|
189
|
+
* Default: 1 MB.
|
|
190
|
+
*/
|
|
191
|
+
maxMessageBytes?: number;
|
|
192
|
+
}
|
|
193
|
+
declare function createSseServerTransport(cfg?: SseServerConfig): ServerTransport & {
|
|
194
|
+
handleSubscribe(clientId: string, controller: ReadableStreamDefaultController, lastEventId?: string): void;
|
|
195
|
+
handleMessage(clientId: string, message: ClientMessage): void;
|
|
196
|
+
handleDisconnect(clientId: string): void;
|
|
197
|
+
createEventStream(clientId: string, lastEventId?: string): ReadableStream;
|
|
198
|
+
};
|
|
199
|
+
interface SseClientConfig {
|
|
200
|
+
eventUrl: string;
|
|
201
|
+
messageUrl: string;
|
|
202
|
+
headers?: Record<string, string>;
|
|
203
|
+
}
|
|
204
|
+
declare function createSseClientTransport(config: SseClientConfig): ClientTransport;
|
|
205
|
+
export { createSseServerTransport, createSseClientTransport, SseServerConfig, SseClientConfig };
|