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.
- package/README.md +483 -17
- package/dist/cjs/htmx/index.cjs +1720 -0
- package/dist/cjs/htmx/index.d.cts +760 -0
- package/dist/cjs/server/index.cjs +12 -0
- package/dist/cjs/server/index.d.cts +14 -0
- package/dist/cjs/server/storage/object/index.cjs +2284 -0
- package/dist/cjs/server/storage/object/index.d.cts +578 -0
- package/dist/cjs/transport/sse.cjs +52 -1
- package/dist/cjs/transport/sse.d.cts +34 -0
- package/dist/htmx/index.d.ts +760 -0
- package/dist/htmx/index.js +297 -0
- package/dist/server/drizzle.js +2 -1
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +16 -2
- package/dist/server/storage/object/index.d.ts +578 -0
- package/dist/server/storage/object/index.js +2232 -0
- package/dist/shared/{esm-77ndhmpv.js → esm-5ahpq25j.js} +4 -14
- package/dist/shared/esm-dcs8qa5n.js +263 -0
- package/dist/shared/esm-f11s9zpb.js +15 -0
- package/dist/transport/sse.d.ts +34 -0
- package/dist/transport/sse.js +52 -1
- package/dist/vanilla/index.js +4 -260
- package/package.json +23 -1
|
@@ -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
|
+
};
|
package/dist/server/drizzle.js
CHANGED
package/dist/server/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/server/index.js
CHANGED
|
@@ -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-
|
|
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;
|