@marktiderman/genesis-native 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +267 -2
- package/dist/block-Dh5ny06W.d.ts +535 -0
- package/dist/chunk-MMTPWUWI.js +36 -0
- package/dist/chunk-MMTPWUWI.js.map +1 -0
- package/dist/index.d.ts +412 -0
- package/dist/index.js +600 -0
- package/dist/index.js.map +1 -0
- package/dist/persist.d.ts +44 -0
- package/dist/persist.js +42 -0
- package/dist/persist.js.map +1 -0
- package/dist/query-DhH3lLFa.d.ts +65 -0
- package/dist/updates.d.ts +20 -0
- package/dist/updates.js +813 -0
- package/dist/updates.js.map +1 -0
- package/package.json +84 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { createNativeQueryClient, connectQueryLifecycle } from './chunk-MMTPWUWI.js';
|
|
2
|
+
export { DAY_MS, connectQueryLifecycle, createNativeQueryClient } from './chunk-MMTPWUWI.js';
|
|
3
|
+
import { createContext, useContext, useSyncExternalStore, Component, useRef, useEffect, useState } from 'react';
|
|
4
|
+
import { View, Text, Pressable } from 'react-native';
|
|
5
|
+
import { GenesisThemeProvider } from '@marktiderman/genesis-design-system/theme';
|
|
6
|
+
import { createSupabaseProvider, GenesisProvider } from '@marktiderman/genesis-core/provider';
|
|
7
|
+
import { renderSlot } from '@marktiderman/genesis-core/slots';
|
|
8
|
+
import { KeyboardScrollProvider } from '@marktiderman/genesis-ui-native/layouts';
|
|
9
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
10
|
+
import { NativeEmptyState, NativeButton } from '@marktiderman/genesis-ui-native/components';
|
|
11
|
+
|
|
12
|
+
var BootTimeoutError = class extends Error {
|
|
13
|
+
step;
|
|
14
|
+
constructor(step, ms, reason = "did not finish") {
|
|
15
|
+
super(`Boot step "${step}" ${reason} within ${ms} ms.`);
|
|
16
|
+
this.name = "BootTimeoutError";
|
|
17
|
+
this.step = step;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
21
|
+
function toError(value) {
|
|
22
|
+
if (value instanceof Error) return value;
|
|
23
|
+
return new Error(typeof value === "string" ? value : "Unknown error", { cause: value });
|
|
24
|
+
}
|
|
25
|
+
function settledWithin(promise, ms) {
|
|
26
|
+
let timer;
|
|
27
|
+
return Promise.race([
|
|
28
|
+
promise.then(
|
|
29
|
+
() => "settled",
|
|
30
|
+
() => "settled"
|
|
31
|
+
),
|
|
32
|
+
new Promise((resolve) => {
|
|
33
|
+
timer = setTimeout(() => resolve("pending"), ms);
|
|
34
|
+
})
|
|
35
|
+
]).finally(() => clearTimeout(timer));
|
|
36
|
+
}
|
|
37
|
+
function createBoot(steps, options = {}) {
|
|
38
|
+
const seen = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const step of steps) {
|
|
40
|
+
if (seen.has(step.id)) throw new Error(`Boot step "${step.id}" is listed twice.`);
|
|
41
|
+
seen.add(step.id);
|
|
42
|
+
}
|
|
43
|
+
const { minimumMs = 0, timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
44
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
45
|
+
let state = { status: "idle", error: null, failedStep: null, completed: [] };
|
|
46
|
+
let inFlight = null;
|
|
47
|
+
const abandoned = /* @__PURE__ */ new Map();
|
|
48
|
+
const runStep = async (step, ms) => {
|
|
49
|
+
const previous = abandoned.get(step.id);
|
|
50
|
+
if (previous) {
|
|
51
|
+
if (await settledWithin(previous, ms) === "pending") {
|
|
52
|
+
throw new BootTimeoutError(step.id, ms, "is still running from the last attempt");
|
|
53
|
+
}
|
|
54
|
+
abandoned.delete(step.id);
|
|
55
|
+
if (await previous === "ok") return;
|
|
56
|
+
}
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const running = Promise.resolve().then(() => step.run({ signal: controller.signal }));
|
|
59
|
+
if (await settledWithin(running, ms) === "pending") {
|
|
60
|
+
controller.abort();
|
|
61
|
+
abandoned.set(step.id, running.then(() => "ok", () => "failed"));
|
|
62
|
+
throw new BootTimeoutError(step.id, ms);
|
|
63
|
+
}
|
|
64
|
+
await running;
|
|
65
|
+
};
|
|
66
|
+
const set = (next) => {
|
|
67
|
+
state = { ...state, ...next };
|
|
68
|
+
for (const listener of listeners) listener();
|
|
69
|
+
};
|
|
70
|
+
const run = async () => {
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
set({ status: "booting", error: null, failedStep: null });
|
|
73
|
+
for (const step of steps) {
|
|
74
|
+
if (state.completed.includes(step.id)) continue;
|
|
75
|
+
try {
|
|
76
|
+
await runStep(step, step.timeoutMs ?? timeoutMs);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
set({ status: "failed", error: toError(error), failedStep: step.id });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
set({ completed: [...state.completed, step.id] });
|
|
82
|
+
}
|
|
83
|
+
const wait = minimumMs - (Date.now() - started);
|
|
84
|
+
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
|
|
85
|
+
set({ status: "ready" });
|
|
86
|
+
};
|
|
87
|
+
const start = () => {
|
|
88
|
+
if (state.status === "ready") return Promise.resolve();
|
|
89
|
+
if (!inFlight) {
|
|
90
|
+
inFlight = run().finally(() => {
|
|
91
|
+
inFlight = null;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return inFlight;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
getState: () => state,
|
|
98
|
+
subscribe(listener) {
|
|
99
|
+
listeners.add(listener);
|
|
100
|
+
return () => listeners.delete(listener);
|
|
101
|
+
},
|
|
102
|
+
start,
|
|
103
|
+
retry: start
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var BootContext = createContext(null);
|
|
107
|
+
function BootProvider({ boot, children }) {
|
|
108
|
+
return /* @__PURE__ */ jsx(BootContext.Provider, { value: boot, children });
|
|
109
|
+
}
|
|
110
|
+
function useBoot() {
|
|
111
|
+
const boot = useContext(BootContext);
|
|
112
|
+
if (!boot) throw new Error("useBoot must be used inside <GenesisNativeApp> or <BootProvider>.");
|
|
113
|
+
const state = useSyncExternalStore(boot.subscribe, boot.getState, boot.getState);
|
|
114
|
+
return { ...state, retry: () => void boot.retry() };
|
|
115
|
+
}
|
|
116
|
+
function FatalScreen({ error, retry }) {
|
|
117
|
+
return /* @__PURE__ */ jsx(
|
|
118
|
+
NativeEmptyState,
|
|
119
|
+
{
|
|
120
|
+
variant: "error",
|
|
121
|
+
title: "Something went wrong",
|
|
122
|
+
body: "The app hit a problem. Try again, and if it keeps happening, close and reopen the app.",
|
|
123
|
+
details: error.message,
|
|
124
|
+
primaryAction: /* @__PURE__ */ jsx(NativeButton, { onPress: retry, testID: "genesis-fatal-retry", children: "Try again" }),
|
|
125
|
+
testID: "genesis-fatal"
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
function PlainFatalScreen({ error, retry }) {
|
|
130
|
+
return /* @__PURE__ */ jsxs(View, { testID: "genesis-fatal-plain", style: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24, gap: 12 }, children: [
|
|
131
|
+
/* @__PURE__ */ jsx(Text, { accessibilityRole: "header", style: { fontSize: 17, fontWeight: "600" }, children: "Something went wrong" }),
|
|
132
|
+
/* @__PURE__ */ jsx(Text, { style: { textAlign: "center" }, children: error.message }),
|
|
133
|
+
/* @__PURE__ */ jsx(Pressable, { accessibilityRole: "button", onPress: retry, testID: "genesis-fatal-plain-retry", style: { padding: 12 }, children: /* @__PURE__ */ jsx(Text, { style: { fontSize: 17 }, children: "Try again" }) })
|
|
134
|
+
] });
|
|
135
|
+
}
|
|
136
|
+
var GenesisErrorBoundary = class extends Component {
|
|
137
|
+
state = { error: null };
|
|
138
|
+
static getDerivedStateFromError(error) {
|
|
139
|
+
return { error: toError(error) };
|
|
140
|
+
}
|
|
141
|
+
componentDidCatch(error, info) {
|
|
142
|
+
this.props.onError?.(toError(error), { fatal: true, source: "boundary", componentStack: info.componentStack ?? void 0 });
|
|
143
|
+
}
|
|
144
|
+
componentDidUpdate(previous) {
|
|
145
|
+
if (this.state.error && previous.resetKey !== this.props.resetKey) this.setState({ error: null });
|
|
146
|
+
}
|
|
147
|
+
retry = () => {
|
|
148
|
+
this.setState({ error: null });
|
|
149
|
+
this.props.onRetry?.();
|
|
150
|
+
};
|
|
151
|
+
render() {
|
|
152
|
+
const { error } = this.state;
|
|
153
|
+
if (!error) return this.props.children ?? null;
|
|
154
|
+
const Fallback = this.props.fallback;
|
|
155
|
+
return /* @__PURE__ */ jsx(Fallback, { error, retry: this.retry });
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
function installGlobalErrorHandler({ errorUtils, onError, onFatal, alreadyFatal }) {
|
|
159
|
+
const previous = errorUtils.getGlobalHandler();
|
|
160
|
+
errorUtils.setGlobalHandler((raw, isFatal) => {
|
|
161
|
+
const error = toError(raw);
|
|
162
|
+
try {
|
|
163
|
+
onError?.(error, { fatal: Boolean(isFatal), source: "global" });
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
if (isFatal && !alreadyFatal?.()) onFatal(error);
|
|
167
|
+
else previous?.(raw, isFatal);
|
|
168
|
+
});
|
|
169
|
+
return () => {
|
|
170
|
+
errorUtils.setGlobalHandler(previous ?? ((error) => console.error(error)));
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function defaultErrorUtils() {
|
|
174
|
+
const candidate = globalThis.ErrorUtils;
|
|
175
|
+
return candidate && typeof candidate.setGlobalHandler === "function" ? candidate : void 0;
|
|
176
|
+
}
|
|
177
|
+
function defaultRejectionTracker() {
|
|
178
|
+
const hermes = globalThis.HermesInternal;
|
|
179
|
+
const enable = hermes?.enablePromiseRejectionTracker;
|
|
180
|
+
return typeof enable === "function" ? enable.bind(hermes) : void 0;
|
|
181
|
+
}
|
|
182
|
+
function installRejectionTracker({ enable, onError }) {
|
|
183
|
+
enable({
|
|
184
|
+
allRejections: true,
|
|
185
|
+
onUnhandled: (id, rejection) => {
|
|
186
|
+
const error = toError(rejection);
|
|
187
|
+
console.warn(`Uncaught (in promise, id: ${id}): ${error.message}`);
|
|
188
|
+
try {
|
|
189
|
+
onError(error, { fatal: false, source: "rejection" });
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
onHandled: () => {
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
var DEFAULT_WAIT_MS = 5e3;
|
|
198
|
+
function defaultUserId(session) {
|
|
199
|
+
const id = session?.user?.id;
|
|
200
|
+
return typeof id === "string" ? id : null;
|
|
201
|
+
}
|
|
202
|
+
function isNetworkError(error) {
|
|
203
|
+
const e = error;
|
|
204
|
+
return e?.name === "AuthRetryableFetchError" || e?.status === 0;
|
|
205
|
+
}
|
|
206
|
+
function storedSessionReader(client) {
|
|
207
|
+
return async () => {
|
|
208
|
+
const auth = client.auth;
|
|
209
|
+
if (!auth.storage || typeof auth.storageKey !== "string") return null;
|
|
210
|
+
const raw = await auth.storage.getItem(auth.storageKey);
|
|
211
|
+
if (typeof raw !== "string") return raw ?? null;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(raw);
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function createSessionStore(client, options = {}) {
|
|
220
|
+
const { onUserChange, waitMs = DEFAULT_WAIT_MS, userIdOf = defaultUserId } = options;
|
|
221
|
+
const readStored = options.readStoredSession ?? storedSessionReader(client);
|
|
222
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
223
|
+
let state = { status: "loading", session: null, userId: null, offline: false };
|
|
224
|
+
let version = 0;
|
|
225
|
+
const set = (session, offline = false) => {
|
|
226
|
+
const userId = session ? userIdOf(session) : null;
|
|
227
|
+
const previous = state;
|
|
228
|
+
if (previous.status !== "loading" && previous.session === session && previous.offline === offline) return;
|
|
229
|
+
state = { status: session ? "signedIn" : "signedOut", session, userId, offline };
|
|
230
|
+
for (const listener of listeners) listener();
|
|
231
|
+
if (previous.status !== "loading" && previous.userId !== userId) onUserChange?.(previous.userId, userId);
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
getState: () => state,
|
|
235
|
+
subscribe(listener) {
|
|
236
|
+
listeners.add(listener);
|
|
237
|
+
return () => listeners.delete(listener);
|
|
238
|
+
},
|
|
239
|
+
async load() {
|
|
240
|
+
const startedAt = version;
|
|
241
|
+
let timer;
|
|
242
|
+
const timedOut = new Promise((resolve) => {
|
|
243
|
+
timer = setTimeout(() => resolve("timeout"), waitMs);
|
|
244
|
+
});
|
|
245
|
+
const result = await Promise.race([client.auth.getSession(), timedOut]).finally(() => clearTimeout(timer));
|
|
246
|
+
if (version !== startedAt) return;
|
|
247
|
+
if (result !== "timeout" && !result.error) return set(result.data.session);
|
|
248
|
+
if (result !== "timeout" && !isNetworkError(result.error)) {
|
|
249
|
+
const error = result.error;
|
|
250
|
+
throw error instanceof Error ? error : new Error("Could not read the session.", { cause: error });
|
|
251
|
+
}
|
|
252
|
+
const stored = await readStored().catch(() => null);
|
|
253
|
+
if (version !== startedAt) return;
|
|
254
|
+
set(stored, stored !== null);
|
|
255
|
+
},
|
|
256
|
+
start() {
|
|
257
|
+
const { data } = client.auth.onAuthStateChange((_event, session) => {
|
|
258
|
+
version++;
|
|
259
|
+
set(session);
|
|
260
|
+
});
|
|
261
|
+
return () => data.subscription.unsubscribe();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
var SessionContext = createContext(null);
|
|
266
|
+
function SessionProvider({ store, children }) {
|
|
267
|
+
return /* @__PURE__ */ jsx(SessionContext.Provider, { value: store, children });
|
|
268
|
+
}
|
|
269
|
+
function useSession() {
|
|
270
|
+
const store = useContext(SessionContext);
|
|
271
|
+
if (!store) throw new Error("useSession must be used inside <GenesisNativeApp> or <SessionProvider>.");
|
|
272
|
+
return useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
273
|
+
}
|
|
274
|
+
function AuthGate({ signedIn, signedOut, loading = null }) {
|
|
275
|
+
const { status } = useSession();
|
|
276
|
+
if (status === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loading });
|
|
277
|
+
return /* @__PURE__ */ jsx(Fragment, { children: status === "signedIn" ? signedIn : signedOut });
|
|
278
|
+
}
|
|
279
|
+
function createLinkHold() {
|
|
280
|
+
let held = null;
|
|
281
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
282
|
+
const set = (next) => {
|
|
283
|
+
if (held === next) return;
|
|
284
|
+
held = next;
|
|
285
|
+
for (const listener of listeners) listener();
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
hold: (path) => set(path),
|
|
289
|
+
peek: () => held,
|
|
290
|
+
take() {
|
|
291
|
+
const path = held;
|
|
292
|
+
set(null);
|
|
293
|
+
return path;
|
|
294
|
+
},
|
|
295
|
+
clear: () => set(null),
|
|
296
|
+
subscribe(listener) {
|
|
297
|
+
listeners.add(listener);
|
|
298
|
+
return () => listeners.delete(listener);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function useHeldLink(hold, { auth, onRelease }) {
|
|
303
|
+
const held = useSyncExternalStore(hold.subscribe, hold.peek, hold.peek);
|
|
304
|
+
const release = useRef(onRelease);
|
|
305
|
+
release.current = onRelease;
|
|
306
|
+
const waitedForSignIn = useRef(false);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (held === null) {
|
|
309
|
+
waitedForSignIn.current = false;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (auth === "signedOut") waitedForSignIn.current = true;
|
|
313
|
+
if (auth !== "signedIn") return;
|
|
314
|
+
const path = hold.take();
|
|
315
|
+
const follow = waitedForSignIn.current;
|
|
316
|
+
waitedForSignIn.current = false;
|
|
317
|
+
if (path !== null && follow) release.current(path);
|
|
318
|
+
}, [auth, held, hold]);
|
|
319
|
+
return held;
|
|
320
|
+
}
|
|
321
|
+
function BootScreen(_props) {
|
|
322
|
+
return /* @__PURE__ */ jsx(View, { testID: "genesis-boot", accessibilityRole: "progressbar", style: { flex: 1, justifyContent: "center", alignItems: "center" }, children: /* @__PURE__ */ jsx(Text, { children: "Loading\u2026" }) });
|
|
323
|
+
}
|
|
324
|
+
var PassThrough = ({ children }) => /* @__PURE__ */ jsx(Fragment, { children });
|
|
325
|
+
var ThemeProvider = GenesisThemeProvider;
|
|
326
|
+
function GenesisNativeApp(props) {
|
|
327
|
+
const [rootKey, setRootKey] = useState(0);
|
|
328
|
+
const onErrorRef = useRef(props.onError);
|
|
329
|
+
onErrorRef.current = props.onError;
|
|
330
|
+
return /* @__PURE__ */ jsx(
|
|
331
|
+
GenesisErrorBoundary,
|
|
332
|
+
{
|
|
333
|
+
fallback: PlainFatalScreen,
|
|
334
|
+
onError: (e, r) => onErrorRef.current?.(e, r),
|
|
335
|
+
onRetry: () => setRootKey((key) => key + 1),
|
|
336
|
+
children: /* @__PURE__ */ jsx(ThemedShell, { ...props })
|
|
337
|
+
},
|
|
338
|
+
rootKey
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function ThemedShell(props) {
|
|
342
|
+
const { brand, keyboardScroll } = props;
|
|
343
|
+
const body = /* @__PURE__ */ jsx(Shell, { ...props });
|
|
344
|
+
return /* @__PURE__ */ jsx(ThemeProvider, { brand, syncNativeAppearance: true, children: keyboardScroll ? /* @__PURE__ */ jsx(KeyboardScrollProvider, { component: keyboardScroll, children: body }) : body });
|
|
345
|
+
}
|
|
346
|
+
var SESSION_STEP_MARGIN_MS = 15e3;
|
|
347
|
+
function Shell(props) {
|
|
348
|
+
const { supabase, provider, queryClient: queryOptions, persist, splash, links, steps = [], bootOptions, sessionWaitMs = 5e3 } = props;
|
|
349
|
+
const { errorUtils, rejectionTracker, lifecycle, slots, slotProps, children } = props;
|
|
350
|
+
const latest = useRef(props);
|
|
351
|
+
latest.current = props;
|
|
352
|
+
const onError = (error, report) => latest.current.onError?.(error, report);
|
|
353
|
+
const fatalShown = useRef(0);
|
|
354
|
+
const [parts] = useState(() => {
|
|
355
|
+
const client2 = queryOptions?.client ?? createNativeQueryClient(queryOptions?.config);
|
|
356
|
+
const persistence2 = persist ? persist(client2) : null;
|
|
357
|
+
const session2 = createSessionStore(supabase, {
|
|
358
|
+
waitMs: sessionWaitMs,
|
|
359
|
+
// Another user (or nobody) now: nothing of the last user's may show or stay on disk.
|
|
360
|
+
onUserChange: (previous, next) => {
|
|
361
|
+
client2.clear();
|
|
362
|
+
void persistence2?.clear();
|
|
363
|
+
if (previous !== null && next === null) latest.current.onSignedOut?.();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const boot2 = createBoot(
|
|
367
|
+
[
|
|
368
|
+
...steps,
|
|
369
|
+
// Settles within its own timeout and never throws: a policy that can't load never blocks the app.
|
|
370
|
+
...props.updates ? [props.updates.bootStep] : [],
|
|
371
|
+
{ id: "genesis:session", run: () => session2.load(), timeoutMs: sessionWaitMs + SESSION_STEP_MARGIN_MS },
|
|
372
|
+
// After the session, so only the cache saved for this user is restored.
|
|
373
|
+
...persistence2 ? [{ id: "genesis:query-cache", run: () => persistence2.restore(session2.getState().userId ?? "") }] : []
|
|
374
|
+
],
|
|
375
|
+
bootOptions
|
|
376
|
+
);
|
|
377
|
+
function FatalFallback2(fallbackProps) {
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
fatalShown.current++;
|
|
380
|
+
return () => {
|
|
381
|
+
fatalShown.current--;
|
|
382
|
+
};
|
|
383
|
+
}, []);
|
|
384
|
+
const { slots: s, slotProps: sp } = latest.current;
|
|
385
|
+
return (
|
|
386
|
+
// A `fatal` slot that throws falls back to the plain screen.
|
|
387
|
+
/* @__PURE__ */ jsx(GenesisErrorBoundary, { fallback: PlainFatalScreen, onError, onRetry: fallbackProps.retry, children: renderSlot(FatalScreen, s?.fatal, fallbackProps, sp?.fatal) })
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return { client: client2, persistence: persistence2, session: session2, boot: boot2, FatalFallback: FatalFallback2, updates: props.updates, provider: provider ?? createSupabaseProvider(supabase) };
|
|
391
|
+
});
|
|
392
|
+
const { boot, session, persistence, client, FatalFallback } = parts;
|
|
393
|
+
const bootState = useSyncExternalStore(boot.subscribe, boot.getState, boot.getState);
|
|
394
|
+
const sessionState = useSyncExternalStore(session.subscribe, session.getState, session.getState);
|
|
395
|
+
const [fatal, setFatal] = useState(null);
|
|
396
|
+
const [resetKey, setResetKey] = useState(0);
|
|
397
|
+
useEffect(() => session.start(), [session]);
|
|
398
|
+
useEffect(() => {
|
|
399
|
+
void boot.start();
|
|
400
|
+
}, [boot]);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (bootState.status === "failed" && bootState.error) onError(bootState.error, { fatal: true, source: "boot" });
|
|
403
|
+
}, [bootState.status, bootState.error]);
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
if (bootState.status === "ready" && persistence) return persistence.subscribe(sessionState.userId ?? "");
|
|
406
|
+
}, [bootState.status, persistence, sessionState.userId]);
|
|
407
|
+
useEffect(() => {
|
|
408
|
+
const utils = errorUtils ?? defaultErrorUtils();
|
|
409
|
+
if (!utils) return;
|
|
410
|
+
return installGlobalErrorHandler({ errorUtils: utils, onError, onFatal: setFatal, alreadyFatal: () => fatalShown.current > 0 });
|
|
411
|
+
}, [errorUtils]);
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
const enable = rejectionTracker === null ? void 0 : rejectionTracker ?? defaultRejectionTracker();
|
|
414
|
+
if (enable) installRejectionTracker({ enable, onError });
|
|
415
|
+
}, [rejectionTracker]);
|
|
416
|
+
useEffect(() => lifecycle ? connectQueryLifecycle(lifecycle) : void 0, [lifecycle]);
|
|
417
|
+
const settled = bootState.status === "ready" || bootState.status === "failed" || fatal !== null;
|
|
418
|
+
const splashHidden = useRef(false);
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (!settled || !splash || splashHidden.current) return;
|
|
421
|
+
splashHidden.current = true;
|
|
422
|
+
splash.hideAsync().catch(() => {
|
|
423
|
+
});
|
|
424
|
+
}, [settled, splash]);
|
|
425
|
+
const auth = bootState.status !== "ready" || sessionState.status === "loading" ? "pending" : sessionState.status;
|
|
426
|
+
useHeldLink(links?.hold ?? NO_HOLD, { auth: links ? auth : "pending", onRelease: (path) => links?.onRelease(path) });
|
|
427
|
+
const retry = () => {
|
|
428
|
+
setFatal(null);
|
|
429
|
+
setResetKey((key) => key + 1);
|
|
430
|
+
if (boot.getState().status === "failed") void boot.retry();
|
|
431
|
+
};
|
|
432
|
+
let content;
|
|
433
|
+
if (fatal) {
|
|
434
|
+
content = /* @__PURE__ */ jsx(FatalFallback, { error: fatal, retry });
|
|
435
|
+
} else if (bootState.status === "failed" && bootState.error) {
|
|
436
|
+
content = /* @__PURE__ */ jsx(FatalFallback, { error: bootState.error, retry });
|
|
437
|
+
} else if (bootState.status !== "ready") {
|
|
438
|
+
content = renderSlot(BootScreen, slots?.boot, { status: bootState.status }, slotProps?.boot);
|
|
439
|
+
} else {
|
|
440
|
+
const screens = sessionState.status === "signedOut" ? renderSlot(PassThrough, slots?.signedOut, { children }, slotProps?.signedOut) : children;
|
|
441
|
+
const guarded = renderSlot(
|
|
442
|
+
GenesisErrorBoundary,
|
|
443
|
+
slots?.errorBoundary,
|
|
444
|
+
{ fallback: FatalFallback, onError, resetKey, onRetry: retry, children: screens },
|
|
445
|
+
slotProps?.errorBoundary
|
|
446
|
+
);
|
|
447
|
+
const Updates = parts.updates?.Root;
|
|
448
|
+
content = Updates ? /* @__PURE__ */ jsx(Updates, { slots, slotProps, children: guarded }) : guarded;
|
|
449
|
+
}
|
|
450
|
+
return /* @__PURE__ */ jsx(GenesisProvider, { provider: parts.provider, queryClient: client, children: /* @__PURE__ */ jsx(SessionProvider, { store: session, children: /* @__PURE__ */ jsx(BootProvider, { boot, children: /* @__PURE__ */ jsx(GenesisErrorBoundary, { fallback: FatalFallback, onError, onRetry: retry, children: content }, resetKey) }) }) });
|
|
451
|
+
}
|
|
452
|
+
var NO_HOLD = {
|
|
453
|
+
hold: () => {
|
|
454
|
+
},
|
|
455
|
+
peek: () => null,
|
|
456
|
+
take: () => null,
|
|
457
|
+
clear: () => {
|
|
458
|
+
},
|
|
459
|
+
subscribe: () => () => {
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/hydration-gate.ts
|
|
464
|
+
function createHydrationGate({ onDroppedWrite } = {}) {
|
|
465
|
+
let hydrated = false;
|
|
466
|
+
let open;
|
|
467
|
+
const opened = new Promise((resolve) => {
|
|
468
|
+
open = resolve;
|
|
469
|
+
});
|
|
470
|
+
const gate = {
|
|
471
|
+
get hydrated() {
|
|
472
|
+
return hydrated;
|
|
473
|
+
},
|
|
474
|
+
markHydrated() {
|
|
475
|
+
if (hydrated) return;
|
|
476
|
+
hydrated = true;
|
|
477
|
+
open();
|
|
478
|
+
},
|
|
479
|
+
whenHydrated: () => opened,
|
|
480
|
+
async run(write) {
|
|
481
|
+
if (!hydrated) await opened;
|
|
482
|
+
return write();
|
|
483
|
+
},
|
|
484
|
+
storage(storage) {
|
|
485
|
+
const setItem = async (key, value) => {
|
|
486
|
+
if (!hydrated) return onDroppedWrite?.(key);
|
|
487
|
+
return storage.setItem(key, value);
|
|
488
|
+
};
|
|
489
|
+
const removeItem = async (key) => {
|
|
490
|
+
if (!hydrated) return onDroppedWrite?.(key);
|
|
491
|
+
return storage.removeItem(key);
|
|
492
|
+
};
|
|
493
|
+
return new Proxy(storage, {
|
|
494
|
+
get(target, property) {
|
|
495
|
+
if (property === "setItem") return setItem;
|
|
496
|
+
if (property === "removeItem") return removeItem;
|
|
497
|
+
const value = Reflect.get(target, property, target);
|
|
498
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
return gate;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/links.ts
|
|
507
|
+
var URL_PATTERN = /^([a-z][a-z0-9+.-]*):\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/i;
|
|
508
|
+
function parse(url, schemes, hosts) {
|
|
509
|
+
const trimmed = url.trim();
|
|
510
|
+
const match = URL_PATTERN.exec(trimmed);
|
|
511
|
+
if (!match) {
|
|
512
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return null;
|
|
513
|
+
const noHash = trimmed.split("#")[0] ?? "";
|
|
514
|
+
const at = noHash.indexOf("?");
|
|
515
|
+
return at < 0 ? { path: noHash, query: "" } : { path: noHash.slice(0, at), query: noHash.slice(at) };
|
|
516
|
+
}
|
|
517
|
+
const [, scheme = "", host = "", path = "", query = ""] = match;
|
|
518
|
+
const lowerScheme = scheme.toLowerCase();
|
|
519
|
+
if (lowerScheme === "http" || lowerScheme === "https") {
|
|
520
|
+
const bareHost = host.toLowerCase().replace(/:\d+$/, "");
|
|
521
|
+
if (!hosts.some((h) => h.toLowerCase() === bareHost)) return null;
|
|
522
|
+
return { path, query };
|
|
523
|
+
}
|
|
524
|
+
if (!schemes.some((s) => s.toLowerCase() === lowerScheme)) return null;
|
|
525
|
+
return { path: `/${host}${path}`, query };
|
|
526
|
+
}
|
|
527
|
+
function clean(path) {
|
|
528
|
+
const joined = `/${path}`.replace(/\/{2,}/g, "/");
|
|
529
|
+
return joined.length > 1 ? joined.replace(/\/$/, "") : joined;
|
|
530
|
+
}
|
|
531
|
+
function matchPattern(pattern, path) {
|
|
532
|
+
const want = clean(pattern).split("/").slice(1);
|
|
533
|
+
const have = path.split("/").slice(1);
|
|
534
|
+
const params = {};
|
|
535
|
+
for (let i = 0; i < want.length; i++) {
|
|
536
|
+
const segment = want[i];
|
|
537
|
+
if (segment === "*" && i === want.length - 1) {
|
|
538
|
+
params["*"] = have.slice(i).join("/");
|
|
539
|
+
return params;
|
|
540
|
+
}
|
|
541
|
+
const value = have[i];
|
|
542
|
+
if (value === void 0) return null;
|
|
543
|
+
if (segment.startsWith(":")) params[segment.slice(1)] = value;
|
|
544
|
+
else if (segment !== value) return null;
|
|
545
|
+
}
|
|
546
|
+
return want.length === have.length ? params : null;
|
|
547
|
+
}
|
|
548
|
+
function fill(target, params) {
|
|
549
|
+
if (typeof target === "function") return target(params);
|
|
550
|
+
return target.replace(
|
|
551
|
+
/:([A-Za-z_][A-Za-z0-9_]*)|\*$/g,
|
|
552
|
+
(token, name) => name === void 0 ? params["*"] ?? "" : params[name] ?? token
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
function isDotSegment(segment) {
|
|
556
|
+
let decoded = segment;
|
|
557
|
+
try {
|
|
558
|
+
decoded = decodeURIComponent(segment);
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
return decoded === "." || decoded === "..";
|
|
562
|
+
}
|
|
563
|
+
function withQuery(path, query) {
|
|
564
|
+
if (!query || query === "?") return path;
|
|
565
|
+
return path.includes("?") ? `${path}&${query.slice(1)}` : `${path}${query}`;
|
|
566
|
+
}
|
|
567
|
+
function normalizeLink(url, options) {
|
|
568
|
+
const { schemes = [], hosts = [], map, ignore = [], onUnknown } = options;
|
|
569
|
+
const unknown = () => {
|
|
570
|
+
onUnknown?.(url);
|
|
571
|
+
return "/";
|
|
572
|
+
};
|
|
573
|
+
let parsed;
|
|
574
|
+
try {
|
|
575
|
+
parsed = parse(url, schemes, hosts);
|
|
576
|
+
} catch {
|
|
577
|
+
parsed = null;
|
|
578
|
+
}
|
|
579
|
+
if (!parsed) return unknown();
|
|
580
|
+
const path = clean(parsed.path);
|
|
581
|
+
if (path.split("/").some(isDotSegment)) {
|
|
582
|
+
return unknown();
|
|
583
|
+
}
|
|
584
|
+
if (ignore.some((pattern) => matchPattern(pattern, path))) return null;
|
|
585
|
+
if (path === "/") return withQuery("/", parsed.query);
|
|
586
|
+
for (const [pattern, target] of Object.entries(map)) {
|
|
587
|
+
const params = matchPattern(pattern, path);
|
|
588
|
+
if (!params) continue;
|
|
589
|
+
try {
|
|
590
|
+
return withQuery(fill(target, params), parsed.query);
|
|
591
|
+
} catch {
|
|
592
|
+
return unknown();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return unknown();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export { AuthGate, BootProvider, BootScreen, BootTimeoutError, FatalScreen, GenesisErrorBoundary, GenesisNativeApp, PlainFatalScreen, SessionProvider, createBoot, createHydrationGate, createLinkHold, createSessionStore, defaultErrorUtils, defaultRejectionTracker, installGlobalErrorHandler, installRejectionTracker, normalizeLink, useBoot, useHeldLink, useSession };
|
|
599
|
+
//# sourceMappingURL=index.js.map
|
|
600
|
+
//# sourceMappingURL=index.js.map
|