reflectdb 0.1.3 → 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.
@@ -0,0 +1,297 @@
1
+ import {
2
+ createSync
3
+ } from "../shared/esm-dcs8qa5n.js";
4
+ import"../shared/esm-8qbr4y0d.js";
5
+ import"../shared/esm-rw7jjtrv.js";
6
+ import"../shared/esm-3tkwvysa.js";
7
+ import"../shared/esm-g5h4a88j.js";
8
+
9
+ // src/htmx/router.ts
10
+ var REFLECT_SCHEME = "reflect:";
11
+ function parseReflectAction(action) {
12
+ if (!action.startsWith(REFLECT_SCHEME))
13
+ return null;
14
+ const rest = action.slice(REFLECT_SCHEME.length);
15
+ const queryAt = rest.indexOf("?");
16
+ const path = queryAt === -1 ? rest : rest.slice(0, queryAt);
17
+ const query = queryAt === -1 ? "" : rest.slice(queryAt + 1);
18
+ const segments = path.split("/").filter((s) => s.length > 0);
19
+ if (segments.length === 0 || segments.length > 2)
20
+ return null;
21
+ const table = safeDecode(segments[0]);
22
+ if (!table)
23
+ return null;
24
+ const rawRowId = segments[1];
25
+ const rowId = rawRowId === undefined ? undefined : safeDecode(rawRowId);
26
+ if (rawRowId !== undefined && !rowId)
27
+ return null;
28
+ return { table, rowId, params: new URLSearchParams(query) };
29
+ }
30
+ function safeDecode(segment) {
31
+ try {
32
+ return decodeURIComponent(segment);
33
+ } catch {
34
+ return "";
35
+ }
36
+ }
37
+ var WRITE_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
38
+ function resolveOperation(action, method, body, options = {}) {
39
+ const parsed = parseReflectAction(action);
40
+ if (!parsed)
41
+ return null;
42
+ const verb = method.toUpperCase();
43
+ const { table, rowId, params } = parsed;
44
+ if (verb === "GET") {
45
+ return { kind: "read", table, rowId, params };
46
+ }
47
+ if (!WRITE_METHODS.has(verb)) {
48
+ return { kind: "error", status: 405, message: `${verb} is not supported by reflect: actions` };
49
+ }
50
+ if (verb === "DELETE") {
51
+ if (!rowId) {
52
+ return {
53
+ kind: "error",
54
+ status: 400,
55
+ message: `DELETE needs a row id: reflect:${table}/<rowId>`
56
+ };
57
+ }
58
+ return { kind: "remove", table, rowId };
59
+ }
60
+ const payload = collectPayload(body);
61
+ if (verb === "POST") {
62
+ if (rowId) {
63
+ return {
64
+ kind: "error",
65
+ status: 400,
66
+ message: `POST targets the collection: reflect:${table}, not reflect:${table}/${rowId}`
67
+ };
68
+ }
69
+ const bodyRowId = typeof payload.id === "string" && payload.id ? payload.id : undefined;
70
+ const generated = options.generateRowId?.() ?? crypto.randomUUID();
71
+ return { kind: "insert", table, rowId: bodyRowId ?? generated, payload };
72
+ }
73
+ if (!rowId) {
74
+ return {
75
+ kind: "error",
76
+ status: 400,
77
+ message: `${verb} needs a row id: reflect:${table}/<rowId>`
78
+ };
79
+ }
80
+ return { kind: "update", table, rowId, payload };
81
+ }
82
+ function collectPayload(body) {
83
+ const payload = {};
84
+ if (!body)
85
+ return payload;
86
+ for (const [key, value] of body) {
87
+ if (typeof value !== "string")
88
+ continue;
89
+ const existing = payload[key];
90
+ if (existing === undefined) {
91
+ payload[key] = value;
92
+ } else if (Array.isArray(existing)) {
93
+ existing.push(value);
94
+ } else {
95
+ payload[key] = [existing, value];
96
+ }
97
+ }
98
+ return payload;
99
+ }
100
+
101
+ // src/htmx/sync.ts
102
+ var HTML_HEADERS = { "Content-Type": "text/html; charset=utf-8" };
103
+ function createHtmxSync(config) {
104
+ const { htmx, ...syncConfig } = config;
105
+ const sync = createSync(syncConfig);
106
+ const configRequestEvent = "htmx:config:request".replace(/:/g, htmx.config?.metaCharacter || ":");
107
+ const views = new Map;
108
+ const parsers = new Map;
109
+ const bindings = new Map;
110
+ const unsubscribes = new Map;
111
+ const bound = new Map;
112
+ const dirty = new Set;
113
+ let flushHandle = null;
114
+ let installed = false;
115
+ let closed = false;
116
+ function bindingFor(table) {
117
+ let binding = bindings.get(table);
118
+ if (!binding) {
119
+ binding = sync.sync(table);
120
+ bindings.set(table, binding);
121
+ unsubscribes.set(table, binding.onChange(() => {
122
+ scheduleRender(table);
123
+ }));
124
+ }
125
+ return binding;
126
+ }
127
+ function scheduleRender(table) {
128
+ if (closed)
129
+ return;
130
+ dirty.add(table);
131
+ if (flushHandle !== null)
132
+ return;
133
+ flushHandle = setTimeout(() => {
134
+ flushHandle = null;
135
+ const tables = [...dirty];
136
+ dirty.clear();
137
+ for (const t of tables)
138
+ renderTable(t);
139
+ }, 0);
140
+ }
141
+ function renderTable(table) {
142
+ for (const [target, entry] of bound) {
143
+ if (!target.isConnected) {
144
+ bound.delete(target);
145
+ continue;
146
+ }
147
+ if (entry.table !== table)
148
+ continue;
149
+ htmx.ajax("GET", entry.action, {
150
+ source: entry.source.isConnected ? entry.source : target,
151
+ target,
152
+ swap: entry.swap,
153
+ confirm: null
154
+ });
155
+ }
156
+ }
157
+ function refresh(table) {
158
+ if (table === undefined) {
159
+ for (const t of new Set([...bound.values()].map((e) => e.table)))
160
+ renderTable(t);
161
+ return;
162
+ }
163
+ renderTable(table);
164
+ }
165
+ function serve(ctx, action, request) {
166
+ const method = request?.method ?? ctx.request.method;
167
+ const body = request?.body ?? ctx.request.body;
168
+ const op = resolveOperation(action, method, body);
169
+ if (!op) {
170
+ return respond(500, `reflectdb: could not parse action "${action}"`);
171
+ }
172
+ if (op.kind === "error") {
173
+ return respond(op.status, `reflectdb: ${op.message}`);
174
+ }
175
+ const binding = bindingFor(op.table);
176
+ if (op.kind === "read") {
177
+ const target = ctx.target ?? ctx.sourceElement;
178
+ bound.set(target, {
179
+ table: op.table,
180
+ action,
181
+ source: ctx.sourceElement,
182
+ target,
183
+ swap: ctx.swap
184
+ });
185
+ const view = views.get(op.table);
186
+ if (!view) {
187
+ return respond(500, `reflectdb: no view registered for "${op.table}" — call .view("${op.table}", …)`);
188
+ }
189
+ if (op.rowId !== undefined) {
190
+ const row = sync.getRow(op.table, op.rowId);
191
+ if (!row)
192
+ return respond(204);
193
+ return html(view({
194
+ rows: [row],
195
+ table: op.table,
196
+ rowId: op.rowId,
197
+ params: op.params
198
+ }));
199
+ }
200
+ return html(view({
201
+ rows: binding.getRows(),
202
+ table: op.table,
203
+ params: op.params
204
+ }));
205
+ }
206
+ if (op.kind === "remove") {
207
+ binding.remove(op.rowId);
208
+ return respond(204);
209
+ }
210
+ const parse = parsers.get(op.table);
211
+ const payload = parse ? parse(op.payload) : op.payload;
212
+ if (op.kind === "insert") {
213
+ binding.insert(op.rowId, payload);
214
+ } else {
215
+ binding.update(op.rowId, payload);
216
+ }
217
+ return respond(204);
218
+ }
219
+ function onConfigRequest(event) {
220
+ const ctx = event.detail?.ctx;
221
+ if (!ctx || !parseReflectAction(ctx.request.action))
222
+ return;
223
+ ctx.fetch = (action, request) => serve(ctx, action, request);
224
+ }
225
+ const api = {
226
+ sync,
227
+ view(table, view) {
228
+ views.set(table, view);
229
+ return api;
230
+ },
231
+ parse(table, parse) {
232
+ parsers.set(table, parse);
233
+ return api;
234
+ },
235
+ install() {
236
+ if (!installed) {
237
+ document.addEventListener(configRequestEvent, onConfigRequest);
238
+ installed = true;
239
+ }
240
+ return api;
241
+ },
242
+ uninstall() {
243
+ if (installed) {
244
+ document.removeEventListener(configRequestEvent, onConfigRequest);
245
+ installed = false;
246
+ }
247
+ bound.clear();
248
+ },
249
+ refresh,
250
+ async connect() {
251
+ api.install();
252
+ await sync.connect();
253
+ },
254
+ async close() {
255
+ closed = true;
256
+ if (flushHandle !== null) {
257
+ clearTimeout(flushHandle);
258
+ flushHandle = null;
259
+ }
260
+ api.uninstall();
261
+ for (const unsub of unsubscribes.values())
262
+ unsub();
263
+ unsubscribes.clear();
264
+ for (const binding of bindings.values())
265
+ binding.destroy();
266
+ bindings.clear();
267
+ await sync.close();
268
+ },
269
+ getState: () => sync.getState(),
270
+ onStateChange: (cb) => sync.onStateChange(cb),
271
+ getPendingCount: () => sync.getPendingCount(),
272
+ onPendingChange: (cb) => sync.onPendingChange(cb)
273
+ };
274
+ return api;
275
+ }
276
+ function html(markup) {
277
+ return Promise.resolve(new Response(markup, { status: 200, headers: HTML_HEADERS }));
278
+ }
279
+ function respond(status, message = "") {
280
+ return Promise.resolve(new Response(status === 204 ? null : message, { status, headers: HTML_HEADERS }));
281
+ }
282
+ // src/htmx/typed.ts
283
+ function createSyncHtmx() {
284
+ return {
285
+ createHtmxSync(config) {
286
+ return createHtmxSync(config);
287
+ }
288
+ };
289
+ }
290
+ export {
291
+ REFLECT_SCHEME,
292
+ collectPayload,
293
+ createHtmxSync,
294
+ createSyncHtmx,
295
+ parseReflectAction,
296
+ resolveOperation
297
+ };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  drizzleTable,
3
3
  drizzleTxAtomic
4
- } from "../shared/esm-77ndhmpv.js";
4
+ } from "../shared/esm-5ahpq25j.js";
5
+ import"../shared/esm-f11s9zpb.js";
5
6
  import"../shared/esm-g5h4a88j.js";
6
7
  export {
7
8
  drizzleTable,
@@ -769,6 +769,20 @@ declare class MessageHandler<TAuth extends AuthContext = AuthContext> {
769
769
  private storage;
770
770
  private minSchemaVersion;
771
771
  private messageQueues;
772
+ /**
773
+ * Resolves once every message queued for `clientId` has finished processing.
774
+ *
775
+ * Messages are handled on a per-client serial queue, and `onMessage` returns
776
+ * before that work settles — fine for a long-lived connection, where replies
777
+ * stream out whenever they are ready. A serverless HTTP handler has no such
778
+ * luxury: it must know when the replies to the request it is holding exist,
779
+ * or it returns an empty body and the client hangs. See the SSE transport's
780
+ * `serverless` mode.
781
+ *
782
+ * Never rejects: a failed message is the caller's concern via its own reply,
783
+ * not this barrier's.
784
+ */
785
+ whenIdle(clientId: string): Promise<void>;
772
786
  private resultCache;
773
787
  private broadcast;
774
788
  private ops;
@@ -12,9 +12,11 @@ import {
12
12
  import {
13
13
  defineTable,
14
14
  drizzleTable,
15
- drizzleTxAtomic,
15
+ drizzleTxAtomic
16
+ } from "../shared/esm-5ahpq25j.js";
17
+ import {
16
18
  nodeRequire
17
- } from "../shared/esm-77ndhmpv.js";
19
+ } from "../shared/esm-f11s9zpb.js";
18
20
  import {
19
21
  MAX_BATCH_SIZE,
20
22
  MAX_CLOCK_DRIFT_MS,
@@ -1838,6 +1840,18 @@ class MessageHandler {
1838
1840
  storage = null;
1839
1841
  minSchemaVersion = 0;
1840
1842
  messageQueues = new Map;
1843
+ async whenIdle(clientId) {
1844
+ for (let i = 0;i < 100; i++) {
1845
+ const queue = this.messageQueues.get(clientId);
1846
+ if (!queue)
1847
+ return;
1848
+ await queue.catch(() => {
1849
+ return;
1850
+ });
1851
+ if (this.messageQueues.get(clientId) === queue)
1852
+ return;
1853
+ }
1854
+ }
1841
1855
  resultCache = new ResultCache;
1842
1856
  broadcast;
1843
1857
  ops;