bunderstack 0.19.2 → 0.21.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/CHANGELOG.md +39 -1
- package/dist/api/crud-router.d.ts +23 -28
- package/dist/api/crud-router.d.ts.map +1 -1
- package/dist/api/crud-router.js +39 -18
- package/dist/api/crud-router.js.map +1 -1
- package/dist/api/live-window.d.ts.map +1 -1
- package/dist/api/live-window.js +13 -3
- package/dist/api/live-window.js.map +1 -1
- package/dist/api/realtime-router.d.ts +3 -1
- package/dist/api/realtime-router.d.ts.map +1 -1
- package/dist/api/realtime-router.js +1 -0
- package/dist/api/realtime-router.js.map +1 -1
- package/dist/api/types.d.ts +2 -2
- package/dist/api/types.d.ts.map +1 -1
- package/dist/api/types.js.map +1 -1
- package/dist/client/client.d.ts +53 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +111 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/live-view.d.ts +49 -0
- package/dist/client/live-view.d.ts.map +1 -0
- package/dist/client/live-view.js +184 -0
- package/dist/client/live-view.js.map +1 -0
- package/dist/client/react.d.ts +4 -0
- package/dist/client/react.d.ts.map +1 -0
- package/dist/client/react.js +6 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/realtime.d.ts +54 -0
- package/dist/client/realtime.d.ts.map +1 -0
- package/dist/client/realtime.js +105 -0
- package/dist/client/realtime.js.map +1 -0
- package/dist/client/rest.d.ts +2 -0
- package/dist/client/rest.d.ts.map +1 -0
- package/dist/client/rest.js +2 -0
- package/dist/client/rest.js.map +1 -0
- package/dist/client/rpc-client.d.ts +38 -0
- package/dist/client/rpc-client.d.ts.map +1 -0
- package/dist/client/rpc-client.js +59 -0
- package/dist/client/rpc-client.js.map +1 -0
- package/dist/client/solid.d.ts +7 -0
- package/dist/client/solid.d.ts.map +1 -0
- package/dist/client/solid.js +21 -0
- package/dist/client/solid.js.map +1 -0
- package/dist/client/svelte.d.ts +6 -0
- package/dist/client/svelte.d.ts.map +1 -0
- package/dist/client/svelte.js +9 -0
- package/dist/client/svelte.js.map +1 -0
- package/dist/client/vue.d.ts +5 -0
- package/dist/client/vue.d.ts.map +1 -0
- package/dist/client/vue.js +11 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/codegen.d.ts +36 -0
- package/dist/codegen.d.ts.map +1 -0
- package/dist/codegen.js +235 -0
- package/dist/codegen.js.map +1 -0
- package/dist/crud-operations.d.ts.map +1 -1
- package/dist/crud-operations.js +7 -3
- package/dist/crud-operations.js.map +1 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live/protocol.d.ts +3 -0
- package/dist/live/protocol.d.ts.map +1 -1
- package/dist/live/protocol.js.map +1 -1
- package/dist/query/api.d.ts +10 -0
- package/dist/query/api.d.ts.map +1 -0
- package/dist/query/api.js +7 -0
- package/dist/query/api.js.map +1 -0
- package/dist/query/client.d.ts +33 -0
- package/dist/query/client.d.ts.map +1 -0
- package/dist/query/client.js +98 -0
- package/dist/query/client.js.map +1 -0
- package/dist/query/fetch.d.ts +3 -0
- package/dist/query/fetch.d.ts.map +1 -0
- package/dist/query/fetch.js +4 -0
- package/dist/query/fetch.js.map +1 -0
- package/dist/query/index.d.ts +8 -0
- package/dist/query/index.d.ts.map +1 -0
- package/dist/query/index.js +4 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/infer.d.ts +25 -0
- package/dist/query/infer.d.ts.map +1 -0
- package/dist/query/infer.js +2 -0
- package/dist/query/infer.js.map +1 -0
- package/dist/query/react.d.ts +3 -0
- package/dist/query/react.d.ts.map +1 -0
- package/dist/query/react.js +2 -0
- package/dist/query/react.js.map +1 -0
- package/dist/query/realtime-flush.d.ts +26 -0
- package/dist/query/realtime-flush.d.ts.map +1 -0
- package/dist/query/realtime-flush.js +45 -0
- package/dist/query/realtime-flush.js.map +1 -0
- package/dist/query/realtime-stream.d.ts +3 -0
- package/dist/query/realtime-stream.d.ts.map +1 -0
- package/dist/query/realtime-stream.js +3 -0
- package/dist/query/realtime-stream.js.map +1 -0
- package/dist/query/realtime.d.ts +88 -0
- package/dist/query/realtime.d.ts.map +1 -0
- package/dist/query/realtime.js +255 -0
- package/dist/query/realtime.js.map +1 -0
- package/dist/realtime/facade.d.ts +3 -1
- package/dist/realtime/facade.d.ts.map +1 -1
- package/dist/realtime/facade.js +2 -1
- package/dist/realtime/facade.js.map +1 -1
- package/dist/realtime/filter.d.ts.map +1 -1
- package/dist/realtime/filter.js +1 -0
- package/dist/realtime/filter.js.map +1 -1
- package/dist/realtime/publisher.d.ts +2 -0
- package/dist/realtime/publisher.d.ts.map +1 -1
- package/dist/realtime/publisher.js.map +1 -1
- package/dist/start/auth-client.d.ts +11 -0
- package/dist/start/auth-client.d.ts.map +1 -0
- package/dist/start/auth-client.js +15 -0
- package/dist/start/auth-client.js.map +1 -0
- package/dist/start/index.d.ts +66 -0
- package/dist/start/index.d.ts.map +1 -0
- package/dist/start/index.js +49 -0
- package/dist/start/index.js.map +1 -0
- package/dist/start/isomorphic-fetch.d.ts +13 -0
- package/dist/start/isomorphic-fetch.d.ts.map +1 -0
- package/dist/start/isomorphic-fetch.js +33 -0
- package/dist/start/isomorphic-fetch.js.map +1 -0
- package/dist/sync/collection.d.ts +106 -0
- package/dist/sync/collection.d.ts.map +1 -0
- package/dist/sync/collection.js +258 -0
- package/dist/sync/collection.js.map +1 -0
- package/dist/sync/index.d.ts +8 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +4 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/realtime-sync.d.ts +31 -0
- package/dist/sync/realtime-sync.d.ts.map +1 -0
- package/dist/sync/realtime-sync.js +46 -0
- package/dist/sync/realtime-sync.js.map +1 -0
- package/dist/sync/sync-client.d.ts +28 -0
- package/dist/sync/sync-client.d.ts.map +1 -0
- package/dist/sync/sync-client.js +52 -0
- package/dist/sync/sync-client.js.map +1 -0
- package/dist/sync/update-queue.d.ts +27 -0
- package/dist/sync/update-queue.d.ts.map +1 -0
- package/dist/sync/update-queue.js +66 -0
- package/dist/sync/update-queue.js.map +1 -0
- package/package.json +107 -3
- package/skills/creating-bunderstack-apps/references/runtime-integrations.md +1 -1
- package/skills/migrating-to-bunderstack/references/runtime-replacements.md +2 -2
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { hashKey, notifyManager } from '@tanstack/query-core';
|
|
2
|
+
import { createFlushScheduler } from './realtime-flush.js';
|
|
3
|
+
import { openRealtimeStream } from './realtime-stream.js';
|
|
4
|
+
function tableQueryKey(api, table) {
|
|
5
|
+
return api[table]?.key?.({ type: 'query' }) ?? [[table], { type: 'query' }];
|
|
6
|
+
}
|
|
7
|
+
function detailQueryKey(api, table, id) {
|
|
8
|
+
return (api[table]?.get?.queryKey?.({ input: { id } }) ?? [
|
|
9
|
+
[table, 'get'],
|
|
10
|
+
{ type: 'query', input: { id } },
|
|
11
|
+
]);
|
|
12
|
+
}
|
|
13
|
+
function listQueryKey(api, table) {
|
|
14
|
+
return (api[table]?.list?.key?.({ type: 'query' }) ?? [
|
|
15
|
+
[table, 'list'],
|
|
16
|
+
{ type: 'query' },
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
function isListSnapshot(value) {
|
|
20
|
+
return (typeof value === 'object' &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
Array.isArray(value.items));
|
|
23
|
+
}
|
|
24
|
+
/** Dates arrive as Dates over the wire, so identity is not enough. */
|
|
25
|
+
function sameValue(a, b) {
|
|
26
|
+
if (a instanceof Date && b instanceof Date)
|
|
27
|
+
return a.getTime() === b.getTime();
|
|
28
|
+
return a === b;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Bunderstack's filter contract is narrow by design — a scalar is `=`, an
|
|
32
|
+
* array is `IN`, and `null` is `IS NULL` — so membership is decidable against
|
|
33
|
+
* a single record without asking the server.
|
|
34
|
+
*/
|
|
35
|
+
function matchesFilters(record, filters) {
|
|
36
|
+
for (const [column, expected] of Object.entries(filters ?? {})) {
|
|
37
|
+
if (expected === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
const actual = record[column];
|
|
40
|
+
if (expected === null) {
|
|
41
|
+
if (actual !== null && actual !== undefined)
|
|
42
|
+
return false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(expected)) {
|
|
46
|
+
if (!expected.some((value) => sameValue(value, actual)))
|
|
47
|
+
return false;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!sameValue(expected, actual))
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
function compareValues(a, b) {
|
|
56
|
+
if (a instanceof Date && b instanceof Date)
|
|
57
|
+
return a.getTime() - b.getTime();
|
|
58
|
+
if (typeof a === 'number' && typeof b === 'number')
|
|
59
|
+
return a - b;
|
|
60
|
+
if (typeof a === 'boolean' && typeof b === 'boolean')
|
|
61
|
+
return Number(a) - Number(b);
|
|
62
|
+
return String(a).localeCompare(String(b));
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where the row goes in an already-sorted page.
|
|
66
|
+
*
|
|
67
|
+
* Ties are common rather than exotic: a `timestamp` column stores seconds, so
|
|
68
|
+
* rows written in the same second share a sort value. SQL does not define an
|
|
69
|
+
* order within a tie, so the new row is placed as the most recent of its
|
|
70
|
+
* equals — first under `desc`, last under `asc` — which is where a reader
|
|
71
|
+
* expects a just-written row to appear.
|
|
72
|
+
*/
|
|
73
|
+
function insertionIndex(items, record, sort, order) {
|
|
74
|
+
const value = record[sort];
|
|
75
|
+
const index = items.findIndex((item) => {
|
|
76
|
+
const comparison = compareValues(value, item[sort]);
|
|
77
|
+
return order === 'desc' ? comparison >= 0 : comparison < 0;
|
|
78
|
+
});
|
|
79
|
+
return index === -1 ? items.length : index;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A cached page can only absorb an insert when it already holds every row the
|
|
83
|
+
* query would return. A partial page cannot: the new row may belong to another
|
|
84
|
+
* page, and inserting would shift the boundary the server drew.
|
|
85
|
+
*/
|
|
86
|
+
function holdsCompleteResult(snapshot) {
|
|
87
|
+
return (snapshot.hasMore === false &&
|
|
88
|
+
!snapshot.cursor &&
|
|
89
|
+
(snapshot.offset === undefined || snapshot.offset === 0));
|
|
90
|
+
}
|
|
91
|
+
function readInput(queryKey) {
|
|
92
|
+
const meta = queryKey[1];
|
|
93
|
+
const input = meta?.input;
|
|
94
|
+
return typeof input === 'object' && input !== null
|
|
95
|
+
? input
|
|
96
|
+
: {};
|
|
97
|
+
}
|
|
98
|
+
export function syncRealtime(options) {
|
|
99
|
+
const scheduler = createFlushScheduler(options.notifyScheduler ?? 'frame', options.clock);
|
|
100
|
+
/** Changes waiting for the next flush, in arrival order. */
|
|
101
|
+
const buffered = [];
|
|
102
|
+
/**
|
|
103
|
+
* Query keys the flush must invalidate, deduplicated by their hash. Keyed
|
|
104
|
+
* this way rather than by table so a list that cannot absorb a change still
|
|
105
|
+
* invalidates only itself, while a burst against one key collapses to one
|
|
106
|
+
* call.
|
|
107
|
+
*/
|
|
108
|
+
const stale = new Map();
|
|
109
|
+
const invalidateAll = async () => {
|
|
110
|
+
await Promise.all(options.tables.map((table) => options.queryClient.invalidateQueries({
|
|
111
|
+
queryKey: tableQueryKey(options.api, table),
|
|
112
|
+
})));
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Write the change into every cached list for the table, marking the table
|
|
116
|
+
* stale for any list whose membership or ordering cannot be settled locally.
|
|
117
|
+
* A list is only patched when the answer is certain.
|
|
118
|
+
*/
|
|
119
|
+
const patchLists = (change) => {
|
|
120
|
+
const prefix = listQueryKey(options.api, change.table);
|
|
121
|
+
const queries = options.queryClient
|
|
122
|
+
.getQueryCache()
|
|
123
|
+
.findAll({ queryKey: prefix });
|
|
124
|
+
const id = change.record['id'];
|
|
125
|
+
for (const query of queries) {
|
|
126
|
+
const snapshot = query.state.data;
|
|
127
|
+
if (!isListSnapshot(snapshot))
|
|
128
|
+
continue;
|
|
129
|
+
const queryKey = query.queryKey;
|
|
130
|
+
const items = snapshot.items;
|
|
131
|
+
const present = items.some((item) => sameValue(item['id'], id));
|
|
132
|
+
if (change.action === 'delete') {
|
|
133
|
+
if (!present)
|
|
134
|
+
continue;
|
|
135
|
+
options.queryClient.setQueryData(queryKey, {
|
|
136
|
+
...snapshot,
|
|
137
|
+
items: items.filter((item) => !sameValue(item['id'], id)),
|
|
138
|
+
...(typeof snapshot.total === 'number'
|
|
139
|
+
? { total: Math.max(0, snapshot.total - 1) }
|
|
140
|
+
: {}),
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const input = readInput(queryKey);
|
|
145
|
+
const belongs = matchesFilters(change.record, input['filters']);
|
|
146
|
+
// An update can move a row out of a filtered list.
|
|
147
|
+
if (!belongs) {
|
|
148
|
+
if (!present)
|
|
149
|
+
continue;
|
|
150
|
+
options.queryClient.setQueryData(queryKey, {
|
|
151
|
+
...snapshot,
|
|
152
|
+
items: items.filter((item) => !sameValue(item['id'], id)),
|
|
153
|
+
...(typeof snapshot.total === 'number'
|
|
154
|
+
? { total: Math.max(0, snapshot.total - 1) }
|
|
155
|
+
: {}),
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
// Already here: replace in place and keep the row where it is.
|
|
160
|
+
if (present) {
|
|
161
|
+
options.queryClient.setQueryData(queryKey, {
|
|
162
|
+
...snapshot,
|
|
163
|
+
items: items.map((item) => sameValue(item['id'], id) ? change.record : item),
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// The row joins this list. Only safe on a page that holds everything,
|
|
168
|
+
// and only when `q` is absent — a text match needs the server's
|
|
169
|
+
// searchable columns to evaluate.
|
|
170
|
+
const searched = snapshot.q ?? input['q'];
|
|
171
|
+
const sort = snapshot.sort ?? input['sort'];
|
|
172
|
+
const order = snapshot.order ?? input['order'];
|
|
173
|
+
if (searched || !sort || !order || !holdsCompleteResult(snapshot)) {
|
|
174
|
+
stale.set(hashKey(queryKey), queryKey);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const next = items.slice();
|
|
178
|
+
next.splice(insertionIndex(items, change.record, sort, order), 0, change.record);
|
|
179
|
+
options.queryClient.setQueryData(queryKey, {
|
|
180
|
+
...snapshot,
|
|
181
|
+
items: next,
|
|
182
|
+
...(typeof snapshot.total === 'number'
|
|
183
|
+
? { total: snapshot.total + 1 }
|
|
184
|
+
: {}),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const applyOne = (change) => {
|
|
189
|
+
// The runtime shape is the same either way; the generic only narrows what
|
|
190
|
+
// callers see.
|
|
191
|
+
options.onChange?.(change);
|
|
192
|
+
const id = change.record['id'];
|
|
193
|
+
if (id !== undefined) {
|
|
194
|
+
const queryKey = detailQueryKey(options.api, change.table, id);
|
|
195
|
+
if (change.action === 'delete')
|
|
196
|
+
options.queryClient.removeQueries({ queryKey });
|
|
197
|
+
else
|
|
198
|
+
options.queryClient.setQueryData(queryKey, change.record);
|
|
199
|
+
}
|
|
200
|
+
if (options.apply === 'patch') {
|
|
201
|
+
patchLists(change);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const tableKey = tableQueryKey(options.api, change.table);
|
|
205
|
+
stale.set(hashKey(tableKey), tableKey);
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* One pass over the buffer, then one invalidation per stale table. The
|
|
209
|
+
* second part is the point: a burst of changes to a table used to issue one
|
|
210
|
+
* `invalidateQueries` each, and now issues one in total.
|
|
211
|
+
*/
|
|
212
|
+
const flush = () => {
|
|
213
|
+
if (buffered.length === 0)
|
|
214
|
+
return;
|
|
215
|
+
const batch = buffered.splice(0, buffered.length);
|
|
216
|
+
notifyManager.batch(() => {
|
|
217
|
+
for (const change of batch)
|
|
218
|
+
applyOne(change);
|
|
219
|
+
for (const queryKey of stale.values())
|
|
220
|
+
void options.queryClient.invalidateQueries({ queryKey });
|
|
221
|
+
stale.clear();
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
const stream = openRealtimeStream({
|
|
225
|
+
subscribe: ({ signal, lastEventId }) => options.api.realtime.changes.call({ tables: options.tables }, { signal, lastEventId }),
|
|
226
|
+
onChange: (change) => {
|
|
227
|
+
buffered.push(change);
|
|
228
|
+
scheduler.schedule(flush);
|
|
229
|
+
},
|
|
230
|
+
onReconnect: async () => {
|
|
231
|
+
// A refetch of everything supersedes whatever was still buffered.
|
|
232
|
+
scheduler.cancel();
|
|
233
|
+
buffered.length = 0;
|
|
234
|
+
stale.clear();
|
|
235
|
+
await invalidateAll();
|
|
236
|
+
await options.onReconnect?.();
|
|
237
|
+
},
|
|
238
|
+
onError: options.onError,
|
|
239
|
+
onRetry: options.onRetry,
|
|
240
|
+
signal: options.signal ?? new AbortController().signal,
|
|
241
|
+
retryMs: options.retryMs,
|
|
242
|
+
maxRetryMs: options.maxRetryMs,
|
|
243
|
+
clock: options.clock,
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
close: () => {
|
|
247
|
+
stream.close();
|
|
248
|
+
// Anything already delivered is still owed to the cache.
|
|
249
|
+
scheduler.cancel();
|
|
250
|
+
flush();
|
|
251
|
+
},
|
|
252
|
+
done: stream.done,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=realtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime.js","sourceRoot":"","sources":["../../src/query/realtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type { QueryClient, QueryKey } from '@tanstack/query-core'\n\nimport { hashKey, notifyManager } from '@tanstack/query-core'\n\nimport type {\n AnyBunderstackApp,\n InferSchema,\n InferSelect,\n InferTables,\n} from './infer'\nimport type { NotifyScheduler, RealtimeClock } from './realtime-flush'\nimport type {\n RealtimeAction,\n RealtimeChange,\n RealtimeProcedure,\n RealtimeSyncHandle,\n} from './realtime-stream'\n\nimport { createFlushScheduler } from './realtime-flush'\nimport { openRealtimeStream } from './realtime-stream'\n\n// The event types live with the stream that produces them; re-exported here so\n// the package's public surface is unchanged.\nexport type { NotifyScheduler, RealtimeClock } from './realtime-flush'\nexport type {\n RealtimeAction,\n RealtimeChange,\n RealtimeEvent,\n RealtimeHeartbeat,\n RealtimeProcedure,\n RealtimeSyncHandle,\n} from './realtime-stream'\n\n/**\n * The change for one table, with `record` typed as that table's row.\n *\n * Passing the app type to `syncRealtime` turns `tables` into a checked list of\n * exposed table names and `onChange` into a union discriminated by `table`, so\n * `change.table === 'todos'` narrows `change.record` to the todo row:\n *\n * ```ts\n * syncRealtime<App>({\n * api,\n * queryClient,\n * tables: ['todos'],\n * onChange: (change) => {\n * if (change.table === 'todos') console.log(change.record.done)\n * },\n * })\n * ```\n */\nexport type RealtimeChangeFor<\n TApp extends AnyBunderstackApp,\n TTable extends InferTables<TApp>,\n> = {\n table: TTable\n action: RealtimeAction\n record: InferSelect<InferSchema<TApp>[TTable]>\n operationId?: string\n}\n\n/**\n * Untyped when no app type is supplied, so existing callers — and consumers\n * that only have a structural client — keep the loose `RealtimeChange`.\n */\nexport type RealtimeChangeOf<TApp, TTable extends string> = [TApp] extends [\n never,\n]\n ? RealtimeChange\n : TApp extends AnyBunderstackApp\n ? TTable extends InferTables<TApp>\n ? RealtimeChangeFor<TApp, TTable>\n : RealtimeChange\n : RealtimeChange\n\n/** Every exposed table name, or any string when no app type is supplied. */\nexport type RealtimeTableName<TApp> = [TApp] extends [never]\n ? string\n : TApp extends AnyBunderstackApp\n ? InferTables<TApp>\n : string\n\nexport type RealtimeQueryApi = {\n realtime: { changes: RealtimeProcedure }\n [table: string]: any\n}\n\n/**\n * How a change reaches the cache.\n *\n * `invalidate` (default) marks the table's queries stale and lets TanStack\n * Query refetch them — always correct, one request per change.\n *\n * `patch` writes the change into cached list results instead, so a write costs\n * no extra request. The event carries the action and the full row, and\n * Bunderstack's list contract is narrow enough to decide membership locally:\n * filters are `=`, `IN`, or `IS NULL`, and ordering is a single column. Any\n * list where that is not decidable — a text search, or a page that is not the\n * complete result — falls back to invalidation.\n */\nexport type RealtimeApplyStrategy = 'invalidate' | 'patch'\n\nexport type RealtimeSyncOptions<\n TApp = never,\n TTable extends string = RealtimeTableName<TApp>,\n> = {\n api: RealtimeQueryApi\n queryClient: QueryClient\n tables: TTable[]\n signal?: AbortSignal\n /** Defaults to `'invalidate'`. */\n apply?: RealtimeApplyStrategy\n /**\n * When buffered changes reach the cache. Defaults to `'frame'`, which\n * collapses a burst into one cache write and one invalidation per table.\n * Pass `'sync'` to write as each change arrives, as versions before 0.18 did.\n */\n notifyScheduler?: NotifyScheduler\n /** Timer functions, injectable for tests. */\n clock?: RealtimeClock\n /** Initial reconnect delay before jitter. Defaults to 1 second. */\n retryMs?: number\n /** Maximum reconnect delay before jitter. Defaults to 30 seconds. */\n maxRetryMs?: number\n onChange?: (change: RealtimeChangeOf<TApp, TTable>) => void\n onReconnect?: () => void | Promise<void>\n onError?: (error: unknown) => void\n onRetry?: (retry: { attempt: number; delayMs: number }) => void\n}\n\nfunction tableQueryKey(api: RealtimeQueryApi, table: string): QueryKey {\n return api[table]?.key?.({ type: 'query' }) ?? [[table], { type: 'query' }]\n}\n\nfunction detailQueryKey(\n api: RealtimeQueryApi,\n table: string,\n id: unknown,\n): QueryKey {\n return (\n api[table]?.get?.queryKey?.({ input: { id } }) ?? [\n [table, 'get'],\n { type: 'query', input: { id } },\n ]\n )\n}\n\nfunction listQueryKey(api: RealtimeQueryApi, table: string): QueryKey {\n return (\n api[table]?.list?.key?.({ type: 'query' }) ?? [\n [table, 'list'],\n { type: 'query' },\n ]\n )\n}\n\n/** The shape every generated list endpoint returns, as far as patching cares. */\ntype ListSnapshot = {\n items: Record<string, unknown>[]\n hasMore?: boolean\n total?: number\n offset?: number\n cursor?: string\n q?: string\n sort?: string\n order?: 'asc' | 'desc'\n}\n\nfunction isListSnapshot(value: unknown): value is ListSnapshot {\n return (\n typeof value === 'object' &&\n value !== null &&\n Array.isArray((value as ListSnapshot).items)\n )\n}\n\n/** Dates arrive as Dates over the wire, so identity is not enough. */\nfunction sameValue(a: unknown, b: unknown): boolean {\n if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()\n return a === b\n}\n\n/**\n * Bunderstack's filter contract is narrow by design — a scalar is `=`, an\n * array is `IN`, and `null` is `IS NULL` — so membership is decidable against\n * a single record without asking the server.\n */\nfunction matchesFilters(\n record: Record<string, unknown>,\n filters: Record<string, unknown> | undefined,\n): boolean {\n for (const [column, expected] of Object.entries(filters ?? {})) {\n if (expected === undefined) continue\n const actual = record[column]\n if (expected === null) {\n if (actual !== null && actual !== undefined) return false\n continue\n }\n if (Array.isArray(expected)) {\n if (!expected.some((value) => sameValue(value, actual))) return false\n continue\n }\n if (!sameValue(expected, actual)) return false\n }\n return true\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime()\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (typeof a === 'boolean' && typeof b === 'boolean')\n return Number(a) - Number(b)\n return String(a).localeCompare(String(b))\n}\n\n/**\n * Where the row goes in an already-sorted page.\n *\n * Ties are common rather than exotic: a `timestamp` column stores seconds, so\n * rows written in the same second share a sort value. SQL does not define an\n * order within a tie, so the new row is placed as the most recent of its\n * equals — first under `desc`, last under `asc` — which is where a reader\n * expects a just-written row to appear.\n */\nfunction insertionIndex(\n items: Record<string, unknown>[],\n record: Record<string, unknown>,\n sort: string,\n order: 'asc' | 'desc',\n): number {\n const value = record[sort]\n const index = items.findIndex((item) => {\n const comparison = compareValues(value, item[sort])\n return order === 'desc' ? comparison >= 0 : comparison < 0\n })\n return index === -1 ? items.length : index\n}\n\n/**\n * A cached page can only absorb an insert when it already holds every row the\n * query would return. A partial page cannot: the new row may belong to another\n * page, and inserting would shift the boundary the server drew.\n */\nfunction holdsCompleteResult(snapshot: ListSnapshot): boolean {\n return (\n snapshot.hasMore === false &&\n !snapshot.cursor &&\n (snapshot.offset === undefined || snapshot.offset === 0)\n )\n}\n\nfunction readInput(queryKey: QueryKey): Record<string, unknown> {\n const meta = queryKey[1] as { input?: unknown } | undefined\n const input = meta?.input\n return typeof input === 'object' && input !== null\n ? (input as Record<string, unknown>)\n : {}\n}\n\nexport function syncRealtime<\n TApp = never,\n TTable extends string = RealtimeTableName<TApp>,\n>(options: RealtimeSyncOptions<TApp, TTable>): RealtimeSyncHandle {\n const scheduler = createFlushScheduler(\n options.notifyScheduler ?? 'frame',\n options.clock,\n )\n /** Changes waiting for the next flush, in arrival order. */\n const buffered: RealtimeChange[] = []\n /**\n * Query keys the flush must invalidate, deduplicated by their hash. Keyed\n * this way rather than by table so a list that cannot absorb a change still\n * invalidates only itself, while a burst against one key collapses to one\n * call.\n */\n const stale = new Map<string, QueryKey>()\n\n const invalidateAll = async () => {\n await Promise.all(\n options.tables.map((table) =>\n options.queryClient.invalidateQueries({\n queryKey: tableQueryKey(options.api, table),\n }),\n ),\n )\n }\n\n /**\n * Write the change into every cached list for the table, marking the table\n * stale for any list whose membership or ordering cannot be settled locally.\n * A list is only patched when the answer is certain.\n */\n const patchLists = (change: RealtimeChange) => {\n const prefix = listQueryKey(options.api, change.table)\n const queries = options.queryClient\n .getQueryCache()\n .findAll({ queryKey: prefix })\n const id = change.record['id']\n\n for (const query of queries) {\n const snapshot = query.state.data\n if (!isListSnapshot(snapshot)) continue\n\n const queryKey = query.queryKey\n const items = snapshot.items\n const present = items.some((item) => sameValue(item['id'], id))\n\n if (change.action === 'delete') {\n if (!present) continue\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.filter((item) => !sameValue(item['id'], id)),\n ...(typeof snapshot.total === 'number'\n ? { total: Math.max(0, snapshot.total - 1) }\n : {}),\n })\n continue\n }\n\n const input = readInput(queryKey)\n const belongs = matchesFilters(\n change.record,\n input['filters'] as Record<string, unknown> | undefined,\n )\n\n // An update can move a row out of a filtered list.\n if (!belongs) {\n if (!present) continue\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.filter((item) => !sameValue(item['id'], id)),\n ...(typeof snapshot.total === 'number'\n ? { total: Math.max(0, snapshot.total - 1) }\n : {}),\n })\n continue\n }\n\n // Already here: replace in place and keep the row where it is.\n if (present) {\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: items.map((item) =>\n sameValue(item['id'], id) ? change.record : item,\n ),\n })\n continue\n }\n\n // The row joins this list. Only safe on a page that holds everything,\n // and only when `q` is absent — a text match needs the server's\n // searchable columns to evaluate.\n const searched = snapshot.q ?? (input['q'] as string | undefined)\n const sort = snapshot.sort ?? (input['sort'] as string | undefined)\n const order =\n snapshot.order ?? (input['order'] as 'asc' | 'desc' | undefined)\n\n if (searched || !sort || !order || !holdsCompleteResult(snapshot)) {\n stale.set(hashKey(queryKey), queryKey)\n continue\n }\n\n const next = items.slice()\n next.splice(\n insertionIndex(items, change.record, sort, order),\n 0,\n change.record,\n )\n options.queryClient.setQueryData(queryKey, {\n ...snapshot,\n items: next,\n ...(typeof snapshot.total === 'number'\n ? { total: snapshot.total + 1 }\n : {}),\n })\n }\n }\n\n const applyOne = (change: RealtimeChange) => {\n // The runtime shape is the same either way; the generic only narrows what\n // callers see.\n options.onChange?.(change as RealtimeChangeOf<TApp, TTable>)\n const id = change.record['id']\n if (id !== undefined) {\n const queryKey = detailQueryKey(options.api, change.table, id)\n if (change.action === 'delete')\n options.queryClient.removeQueries({ queryKey })\n else options.queryClient.setQueryData(queryKey, change.record)\n }\n\n if (options.apply === 'patch') {\n patchLists(change)\n return\n }\n\n const tableKey = tableQueryKey(options.api, change.table)\n stale.set(hashKey(tableKey), tableKey)\n }\n\n /**\n * One pass over the buffer, then one invalidation per stale table. The\n * second part is the point: a burst of changes to a table used to issue one\n * `invalidateQueries` each, and now issues one in total.\n */\n const flush = () => {\n if (buffered.length === 0) return\n const batch = buffered.splice(0, buffered.length)\n notifyManager.batch(() => {\n for (const change of batch) applyOne(change)\n for (const queryKey of stale.values())\n void options.queryClient.invalidateQueries({ queryKey })\n stale.clear()\n })\n }\n\n const stream = openRealtimeStream({\n subscribe: ({ signal, lastEventId }) =>\n options.api.realtime.changes.call(\n { tables: options.tables },\n { signal, lastEventId },\n ),\n onChange: (change) => {\n buffered.push(change)\n scheduler.schedule(flush)\n },\n onReconnect: async () => {\n // A refetch of everything supersedes whatever was still buffered.\n scheduler.cancel()\n buffered.length = 0\n stale.clear()\n await invalidateAll()\n await options.onReconnect?.()\n },\n onError: options.onError,\n onRetry: options.onRetry,\n signal: options.signal ?? new AbortController().signal,\n retryMs: options.retryMs,\n maxRetryMs: options.maxRetryMs,\n clock: options.clock,\n })\n\n return {\n close: () => {\n stream.close()\n // Anything already delivered is still owed to the cache.\n scheduler.cancel()\n flush()\n },\n done: stream.done,\n }\n}\n"]}
|
|
@@ -5,7 +5,9 @@ export type SchemaTable<TSchema extends Record<string, unknown>> = Extract<TSche
|
|
|
5
5
|
export interface RealtimeFacade<TSchema extends Record<string, unknown> = Record<string, unknown>> {
|
|
6
6
|
readonly enabled: boolean;
|
|
7
7
|
readonly transport: RealtimeTransport;
|
|
8
|
-
publish<TTable extends SchemaTable<TSchema>>(table: TTable, action: RealtimeAction, record: InferSelectModel<TTable
|
|
8
|
+
publish<TTable extends SchemaTable<TSchema>>(table: TTable, action: RealtimeAction, record: InferSelectModel<TTable>, metadata?: {
|
|
9
|
+
operationId?: string;
|
|
10
|
+
}): Promise<void>;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* One name for a table everywhere: events, subscriptions, and the CRUD router
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../../src/realtime/facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,KAAK,EACX,MAAM,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpE,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE/D,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,OAAO,CACxE,OAAO,CAAC,MAAM,OAAO,CAAC,EACtB,KAAK,CACN,CAAA;AAED,MAAM,WAAW,cAAc,CAC7B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAA;IAErC,OAAO,CAAC,MAAM,SAAS,WAAW,CAAC,OAAO,CAAC,EACzC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"facade.d.ts","sourceRoot":"","sources":["../../src/realtime/facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,KAAK,EACX,MAAM,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAEpE,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE/D,MAAM,MAAM,WAAW,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,OAAO,CACxE,OAAO,CAAC,MAAM,OAAO,CAAC,EACtB,KAAK,CACN,CAAA;AAED,MAAM,WAAW,cAAc,CAC7B,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAEjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAA;IAErC,OAAO,CAAC,MAAM,SAAS,WAAW,CAAC,OAAO,CAAC,EACzC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,EAChC,QAAQ,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAClC,OAAO,CAAC,IAAI,CAAC,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,SAAS,CAAC,EAAE,iBAAiB,EAC7B,SAAS,GAAE,iBAAqD,EAChE,MAAM,CAAC,EAAE,OAAO,GACf,cAAc,CAAC,OAAO,CAAC,CA+BzB"}
|
package/dist/realtime/facade.js
CHANGED
|
@@ -21,7 +21,7 @@ export function createRealtimeFacade(publisher, transport = publisher ? 'memory'
|
|
|
21
21
|
return {
|
|
22
22
|
enabled: publisher !== undefined,
|
|
23
23
|
transport,
|
|
24
|
-
async publish(table, action, record) {
|
|
24
|
+
async publish(table, action, record, metadata) {
|
|
25
25
|
if (!publisher)
|
|
26
26
|
return;
|
|
27
27
|
const tableName = getTableName(table);
|
|
@@ -29,6 +29,7 @@ export function createRealtimeFacade(publisher, transport = publisher ? 'memory'
|
|
|
29
29
|
table: keyByTableName.get(tableName) ?? tableName,
|
|
30
30
|
action,
|
|
31
31
|
record: record,
|
|
32
|
+
...(metadata?.operationId ? { operationId: metadata.operationId } : {}),
|
|
32
33
|
});
|
|
33
34
|
},
|
|
34
35
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"facade.js","sourceRoot":"","sources":["../../src/realtime/facade.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,OAAO,GAGR,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"facade.js","sourceRoot":"","sources":["../../src/realtime/facade.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,OAAO,GAGR,MAAM,aAAa,CAAA;AAyBpB;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,SAA6B,EAC7B,YAA+B,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,UAAU,EAChE,MAAgB;IAEhB,IAAI,CAAC,SAAS,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAA;IACH,CAAC;IACD,IAAI,SAAS,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,sEAAsE,CACvE,CAAA;IACH,CAAC;IAED,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;IAChD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;QACxD,IAAI,OAAO,CAAC,KAAK,CAAC;YAAE,cAAc,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAA;IAClE,CAAC;IAED,OAAO;QACL,OAAO,EAAE,SAAS,KAAK,SAAS;QAChC,SAAS;QACT,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ;YAC3C,IAAI,CAAC,SAAS;gBAAE,OAAM;YACtB,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,CAAA;YACrC,MAAM,SAAS,CAAC,OAAO,CAAC,QAAQ,EAAE;gBAChC,KAAK,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,SAAS;gBACjD,MAAM;gBACN,MAAM,EAAE,MAA4C;gBACpD,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACxE,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import {\n getTableName,\n isTable,\n type InferSelectModel,\n type Table,\n} from 'drizzle-orm'\n\nimport type { RealtimeAction, RealtimePublisher } from './publisher'\n\nexport type RealtimeTransport = 'disabled' | 'memory' | 'redis'\n\nexport type SchemaTable<TSchema extends Record<string, unknown>> = Extract<\n TSchema[keyof TSchema],\n Table\n>\n\nexport interface RealtimeFacade<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n> {\n readonly enabled: boolean\n readonly transport: RealtimeTransport\n\n publish<TTable extends SchemaTable<TSchema>>(\n table: TTable,\n action: RealtimeAction,\n record: InferSelectModel<TTable>,\n metadata?: { operationId?: string },\n ): Promise<void>\n}\n\n/**\n * One name for a table everywhere: events, subscriptions, and the CRUD router\n * all use the schema key. Pass the schema so a key like `creditBalances` is not\n * published as its SQL name `credit_balances` — clients subscribe with the key\n * they call procedures with. Without a schema, the SQL name is the only name\n * available and is used as-is.\n */\nexport function createRealtimeFacade<TSchema extends Record<string, unknown>>(\n publisher?: RealtimePublisher,\n transport: RealtimeTransport = publisher ? 'memory' : 'disabled',\n schema?: TSchema,\n): RealtimeFacade<TSchema> {\n if (!publisher && transport !== 'disabled') {\n throw new Error(\n '[bunderstack] an enabled realtime transport requires a publisher',\n )\n }\n if (publisher && transport === 'disabled') {\n throw new Error(\n '[bunderstack] a realtime publisher cannot use the disabled transport',\n )\n }\n\n const keyByTableName = new Map<string, string>()\n for (const [key, value] of Object.entries(schema ?? {})) {\n if (isTable(value)) keyByTableName.set(getTableName(value), key)\n }\n\n return {\n enabled: publisher !== undefined,\n transport,\n async publish(table, action, record, metadata) {\n if (!publisher) return\n const tableName = getTableName(table)\n await publisher.publish('change', {\n table: keyByTableName.get(tableName) ?? tableName,\n action,\n record: record as unknown as Record<string, unknown>,\n ...(metadata?.operationId ? { operationId: metadata.operationId } : {}),\n })\n },\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/realtime/filter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,mBAAmB,EACpB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,KAAK,UAAU,GAAG,MAAM,OAAO,CAAC;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IACvB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC,CAAC,CAAA;AAEF,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,mBAAmB,CAAA;IAC1B;;;OAGG;IACH,IAAI,EAAE,aAAa,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GACzE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../src/realtime/filter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,cAAc,EACd,mBAAmB,EACpB,MAAM,WAAW,CAAA;AAClB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAIjD,KAAK,UAAU,GAAG,MAAM,OAAO,CAAC;IAC9B,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IACvB,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC,CAAC,CAAA;AAEF,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,MAAM,EAAE,cAAc,CAAA;IACtB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,mBAAmB,CAAA;IAC1B;;;OAGG;IACH,IAAI,EAAE,aAAa,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GACzE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,OAAO,CAAC,CA+B9C;AAcD,wBAAuB,qBAAqB,CAC1C,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,EACrC,OAAO,EAAE,4BAA4B,GACpC,cAAc,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,CAiC5C;AAED;;;GAGG;AACH,wBAAuB,kBAAkB,CACvC,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,EACrC,OAAO,EAAE,yBAAyB,GACjC,cAAc,CAAC,cAAc,EAAE,IAAI,EAAE,IAAI,CAAC,CAO5C"}
|
package/dist/realtime/filter.js
CHANGED
|
@@ -37,6 +37,7 @@ function project(change) {
|
|
|
37
37
|
table: change.table,
|
|
38
38
|
action: change.action,
|
|
39
39
|
record: change.record,
|
|
40
|
+
...(change.operationId ? { operationId: change.operationId } : {}),
|
|
40
41
|
};
|
|
41
42
|
const meta = getEventMeta(change);
|
|
42
43
|
return meta ? withEventMeta(projected, meta) : projected;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/realtime/filter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAU1D,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AA2B3E;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAA0B,EAC1B,OAA0E;IAE1E,IAAI,cAAkD,CAAA;IACtD,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,CAAC,cAAc,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,CAAA;IAElE,OAAO,KAAK,EAAE,MAAM,EAAE,EAAE;QACtB,IAAI,CAAC,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,KAAK,CAAA;QAE3D,MAAM,YAAY,GAChB,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,CAAA;QAC5D,MAAM,OAAO,GAAG,YAAY;YAC1B,CAAC,CAAC,MAAM,UAAU,EAAE;YACpB,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,CAAA;QAC9C,MAAM,OAAO,GAAG;YACd,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,GAAG,EAAE,MAAM,CAAC,MAAM;YAClB,OAAO,EAAE,EAAE,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,EAAE;SAChE,CAAA;QACD,
|
|
1
|
+
{"version":3,"file":"filter.js","sourceRoot":"","sources":["../../src/realtime/filter.ts"],"names":[],"mappings":"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","sourcesContent":["import { getEventMeta, withEventMeta } from '@orpc/server'\n\nimport type {\n AccessUser,\n OperationRule,\n ResolvedAccess,\n ResolvedTableAccess,\n} from '../access'\nimport type { RealtimeChange } from './publisher'\n\nimport { checkAccess, rowMatchesScope, tableEntryForName } from '../access'\n\ntype GetSession = () => Promise<{\n user: AccessUser | null\n activeOrganizationId: string | null\n}>\n\nexport interface FilterRealtimeChangesOptions {\n subscriptions: readonly string[]\n access: ResolvedAccess\n request: Request\n getSession: GetSession\n}\n\nexport interface FilterTableChangesOptions {\n /** Events arrive under this name — the schema key they were published with. */\n tableName: string\n entry: ResolvedTableAccess\n /**\n * The operation right this stream reads under: `get` for a subscription to\n * rows, `list` for a live view of a query.\n */\n rule: OperationRule\n request: Request\n getSession: GetSession\n}\n\n/**\n * Whether one change may reach this subscriber: the given right plus the read\n * scope of the row, evaluated against the caller's session. The session\n * resolves at most once per stream.\n */\nexport function createChangeGuard(\n entry: ResolvedTableAccess,\n options: { rule: OperationRule; request: Request; getSession: GetSession },\n): (change: RealtimeChange) => Promise<boolean> {\n let sessionPromise: ReturnType<GetSession> | undefined\n const getSession = () => (sessionPromise ??= options.getSession())\n\n return async (change) => {\n if (!entry.enabled || options.rule === 'deny') return false\n\n const needsSession =\n options.rule !== 'public' || entry.readScope !== undefined\n const session = needsSession\n ? await getSession()\n : { user: null, activeOrganizationId: null }\n const context = {\n request: options.request,\n user: session.user,\n row: change.record,\n session: { activeOrganizationId: session.activeOrganizationId },\n }\n if (\n !(await checkAccess(options.rule, context, entry.ownerColumn)).allowed\n ) {\n return false\n }\n if (\n entry.readScope &&\n !rowMatchesScope(change.record, entry.readScope(context))\n ) {\n return false\n }\n return true\n }\n}\n\n/** Preserve publisher event metadata (ids) across the projection. */\nfunction project(change: RealtimeChange): RealtimeChange {\n const projected: RealtimeChange = {\n table: change.table,\n action: change.action,\n record: change.record,\n ...(change.operationId ? { operationId: change.operationId } : {}),\n }\n const meta = getEventMeta(change)\n return meta ? withEventMeta(projected, meta) : projected\n}\n\nexport async function* filterRealtimeChanges(\n source: AsyncIterable<RealtimeChange>,\n options: FilterRealtimeChangesOptions,\n): AsyncGenerator<RealtimeChange, void, void> {\n const subscriptions = new Set(options.subscriptions)\n // One guard — and therefore one cached session — per table entry.\n const guards = new Map<\n ResolvedTableAccess,\n ReturnType<typeof createChangeGuard>\n >()\n\n for await (const change of source) {\n // Events name tables by schema key; the SQL-name lookup stays as a fallback\n // for publishers outside the CRUD path that only know the physical name.\n const entry =\n options.access.get(change.table) ??\n tableEntryForName(options.access, change.table)\n if (!entry?.enabled) continue\n\n const recordId = change.record.id\n if (\n !subscriptions.has(change.table) &&\n (recordId == null ||\n !subscriptions.has(`${change.table}/${String(recordId)}`))\n ) {\n continue\n }\n\n let guard = guards.get(entry)\n if (!guard) {\n guard = createChangeGuard(entry, { ...options, rule: entry.get })\n guards.set(entry, guard)\n }\n if (!(await guard(change))) continue\n yield project(change)\n }\n}\n\n/**\n * The same stream narrowed to one table with its access entry known up front —\n * what a live view (`GET /{table}:live`) consumes.\n */\nexport async function* filterTableChanges(\n source: AsyncIterable<RealtimeChange>,\n options: FilterTableChangesOptions,\n): AsyncGenerator<RealtimeChange, void, void> {\n const guard = createChangeGuard(options.entry, options)\n for await (const change of source) {\n if (change.table !== options.tableName) continue\n if (!(await guard(change))) continue\n yield project(change)\n }\n}\n"]}
|
|
@@ -5,6 +5,8 @@ export interface RealtimeChange {
|
|
|
5
5
|
table: string;
|
|
6
6
|
action: RealtimeAction;
|
|
7
7
|
record: Record<string, unknown>;
|
|
8
|
+
/** Correlates this confirmed write with the client operation that caused it. */
|
|
9
|
+
operationId?: string;
|
|
8
10
|
}
|
|
9
11
|
export interface RealtimeEvents extends Record<string, object> {
|
|
10
12
|
change: RealtimeChange;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/realtime/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAKtC,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE3D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"publisher.d.ts","sourceRoot":"","sources":["../../src/realtime/publisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAKtC,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE3D,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,cAAc,CAAA;IACtB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC/B,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,cAAe,SAAQ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAC5D,MAAM,EAAE,cAAc,CAAA;CACvB;AAED,MAAM,MAAM,iBAAiB,GAAG,SAAS,CAAC,cAAc,CAAC,CAAA;AAEzD,MAAM,WAAW,wBAAwB;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,6BAA6B,CAC3C,OAAO,GAAE,wBAA6B,GACrC,iBAAiB,CAKnB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,WAAW,EAClB,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,EAC9C,OAAO,GAAE,wBAAwB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3D,iBAAiB,CAOnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publisher.js","sourceRoot":"","sources":["../../src/realtime/publisher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"publisher.js","sourceRoot":"","sources":["../../src/realtime/publisher.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAuBxD,MAAM,UAAU,6BAA6B,CAC3C,UAAoC,EAAE;IAEtC,OAAO,IAAI,eAAe,CAAiB;QACzC,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,IAAI,GAAG,EAAE;KACjE,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,UAAU,4BAA4B,CAC1C,KAAkB,EAClB,UAA8C,EAC9C,UAA0D,EAAE;IAE5D,OAAO,IAAI,iBAAiB,CAAiB,KAAK,EAAE;QAClD,UAAU;QACV,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,cAAc;QACxC,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;QAC5C,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,aAAa,IAAI,GAAG,EAAE;KACjE,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { Publisher } from '@orpc/publisher'\nimport type { RedisClient } from 'bun'\n\nimport { BunRedisPublisher } from '@orpc/bun'\nimport { MemoryPublisher } from '@orpc/publisher/memory'\n\nexport type RealtimeAction = 'create' | 'update' | 'delete'\n\nexport interface RealtimeChange {\n table: string\n action: RealtimeAction\n record: Record<string, unknown>\n /** Correlates this confirmed write with the client operation that caused it. */\n operationId?: string\n}\n\nexport interface RealtimeEvents extends Record<string, object> {\n change: RealtimeChange\n}\n\nexport type RealtimePublisher = Publisher<RealtimeEvents>\n\nexport interface RealtimePublisherOptions {\n maxBufferedEvents?: number\n resumeSeconds?: number\n}\n\nexport function createMemoryRealtimePublisher(\n options: RealtimePublisherOptions = {},\n): RealtimePublisher {\n return new MemoryPublisher<RealtimeEvents>({\n maxBufferedEvents: options.maxBufferedEvents,\n resume: { enabled: true, seconds: options.resumeSeconds ?? 300 },\n })\n}\n\nexport function createRedisRealtimePublisher(\n redis: RedisClient,\n subscriber: RedisClient | Promise<RedisClient>,\n options: RealtimePublisherOptions & { prefix?: string } = {},\n): RealtimePublisher {\n return new BunRedisPublisher<RealtimeEvents>(redis, {\n subscriber,\n prefix: options.prefix ?? 'bunderstack:',\n maxBufferedEvents: options.maxBufferedEvents,\n resume: { enabled: true, seconds: options.resumeSeconds ?? 300 },\n })\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BetterAuth browser client pointed at the Bunderstack handler's
|
|
3
|
+
* /api/auth/* routes. Defaults to the current origin in the browser and
|
|
4
|
+
* APP_URL (or localhost:3000) during SSR.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createStartAuthClient(options?: {
|
|
7
|
+
baseURL?: string;
|
|
8
|
+
}): import("better-auth/react").ReactAuthClient<{
|
|
9
|
+
baseURL: string;
|
|
10
|
+
}>;
|
|
11
|
+
//# sourceMappingURL=auth-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../src/start/auth-client.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO;;GAQvE"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createAuthClient } from 'better-auth/react';
|
|
2
|
+
/**
|
|
3
|
+
* BetterAuth browser client pointed at the Bunderstack handler's
|
|
4
|
+
* /api/auth/* routes. Defaults to the current origin in the browser and
|
|
5
|
+
* APP_URL (or localhost:3000) during SSR.
|
|
6
|
+
*/
|
|
7
|
+
export function createStartAuthClient(options = {}) {
|
|
8
|
+
return createAuthClient({
|
|
9
|
+
baseURL: options.baseURL ??
|
|
10
|
+
(typeof window !== 'undefined'
|
|
11
|
+
? window.location.origin
|
|
12
|
+
: (process.env.APP_URL ?? 'http://localhost:3000')),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=auth-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-client.js","sourceRoot":"","sources":["../../src/start/auth-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAA;AAEpD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAgC,EAAE;IACtE,OAAO,gBAAgB,CAAC;QACtB,OAAO,EACL,OAAO,CAAC,OAAO;YACf,CAAC,OAAO,MAAM,KAAK,WAAW;gBAC5B,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM;gBACxB,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,uBAAuB,CAAC,CAAC;KACxD,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { createAuthClient } from 'better-auth/react'\n\n/**\n * BetterAuth browser client pointed at the Bunderstack handler's\n * /api/auth/* routes. Defaults to the current origin in the browser and\n * APP_URL (or localhost:3000) during SSR.\n */\nexport function createStartAuthClient(options: { baseURL?: string } = {}) {\n return createAuthClient({\n baseURL:\n options.baseURL ??\n (typeof window !== 'undefined'\n ? window.location.origin\n : (process.env.APP_URL ?? 'http://localhost:3000')),\n })\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { type AnyBunderstackApp, type BunderstackSyncClient } from '../sync/index.js';
|
|
3
|
+
export { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
4
|
+
export type BunderstackStartOptions = {
|
|
5
|
+
/** API mount point. Defaults to '/api'. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/** Default query staleTime (ms). Defaults to 30_000. */
|
|
8
|
+
staleTime?: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* One-call client setup for TanStack Start apps: SSR-aware fetch, sensible
|
|
12
|
+
* QueryClient defaults, and a sync client whose tables and buckets are
|
|
13
|
+
* inferred from the server app type.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // src/api.ts — NOT src/client.ts, which is a reserved TanStack Start
|
|
17
|
+
* // entry-point name (it would replace the hydration entry).
|
|
18
|
+
* import type { App } from './bunderstack'
|
|
19
|
+
* export const { createQueryClient, createApi } = bunderstackStart<App>()
|
|
20
|
+
*/
|
|
21
|
+
export declare function bunderstackStart<TApp extends AnyBunderstackApp>(options?: BunderstackStartOptions): {
|
|
22
|
+
createQueryClient: () => QueryClient;
|
|
23
|
+
createApi: (queryClient: QueryClient) => BunderstackSyncClient<TApp>;
|
|
24
|
+
};
|
|
25
|
+
type StartRequestContext = {
|
|
26
|
+
request: Request;
|
|
27
|
+
};
|
|
28
|
+
type StartHandler = (ctx: StartRequestContext) => Promise<Response>;
|
|
29
|
+
/**
|
|
30
|
+
* Handlers object for the catch-all API file route.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // src/routes/api/$.tsx
|
|
34
|
+
* export const Route = createFileRoute('/api/$')({
|
|
35
|
+
* server: { handlers: createApiHandlers(app) },
|
|
36
|
+
* })
|
|
37
|
+
*/
|
|
38
|
+
export declare function createApiHandlers(app: {
|
|
39
|
+
handler: (req: Request) => Promise<Response>;
|
|
40
|
+
}): {
|
|
41
|
+
GET: StartHandler;
|
|
42
|
+
POST: StartHandler;
|
|
43
|
+
PATCH: StartHandler;
|
|
44
|
+
DELETE: StartHandler;
|
|
45
|
+
};
|
|
46
|
+
export type SessionUser = {
|
|
47
|
+
id: string;
|
|
48
|
+
email: string;
|
|
49
|
+
name: string;
|
|
50
|
+
image?: string | null;
|
|
51
|
+
role?: string;
|
|
52
|
+
};
|
|
53
|
+
type SessionApp = {
|
|
54
|
+
auth: {
|
|
55
|
+
api: {
|
|
56
|
+
getSession: (opts: {
|
|
57
|
+
headers: Headers;
|
|
58
|
+
}) => Promise<{
|
|
59
|
+
user: SessionUser | null;
|
|
60
|
+
} | null>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Resolve the BetterAuth session user for an incoming request (server-side). */
|
|
65
|
+
export declare function getSessionUser(app: SessionApp, request: Request): Promise<SessionUser | null>;
|
|
66
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,eAAe,CAAA;AAGtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,MAAM,MAAM,uBAAuB,GAAG;IACpC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,SAAS,iBAAiB,EAC7D,OAAO,GAAE,uBAA4B;;6BAUV,WAAW,KAAG,qBAAqB,CAAC,IAAI,CAAC;EAOrE;AAED,KAAK,mBAAmB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAA;AAC/C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,mBAAmB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEnE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE;IACrC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7C,GAAG;IACF,GAAG,EAAE,YAAY,CAAA;IACjB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB,MAAM,EAAE,YAAY,CAAA;CACrB,CAGA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QACJ,GAAG,EAAE;YACH,UAAU,EAAE,CAAC,IAAI,EAAE;gBACjB,OAAO,EAAE,OAAO,CAAA;aACjB,KAAK,OAAO,CAAC;gBAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAC,CAAA;SACnD,CAAA;KACF,CAAA;CACF,CAAA;AAED,iFAAiF;AACjF,wBAAsB,cAAc,CAClC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAG7B"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { createSyncClient, } from '../sync/index.js';
|
|
3
|
+
import { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
4
|
+
export { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
5
|
+
/**
|
|
6
|
+
* One-call client setup for TanStack Start apps: SSR-aware fetch, sensible
|
|
7
|
+
* QueryClient defaults, and a sync client whose tables and buckets are
|
|
8
|
+
* inferred from the server app type.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // src/api.ts — NOT src/client.ts, which is a reserved TanStack Start
|
|
12
|
+
* // entry-point name (it would replace the hydration entry).
|
|
13
|
+
* import type { App } from './bunderstack'
|
|
14
|
+
* export const { createQueryClient, createApi } = bunderstackStart<App>()
|
|
15
|
+
*/
|
|
16
|
+
export function bunderstackStart(options = {}) {
|
|
17
|
+
const isoFetch = createIsomorphicFetch();
|
|
18
|
+
return {
|
|
19
|
+
createQueryClient: () => new QueryClient({
|
|
20
|
+
defaultOptions: {
|
|
21
|
+
queries: { staleTime: options.staleTime ?? 30_000 },
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
createApi: (queryClient) => createSyncClient({
|
|
25
|
+
queryClient,
|
|
26
|
+
fetch: isoFetch,
|
|
27
|
+
baseUrl: options.baseUrl,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Handlers object for the catch-all API file route.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // src/routes/api/$.tsx
|
|
36
|
+
* export const Route = createFileRoute('/api/$')({
|
|
37
|
+
* server: { handlers: createApiHandlers(app) },
|
|
38
|
+
* })
|
|
39
|
+
*/
|
|
40
|
+
export function createApiHandlers(app) {
|
|
41
|
+
const handle = ({ request }) => app.handler(request);
|
|
42
|
+
return { GET: handle, POST: handle, PATCH: handle, DELETE: handle };
|
|
43
|
+
}
|
|
44
|
+
/** Resolve the BetterAuth session user for an incoming request (server-side). */
|
|
45
|
+
export async function getSessionUser(app, request) {
|
|
46
|
+
const session = await app.auth.api.getSession({ headers: request.headers });
|
|
47
|
+
return session?.user ?? null;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EACL,gBAAgB,GAGjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAS1D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAmC,EAAE;IAErC,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAA;IACxC,OAAO;QACL,iBAAiB,EAAE,GAAG,EAAE,CACtB,IAAI,WAAW,CAAC;YACd,cAAc,EAAE;gBACd,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,MAAM,EAAE;aACpD;SACF,CAAC;QACJ,SAAS,EAAE,CAAC,WAAwB,EAA+B,EAAE,CACnE,gBAAgB,CAAO;YACrB,WAAW;YACX,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC;KACL,CAAA;AACH,CAAC;AAKD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAEjC;IAMC,MAAM,MAAM,GAAiB,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAClE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;AACrE,CAAC;AAoBD,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAe,EACf,OAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3E,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAA;AAC9B,CAAC","sourcesContent":["import { QueryClient } from '@tanstack/react-query'\n\nimport {\n createSyncClient,\n type AnyBunderstackApp,\n type BunderstackSyncClient,\n} from '../sync/index'\nimport { createIsomorphicFetch } from './isomorphic-fetch'\n\nexport { createIsomorphicFetch } from './isomorphic-fetch'\n\nexport type BunderstackStartOptions = {\n /** API mount point. Defaults to '/api'. */\n baseUrl?: string\n /** Default query staleTime (ms). Defaults to 30_000. */\n staleTime?: number\n}\n\n/**\n * One-call client setup for TanStack Start apps: SSR-aware fetch, sensible\n * QueryClient defaults, and a sync client whose tables and buckets are\n * inferred from the server app type.\n *\n * @example\n * // src/api.ts — NOT src/client.ts, which is a reserved TanStack Start\n * // entry-point name (it would replace the hydration entry).\n * import type { App } from './bunderstack'\n * export const { createQueryClient, createApi } = bunderstackStart<App>()\n */\nexport function bunderstackStart<TApp extends AnyBunderstackApp>(\n options: BunderstackStartOptions = {},\n) {\n const isoFetch = createIsomorphicFetch()\n return {\n createQueryClient: () =>\n new QueryClient({\n defaultOptions: {\n queries: { staleTime: options.staleTime ?? 30_000 },\n },\n }),\n createApi: (queryClient: QueryClient): BunderstackSyncClient<TApp> =>\n createSyncClient<TApp>({\n queryClient,\n fetch: isoFetch,\n baseUrl: options.baseUrl,\n }),\n }\n}\n\ntype StartRequestContext = { request: Request }\ntype StartHandler = (ctx: StartRequestContext) => Promise<Response>\n\n/**\n * Handlers object for the catch-all API file route.\n *\n * @example\n * // src/routes/api/$.tsx\n * export const Route = createFileRoute('/api/$')({\n * server: { handlers: createApiHandlers(app) },\n * })\n */\nexport function createApiHandlers(app: {\n handler: (req: Request) => Promise<Response>\n}): {\n GET: StartHandler\n POST: StartHandler\n PATCH: StartHandler\n DELETE: StartHandler\n} {\n const handle: StartHandler = ({ request }) => app.handler(request)\n return { GET: handle, POST: handle, PATCH: handle, DELETE: handle }\n}\n\nexport type SessionUser = {\n id: string\n email: string\n name: string\n image?: string | null\n role?: string\n}\n\ntype SessionApp = {\n auth: {\n api: {\n getSession: (opts: {\n headers: Headers\n }) => Promise<{ user: SessionUser | null } | null>\n }\n }\n}\n\n/** Resolve the BetterAuth session user for an incoming request (server-side). */\nexport async function getSessionUser(\n app: SessionApp,\n request: Request,\n): Promise<SessionUser | null> {\n const session = await app.auth.api.getSession({ headers: request.headers })\n return session?.user ?? null\n}\n"]}
|