reflectdb 0.1.2 → 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.
Files changed (48) hide show
  1. package/README.md +483 -17
  2. package/dist/cjs/client/index.cjs +0 -1
  3. package/dist/cjs/client/storage/indexeddb.cjs +0 -1
  4. package/dist/cjs/core/index.cjs +0 -1
  5. package/dist/cjs/htmx/index.cjs +1720 -0
  6. package/dist/cjs/htmx/index.d.cts +760 -0
  7. package/dist/cjs/react/index.cjs +0 -1
  8. package/dist/cjs/server/drizzle.cjs +15 -4
  9. package/dist/cjs/server/ephemeral/index.cjs +0 -1
  10. package/dist/cjs/server/ephemeral/redis.cjs +0 -1
  11. package/dist/cjs/server/index.cjs +34 -13
  12. package/dist/cjs/server/index.d.cts +14 -0
  13. package/dist/cjs/server/storage/object/index.cjs +2284 -0
  14. package/dist/cjs/server/storage/object/index.d.cts +578 -0
  15. package/dist/cjs/svelte/index.cjs +0 -1
  16. package/dist/cjs/transport/bun-ws.cjs +0 -1
  17. package/dist/cjs/transport/polling.cjs +0 -1
  18. package/dist/cjs/transport/sse.cjs +52 -2
  19. package/dist/cjs/transport/sse.d.cts +34 -0
  20. package/dist/cjs/transport/ws.cjs +0 -1
  21. package/dist/cjs/vanilla/index.cjs +0 -1
  22. package/dist/client/index.js +1 -1
  23. package/dist/client/storage/indexeddb.js +1 -1
  24. package/dist/core/index.js +1 -1
  25. package/dist/htmx/index.d.ts +760 -0
  26. package/dist/htmx/index.js +297 -0
  27. package/dist/react/index.js +1 -1
  28. package/dist/server/drizzle.js +3 -2
  29. package/dist/server/ephemeral/index.js +1 -1
  30. package/dist/server/ephemeral/redis.js +1 -1
  31. package/dist/server/index.d.ts +14 -0
  32. package/dist/server/index.js +19 -6
  33. package/dist/server/storage/object/index.d.ts +578 -0
  34. package/dist/server/storage/object/index.js +2232 -0
  35. package/dist/shared/{esm-z1xse19c.js → esm-5ahpq25j.js} +4 -4
  36. package/dist/shared/esm-dcs8qa5n.js +263 -0
  37. package/dist/shared/esm-f11s9zpb.js +15 -0
  38. package/dist/shared/esm-g5h4a88j.js +9 -0
  39. package/dist/shared/{esm-ck88h30s.js → esm-xtkhzxg2.js} +1 -1
  40. package/dist/svelte/index.js +1 -1
  41. package/dist/transport/bun-ws.js +1 -1
  42. package/dist/transport/polling.js +1 -1
  43. package/dist/transport/sse.d.ts +34 -0
  44. package/dist/transport/sse.js +53 -2
  45. package/dist/transport/ws.js +1 -1
  46. package/dist/vanilla/index.js +5 -261
  47. package/package.json +26 -3
  48. package/dist/shared/esm-k7kedp3y.js +0 -4
@@ -1,5 +1,6 @@
1
- // src/server/drizzle.ts
2
- import { createRequire } from "node:module";
1
+ import {
2
+ nodeRequire
3
+ } from "./esm-f11s9zpb.js";
3
4
 
4
5
  // src/server/factory.ts
5
6
  function getByDottedPath(root, path) {
@@ -226,12 +227,11 @@ function defineTable(adapter, opts = {}) {
226
227
  }
227
228
 
228
229
  // src/server/drizzle.ts
229
- var requireFn = createRequire(import.meta.url);
230
230
  var cachedDrizzle;
231
231
  function loadDrizzleSync() {
232
232
  if (cachedDrizzle === undefined) {
233
233
  try {
234
- const mod = requireFn("drizzle-orm");
234
+ const mod = nodeRequire("drizzle-orm");
235
235
  cachedDrizzle = { eq: mod.eq, sql: mod.sql, getTableName: mod.getTableName };
236
236
  } catch {
237
237
  cachedDrizzle = null;
@@ -0,0 +1,263 @@
1
+ import {
2
+ SyncClient,
3
+ pushSafely
4
+ } from "./esm-8qbr4y0d.js";
5
+
6
+ // src/vanilla/sync.ts
7
+ function createBrowserWsTransport(url) {
8
+ let ws = null;
9
+ let handler = null;
10
+ let intentionalClose = false;
11
+ const sendQueue = [];
12
+ function ensureConnected() {
13
+ if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) {
14
+ return ws;
15
+ }
16
+ const socket = new WebSocket(url);
17
+ socket.onopen = () => {
18
+ for (const msg of sendQueue) {
19
+ socket.send(JSON.stringify(msg));
20
+ }
21
+ sendQueue.length = 0;
22
+ };
23
+ socket.onmessage = (event) => {
24
+ handler?.(JSON.parse(event.data));
25
+ };
26
+ socket.onclose = () => {
27
+ ws = null;
28
+ if (!intentionalClose) {
29
+ handler?.({ type: "disconnect", reason: "transport_closed" });
30
+ }
31
+ };
32
+ ws = socket;
33
+ return socket;
34
+ }
35
+ return {
36
+ async send(message) {
37
+ const socket = ensureConnected();
38
+ if (socket.readyState === WebSocket.OPEN) {
39
+ socket.send(JSON.stringify(message));
40
+ } else {
41
+ sendQueue.push(message);
42
+ }
43
+ },
44
+ subscribe(h) {
45
+ handler = h;
46
+ },
47
+ async close() {
48
+ intentionalClose = true;
49
+ if (ws) {
50
+ ws.close();
51
+ ws = null;
52
+ }
53
+ }
54
+ };
55
+ }
56
+ function createSync(config) {
57
+ const clientId = config.clientId ?? `browser-${crypto.randomUUID().slice(0, 8)}`;
58
+ const transport = createBrowserWsTransport(config.url);
59
+ const managedTables = new Set(config.tables ?? []);
60
+ const unmanagedRefCounts = new Map;
61
+ const unmanagedParamKeys = new Map;
62
+ function stableKey(obj) {
63
+ if (!obj)
64
+ return "";
65
+ const keys = Object.keys(obj).sort();
66
+ return keys.map((k) => `${k}:${JSON.stringify(obj[k])}`).join("|");
67
+ }
68
+ const client = new SyncClient({
69
+ clientId,
70
+ transport,
71
+ token: config.token,
72
+ storage: config.storage,
73
+ onReauth: () => config.onReauth?.() ?? Promise.reject(new Error("No onReauth handler")),
74
+ onError: (err) => config.onError?.(err)
75
+ });
76
+ async function connect() {
77
+ if (config.storage) {
78
+ await client.init();
79
+ }
80
+ await client.connect();
81
+ for (const table of managedTables) {
82
+ await client.sync(table);
83
+ }
84
+ await client.resume();
85
+ }
86
+ async function close() {
87
+ await client.close();
88
+ }
89
+ function getState() {
90
+ return client.getState();
91
+ }
92
+ function onStateChange(cb) {
93
+ let lastState = client.getState();
94
+ return client.subscribe(() => {
95
+ const current = client.getState();
96
+ if (current !== lastState) {
97
+ lastState = current;
98
+ cb(current);
99
+ }
100
+ });
101
+ }
102
+ function getPendingCount() {
103
+ return client.getPendingCount();
104
+ }
105
+ function onPendingChange(cb) {
106
+ let lastCount = client.getPendingCount();
107
+ return client.subscribe(() => {
108
+ const current = client.getPendingCount();
109
+ if (current !== lastCount) {
110
+ lastCount = current;
111
+ cb(current);
112
+ }
113
+ });
114
+ }
115
+ function sync(table, options) {
116
+ const isManaged = managedTables.has(table);
117
+ const paramKey = stableKey(options?.params);
118
+ if (!isManaged) {
119
+ const currentParamKey = unmanagedParamKeys.get(table);
120
+ const count = unmanagedRefCounts.get(table) ?? 0;
121
+ if (count === 0 || currentParamKey !== paramKey) {
122
+ if (currentParamKey !== undefined && currentParamKey !== paramKey) {
123
+ client.unsync(table);
124
+ }
125
+ client.sync(table, options?.params, options?.window ? { window: options.window } : undefined);
126
+ client.scheduleBootstrap();
127
+ unmanagedParamKeys.set(table, paramKey);
128
+ }
129
+ unmanagedRefCounts.set(table, count + 1);
130
+ }
131
+ return {
132
+ getRows(getOptions) {
133
+ return client.getRows(table, {
134
+ includeDeleted: getOptions?.includeDeleted ?? options?.includeDeleted
135
+ });
136
+ },
137
+ insert(rowId, payload) {
138
+ client.insert(table, rowId, payload);
139
+ pushSafely(client);
140
+ },
141
+ update(rowId, payload) {
142
+ client.update(table, rowId, payload);
143
+ pushSafely(client);
144
+ },
145
+ remove(rowId) {
146
+ client.delete(table, rowId);
147
+ pushSafely(client);
148
+ },
149
+ onChange(cb) {
150
+ return client.subscribeTable(table, cb);
151
+ },
152
+ destroy() {
153
+ if (isManaged)
154
+ return;
155
+ const count = (unmanagedRefCounts.get(table) ?? 1) - 1;
156
+ if (count <= 0) {
157
+ unmanagedRefCounts.delete(table);
158
+ unmanagedParamKeys.delete(table);
159
+ client.unsync(table);
160
+ } else {
161
+ unmanagedRefCounts.set(table, count);
162
+ }
163
+ }
164
+ };
165
+ }
166
+ function getRow(table, rowId) {
167
+ return client.getRow(table, rowId);
168
+ }
169
+ function onRowChange(table, rowId, cb) {
170
+ return client.subscribeTable(table, () => {
171
+ cb(client.getRow(table, rowId));
172
+ });
173
+ }
174
+ function ephemeral(ephConfig) {
175
+ let events = {};
176
+ const timers = new Map;
177
+ const changeListeners = new Set;
178
+ function notifyChange() {
179
+ for (const listener of changeListeners) {
180
+ listener(events);
181
+ }
182
+ }
183
+ const unsub = client.subscribeEphemeral(ephConfig.key, (event) => {
184
+ events = { ...events, [event.clientId]: event.data };
185
+ if (ephConfig.ttlMs) {
186
+ const existing = timers.get(event.clientId);
187
+ if (existing) {
188
+ clearTimeout(existing);
189
+ }
190
+ const timer = setTimeout(() => {
191
+ const next = { ...events };
192
+ delete next[event.clientId];
193
+ events = next;
194
+ timers.delete(event.clientId);
195
+ notifyChange();
196
+ }, ephConfig.ttlMs);
197
+ timers.set(event.clientId, timer);
198
+ }
199
+ notifyChange();
200
+ });
201
+ return {
202
+ getEvents() {
203
+ return events;
204
+ },
205
+ broadcast(data) {
206
+ return client.sendEphemeral({
207
+ key: ephConfig.key,
208
+ userId: ephConfig.userId,
209
+ data,
210
+ ttlMs: ephConfig.ttlMs
211
+ });
212
+ },
213
+ onChange(cb) {
214
+ changeListeners.add(cb);
215
+ return () => {
216
+ changeListeners.delete(cb);
217
+ };
218
+ },
219
+ destroy() {
220
+ unsub();
221
+ for (const timer of timers.values()) {
222
+ clearTimeout(timer);
223
+ }
224
+ timers.clear();
225
+ changeListeners.clear();
226
+ }
227
+ };
228
+ }
229
+ function getTotalCount(table) {
230
+ return client.getTotalCount(table);
231
+ }
232
+ function onTotalCountChange(table, cb) {
233
+ let lastCount = client.getTotalCount(table);
234
+ return client.subscribeTable(table, () => {
235
+ const current = client.getTotalCount(table);
236
+ if (current !== lastCount) {
237
+ lastCount = current;
238
+ cb(current);
239
+ }
240
+ });
241
+ }
242
+ function loadMore(table, count) {
243
+ client.loadMore(table, count);
244
+ }
245
+ return {
246
+ client,
247
+ connect,
248
+ close,
249
+ getState,
250
+ onStateChange,
251
+ getPendingCount,
252
+ onPendingChange,
253
+ sync,
254
+ getRow,
255
+ onRowChange,
256
+ ephemeral,
257
+ getTotalCount,
258
+ onTotalCountChange,
259
+ loadMore
260
+ };
261
+ }
262
+
263
+ export { createBrowserWsTransport, createSync };
@@ -0,0 +1,15 @@
1
+ // src/server/node-require.ts
2
+ var cached;
3
+ function nodeRequire(id) {
4
+ if (!cached) {
5
+ const proc = globalThis.process;
6
+ const mod = proc?.getBuiltinModule?.("node:module");
7
+ if (!mod?.createRequire) {
8
+ throw new Error(`Cannot resolve "${id}": this runtime has no CommonJS resolver (process.getBuiltinModule is unavailable). reflectdb requires Node >= 22.3 or Bun >= 1.1.30 on the server.`);
9
+ }
10
+ cached = mod.createRequire(import.meta.url);
11
+ }
12
+ return cached(id);
13
+ }
14
+
15
+ export { nodeRequire };
@@ -0,0 +1,9 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+
9
+ export { __require };
@@ -10,7 +10,7 @@ import {
10
10
  isErrorReason,
11
11
  reasonFromError
12
12
  } from "./esm-3tkwvysa.js";
13
- import"./esm-k7kedp3y.js";
13
+ import"./esm-g5h4a88j.js";
14
14
  export {
15
15
  MAX_BATCH_SIZE,
16
16
  MAX_CLOCK_DRIFT_MS,
@@ -4,7 +4,7 @@ import {
4
4
  } from "../shared/esm-8qbr4y0d.js";
5
5
  import"../shared/esm-rw7jjtrv.js";
6
6
  import"../shared/esm-3tkwvysa.js";
7
- import"../shared/esm-k7kedp3y.js";
7
+ import"../shared/esm-g5h4a88j.js";
8
8
 
9
9
  // src/svelte/stores.ts
10
10
  function createBrowserWsTransport(url) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TransportSendError
3
3
  } from "../shared/esm-3tkwvysa.js";
4
- import"../shared/esm-k7kedp3y.js";
4
+ import"../shared/esm-g5h4a88j.js";
5
5
 
6
6
  // src/transport/bun-ws.ts
7
7
  function trySend(clientId, ws, data, maxBufferedBytes) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TransportSendError
3
3
  } from "../shared/esm-3tkwvysa.js";
4
- import"../shared/esm-k7kedp3y.js";
4
+ import"../shared/esm-g5h4a88j.js";
5
5
 
6
6
  // src/transport/polling.ts
7
7
  function pollingBodyTooLarge(body, limit = 1e6) {
@@ -189,17 +189,51 @@ interface SseServerConfig {
189
189
  * Default: 1 MB.
190
190
  */
191
191
  maxMessageBytes?: number;
192
+ /**
193
+ * Return replies from the POST that produced them instead of assuming they
194
+ * can be streamed.
195
+ *
196
+ * Normally SSE is server→client only: a client POSTs a message and every
197
+ * reply — `hello_ack`, snapshots, op acks — comes back down the held stream.
198
+ * That works only while the POST and the stream are handled by the SAME
199
+ * process. On a serverless platform (Vercel, Lambda, Workers) they are two
200
+ * separate invocations, so those replies would be enqueued onto a stream the
201
+ * POST's process does not have, and the client would hang at the handshake.
202
+ *
203
+ * With this set, `collectReplies` runs a message and hands back what it
204
+ * produced so the HTTP handler can put it in the POST response body. The
205
+ * stream is then only used for what it is actually good at: pushing OTHER
206
+ * clients' changes. Pair it with `serverless: true` on the client transport.
207
+ *
208
+ * Off by default — a single-process server should keep streaming everything,
209
+ * and turning this on there would deliver each reply twice.
210
+ */
211
+ serverless?: boolean;
192
212
  }
193
213
  declare function createSseServerTransport(cfg?: SseServerConfig): ServerTransport & {
194
214
  handleSubscribe(clientId: string, controller: ReadableStreamDefaultController, lastEventId?: string): void;
195
215
  handleMessage(clientId: string, message: ClientMessage): void;
196
216
  handleDisconnect(clientId: string): void;
197
217
  createEventStream(clientId: string, lastEventId?: string): ReadableStream;
218
+ collectReplies(clientId: string, message: ClientMessage, settle?: () => Promise<void>): Promise<ServerMessage[]>;
198
219
  };
199
220
  interface SseClientConfig {
200
221
  eventUrl: string;
201
222
  messageUrl: string;
202
223
  headers?: Record<string, string>;
224
+ /**
225
+ * Read replies out of the POST response instead of waiting for them on the
226
+ * stream. Must match `serverless: true` on the server transport.
227
+ *
228
+ * Needed wherever the POST and the event stream are handled by different
229
+ * processes — Vercel, Lambda, Workers. There, replies to a POST are produced
230
+ * by a process that does not own this client's stream, so they can never be
231
+ * streamed and the handshake never completes.
232
+ *
233
+ * Leave it off for a single-process server: replies stream normally there,
234
+ * and turning it on would deliver every one of them twice.
235
+ */
236
+ serverless?: boolean;
203
237
  }
204
238
  declare function createSseClientTransport(config: SseClientConfig): ClientTransport;
205
239
  export { SseClientConfig, SseServerConfig, createSseClientTransport, createSseServerTransport };
@@ -1,14 +1,19 @@
1
1
  import {
2
2
  TransportSendError
3
3
  } from "../shared/esm-3tkwvysa.js";
4
- import"../shared/esm-k7kedp3y.js";
4
+ import"../shared/esm-g5h4a88j.js";
5
5
 
6
6
  // src/transport/sse.ts
7
+ var COLLECT_STABLE_TICKS = 3;
8
+ var MAX_COLLECT_TICKS = 60;
7
9
  function createSseServerTransport(cfg = {}) {
8
10
  const controllers = new Map;
9
11
  const buffers = new Map;
10
12
  const counters = new Map;
11
13
  const replayBufferSize = cfg.replayBufferSize ?? 256;
14
+ const serverless = cfg.serverless ?? false;
15
+ const collecting = new Map;
16
+ const connected = new Set;
12
17
  const maxMessageBytes = cfg.maxMessageBytes ?? 1e6;
13
18
  let messageHandler = null;
14
19
  let connectHandler = null;
@@ -56,7 +61,10 @@ data: ${data}
56
61
  }
57
62
  }
58
63
  }
59
- connectHandler?.(clientId, new Request("https://sse-connect"));
64
+ if (!connected.has(clientId)) {
65
+ connected.add(clientId);
66
+ connectHandler?.(clientId, new Request("https://sse-connect"));
67
+ }
60
68
  },
61
69
  handleMessage(clientId, message) {
62
70
  let byteLen;
@@ -74,6 +82,7 @@ data: ${data}
74
82
  },
75
83
  handleDisconnect(clientId) {
76
84
  controllers.delete(clientId);
85
+ connected.delete(clientId);
77
86
  disconnectHandler?.(clientId);
78
87
  },
79
88
  createEventStream(clientId, lastEventId) {
@@ -87,6 +96,11 @@ data: ${data}
87
96
  });
88
97
  },
89
98
  async send(clientId, message) {
99
+ const sink = collecting.get(clientId);
100
+ if (sink) {
101
+ sink.push(message);
102
+ return;
103
+ }
90
104
  const id = nextId(clientId);
91
105
  const encoded = encode(id, message);
92
106
  const controller = controllers.get(clientId);
@@ -108,6 +122,31 @@ data: ${data}
108
122
  throw new TransportSendError(clientId, `sse replay buffer overflow (size ${replayBufferSize}); undelivered frames dropped`);
109
123
  }
110
124
  },
125
+ async collectReplies(clientId, message, settle) {
126
+ if (!serverless) {
127
+ this.handleMessage(clientId, message);
128
+ return [];
129
+ }
130
+ if (!connected.has(clientId)) {
131
+ connected.add(clientId);
132
+ connectHandler?.(clientId, new Request("https://sse-connect"));
133
+ }
134
+ const sink = [];
135
+ collecting.set(clientId, sink);
136
+ try {
137
+ this.handleMessage(clientId, message);
138
+ await (settle?.() ?? Promise.resolve());
139
+ let stable = 0;
140
+ for (let i = 0;i < MAX_COLLECT_TICKS && stable < COLLECT_STABLE_TICKS; i++) {
141
+ const before = sink.length;
142
+ await new Promise((resolve) => setTimeout(resolve, 0));
143
+ stable = sink.length === before ? stable + 1 : 0;
144
+ }
145
+ } finally {
146
+ collecting.delete(clientId);
147
+ }
148
+ return sink;
149
+ },
111
150
  async broadcast(_roomId, message, exclude) {
112
151
  for (const [clientId, controller] of controllers) {
113
152
  if (clientId === exclude)
@@ -173,6 +212,18 @@ function createSseClientTransport(config) {
173
212
  if (!res.ok) {
174
213
  throw new Error(`SSE send failed: ${res.status} ${res.statusText}`);
175
214
  }
215
+ if (!config.serverless)
216
+ return;
217
+ let replies;
218
+ try {
219
+ const body = await res.json();
220
+ replies = Array.isArray(body) ? body : body.messages ?? [];
221
+ } catch {
222
+ console.warn("[reflectdb] SSE: serverless mode is on but the POST returned no JSON replies. " + "Set `serverless: true` on createSseServerTransport and return " + "`collectReplies(...)` from the message endpoint.");
223
+ return;
224
+ }
225
+ for (const reply of replies)
226
+ handler?.(reply);
176
227
  },
177
228
  subscribe(h) {
178
229
  handler = h;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  TransportSendError
3
3
  } from "../shared/esm-3tkwvysa.js";
4
- import"../shared/esm-k7kedp3y.js";
4
+ import"../shared/esm-g5h4a88j.js";
5
5
 
6
6
  // src/transport/ws.ts
7
7
  function trySend(clientId, ws, data, maxBufferedBytes) {