@qlever-llc/trellis-svelte 0.7.0 → 0.8.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/package.json +2 -2
- package/src/components/TrellisContextProvider.svelte +24 -0
- package/src/components/TrellisProvider.svelte +115 -200
- package/src/components/TrellisProvider.types.ts +26 -0
- package/src/context.svelte.ts +196 -28
- package/src/device_activation.svelte.ts +28 -0
- package/src/device_activation_controller.ts +390 -0
- package/src/index.ts +26 -8
- package/src/portal_flow.svelte.ts +14 -6
- package/src/state/auth.svelte.ts +0 -491
- package/src/state/nats.svelte.ts +0 -435
- package/src/state/trellis.svelte.ts +0 -87
package/README.md
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Svelte integration for Trellis browser applications.
|
|
4
4
|
|
|
5
|
-
Provides `TrellisProvider` for app-level wiring
|
|
5
|
+
Provides `TrellisProvider` for app-level wiring and `createTrellisApp(...)` for
|
|
6
|
+
app-scoped typed Svelte context around the real connected Trellis client.
|
|
6
7
|
|
|
7
8
|
Use `TrellisProvider` as the primary integration surface:
|
|
8
9
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- create an app context with `createTrellisApp({ contract, trellisUrl })`
|
|
11
|
+
- pass `trellisApp` to `TrellisProvider`
|
|
12
|
+
- read the live client synchronously with `app.getTrellis()` from child
|
|
13
|
+
components
|
|
12
14
|
|
|
13
|
-
Uses the contract/runtime model from `@qlever-llc/trellis/contracts` and
|
|
15
|
+
Uses the contract/runtime model from `@qlever-llc/trellis/contracts` and
|
|
16
|
+
`@qlever-llc/trellis`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qlever-llc/trellis-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-rc.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Svelte components and state helpers for Trellis browser applications.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@nats-io/nats-core": "^3.3.1",
|
|
31
|
-
"@qlever-llc/trellis": "^0.
|
|
31
|
+
"@qlever-llc/trellis": "^0.8.0-rc.3",
|
|
32
32
|
"typebox": "^1.0.15"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import {
|
|
4
|
+
provideConnectedTrellisContext,
|
|
5
|
+
type TrellisAppOwner,
|
|
6
|
+
type TrellisContextClient,
|
|
7
|
+
} from "../context.svelte.ts";
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
trellisApp: TrellisAppOwner;
|
|
11
|
+
trellis: TrellisContextClient;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const { trellisApp, trellis, children }: Props = $props();
|
|
16
|
+
|
|
17
|
+
function installContext(): void {
|
|
18
|
+
provideConnectedTrellisContext(trellisApp, trellis);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
installContext();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
{@render children()}
|
|
@@ -1,229 +1,144 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { NatsConnection } from "@nats-io/nats-core";
|
|
3
|
-
import { AsyncResult } from "@qlever-llc/result";
|
|
4
|
-
import type { Trellis, TrellisAPI } from "../../../trellis/trellis.ts";
|
|
5
|
-
import { onDestroy } from "svelte";
|
|
6
|
-
import type { Snippet } from "svelte";
|
|
1
|
+
<script lang="ts" generics="TContract extends TrellisContractLike">
|
|
7
2
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
trellisUrl: string;
|
|
24
|
-
loginPath?: string;
|
|
25
|
-
contract: TrellisClientContract;
|
|
26
|
-
onAuthExpired?: () => void;
|
|
27
|
-
onAuthFailed?: (error: unknown) => void;
|
|
28
|
-
onAuthRequired?: (redirectTo: string) => void;
|
|
29
|
-
onBindError?: (result: BindErrorResult) => void;
|
|
30
|
-
onNatsConnecting?: () => void;
|
|
31
|
-
onNatsConnected?: () => void;
|
|
32
|
-
onNatsDisconnect?: () => void;
|
|
33
|
-
onNatsReconnecting?: () => void;
|
|
34
|
-
onNatsReconnect?: () => void;
|
|
35
|
-
onNatsError?: (error: Error) => void;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
let {
|
|
3
|
+
ClientAuthHandledError,
|
|
4
|
+
TrellisClient,
|
|
5
|
+
type ClientAuthOptions,
|
|
6
|
+
type ConnectedTrellisClient,
|
|
7
|
+
} from "@qlever-llc/trellis";
|
|
8
|
+
import { onMount } from "svelte";
|
|
9
|
+
import type { TrellisContractLike } from "../context.svelte.ts";
|
|
10
|
+
import { resolveTrellisAppUrl } from "../context.svelte.ts";
|
|
11
|
+
import TrellisContextProvider from "./TrellisContextProvider.svelte";
|
|
12
|
+
import type { TrellisProviderProps } from "./TrellisProvider.types.ts";
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
trellisApp,
|
|
16
|
+
auth,
|
|
17
|
+
client,
|
|
39
18
|
children,
|
|
40
19
|
loading,
|
|
41
|
-
|
|
42
|
-
trellisUrl,
|
|
43
|
-
loginPath,
|
|
44
|
-
contract,
|
|
45
|
-
onAuthExpired,
|
|
46
|
-
onAuthFailed,
|
|
20
|
+
error: errorSnippet,
|
|
47
21
|
onAuthRequired,
|
|
48
|
-
|
|
49
|
-
onNatsConnecting,
|
|
50
|
-
onNatsConnected,
|
|
51
|
-
onNatsDisconnect,
|
|
52
|
-
onNatsReconnecting,
|
|
53
|
-
onNatsReconnect,
|
|
54
|
-
onNatsError,
|
|
55
|
-
}: Props = $props();
|
|
22
|
+
}: TrellisProviderProps<TContract> = $props();
|
|
56
23
|
|
|
57
|
-
let
|
|
58
|
-
let
|
|
24
|
+
let trellis = $state<ConnectedTrellisClient<TContract> | null>(null);
|
|
25
|
+
let connectError = $state<unknown>(null);
|
|
59
26
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
providerAuth.setAuthUrl(trellisUrl);
|
|
66
|
-
|
|
67
|
-
return providerAuth;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
type InitContext = {
|
|
71
|
-
auth: ReturnType<typeof createAuthState>;
|
|
72
|
-
nats: NatsState;
|
|
73
|
-
trellis: Trellis<TrellisAPI>;
|
|
27
|
+
type SerializableTrellisError = {
|
|
28
|
+
message?: unknown;
|
|
29
|
+
code?: unknown;
|
|
30
|
+
hint?: unknown;
|
|
31
|
+
context?: unknown;
|
|
74
32
|
};
|
|
75
|
-
const isBrowser = typeof window !== "undefined";
|
|
76
33
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!
|
|
81
|
-
|
|
34
|
+
function maybeSerializableError(
|
|
35
|
+
value: unknown,
|
|
36
|
+
): SerializableTrellisError | undefined {
|
|
37
|
+
if (!value || typeof value !== "object" || !("toSerializable" in value)) {
|
|
38
|
+
return undefined;
|
|
82
39
|
}
|
|
83
|
-
|
|
84
|
-
return
|
|
40
|
+
const serialize = value.toSerializable;
|
|
41
|
+
if (typeof serialize !== "function") return undefined;
|
|
42
|
+
const serialized = serialize.call(value);
|
|
43
|
+
return serialized && typeof serialized === "object"
|
|
44
|
+
? (serialized as SerializableTrellisError)
|
|
45
|
+
: undefined;
|
|
85
46
|
}
|
|
86
47
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return window.location.pathname + window.location.search;
|
|
48
|
+
function contextRecord(value: unknown): Record<string, unknown> | undefined {
|
|
49
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
50
|
+
? (value as Record<string, unknown>)
|
|
51
|
+
: undefined;
|
|
93
52
|
}
|
|
94
53
|
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
54
|
+
function logConnectionError(error: unknown): void {
|
|
55
|
+
const serialized = maybeSerializableError(error);
|
|
56
|
+
const context = contextRecord(serialized?.context);
|
|
57
|
+
const causeMessage =
|
|
58
|
+
typeof context?.causeMessage === "string"
|
|
59
|
+
? context.causeMessage
|
|
60
|
+
: undefined;
|
|
61
|
+
const message =
|
|
62
|
+
typeof serialized?.message === "string"
|
|
63
|
+
? serialized.message
|
|
64
|
+
: error instanceof Error
|
|
65
|
+
? error.message
|
|
66
|
+
: String(error);
|
|
67
|
+
|
|
68
|
+
console.error("Error:", error);
|
|
102
69
|
}
|
|
103
70
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onAuthRequired?.(redirectTo);
|
|
107
|
-
if (!onAuthRequired) {
|
|
108
|
-
redirectToLogin(redirectTo);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
71
|
+
onMount(() => {
|
|
72
|
+
let active = true;
|
|
111
73
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const bindResult = await auth.handleCallback();
|
|
118
|
-
|
|
119
|
-
if (bindResult !== null && bindResult.status !== "bound") {
|
|
120
|
-
bindErrorResult = bindResult;
|
|
121
|
-
onBindError?.(bindResult);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!auth.isAuthenticated) {
|
|
126
|
-
handleAuthRequired();
|
|
127
|
-
return null;
|
|
74
|
+
function withBrowserAuthDefaults(
|
|
75
|
+
authOptions: ClientAuthOptions | undefined,
|
|
76
|
+
): ClientAuthOptions {
|
|
77
|
+
if (authOptions?.mode === "session_key") {
|
|
78
|
+
return authOptions;
|
|
128
79
|
}
|
|
129
80
|
|
|
130
|
-
const trellis = await TrellisClient.connect({
|
|
131
|
-
trellisUrl: requireTrellisUrl(),
|
|
132
|
-
contract,
|
|
133
|
-
auth: {
|
|
134
|
-
handle: auth.handle ?? undefined,
|
|
135
|
-
},
|
|
136
|
-
onAuthRequired: ({ loginUrl }) => {
|
|
137
|
-
onAuthExpired?.();
|
|
138
|
-
const redirectTo = getRedirectTo();
|
|
139
|
-
onAuthRequired?.(redirectTo);
|
|
140
|
-
if (!onAuthRequired && typeof window !== "undefined") {
|
|
141
|
-
window.location.href = loginUrl;
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const natsState = await createConnectedNatsState(trellis.natsConnection, auth, {
|
|
147
|
-
onConnecting: onNatsConnecting,
|
|
148
|
-
onConnected: onNatsConnected,
|
|
149
|
-
onDisconnect: onNatsDisconnect,
|
|
150
|
-
onReconnecting: onNatsReconnecting,
|
|
151
|
-
onReconnect: onNatsReconnect,
|
|
152
|
-
onError: onNatsError,
|
|
153
|
-
onAuthRequired: () => {
|
|
154
|
-
onAuthExpired?.();
|
|
155
|
-
handleAuthRequired();
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
|
|
159
81
|
return {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
82
|
+
...authOptions,
|
|
83
|
+
currentUrl:
|
|
84
|
+
authOptions?.currentUrl ?? (() => new URL(window.location.href)),
|
|
163
85
|
};
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (result.isErr()) {
|
|
167
|
-
const error = result.error;
|
|
168
|
-
getProviderAuth().clearAuth();
|
|
169
|
-
onAuthFailed?.(error);
|
|
170
|
-
throw error;
|
|
171
86
|
}
|
|
172
87
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const initPromise = isBrowser ? initialize() : null;
|
|
182
|
-
const readyPromise: Promise<InitContext> | null = initPromise?.then((ctx) => {
|
|
183
|
-
if (!ctx) {
|
|
184
|
-
throw new Error("Trellis context is not available");
|
|
88
|
+
const connectAuth = withBrowserAuthDefaults(auth);
|
|
89
|
+
const trellisUrl = resolveTrellisAppUrl(trellisApp.trellisUrl);
|
|
90
|
+
if (!trellisUrl) {
|
|
91
|
+
connectError = new TypeError(
|
|
92
|
+
"Expected trellisApp to resolve a Trellis URL",
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
185
95
|
}
|
|
186
|
-
return ctx;
|
|
187
|
-
}) ?? null;
|
|
188
|
-
const natsStatePromise: Promise<NatsState> | null = readyPromise?.then((ctx) => ctx.nats) ?? null;
|
|
189
|
-
const trellisPromise: Promise<Trellis<TrellisAPI>> | null = readyPromise?.then((ctx) => ctx.trellis) ?? null;
|
|
190
|
-
const natsPromise: Promise<NatsConnection> | null = readyPromise?.then((ctx) => ctx.nats.nc) ?? null;
|
|
191
|
-
|
|
192
|
-
if (readyPromise && natsStatePromise && trellisPromise && natsPromise) {
|
|
193
|
-
void readyPromise.catch(() => {});
|
|
194
|
-
setAuthContext(() => getProviderAuth());
|
|
195
|
-
setNatsStateContext(natsStatePromise);
|
|
196
|
-
setTrellisContext({
|
|
197
|
-
trellis: trellisPromise,
|
|
198
|
-
nats: natsPromise,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
96
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
97
|
+
void (async () => {
|
|
98
|
+
try {
|
|
99
|
+
const connected = await TrellisClient.connect({
|
|
100
|
+
...client,
|
|
101
|
+
trellisUrl,
|
|
102
|
+
contract: trellisApp.contract,
|
|
103
|
+
auth: connectAuth,
|
|
104
|
+
onAuthRequired: onAuthRequired
|
|
105
|
+
? async (ctx) => {
|
|
106
|
+
await onAuthRequired(ctx.loginUrl, ctx);
|
|
107
|
+
return { status: "handled" };
|
|
108
|
+
}
|
|
109
|
+
: undefined,
|
|
110
|
+
}).orThrow();
|
|
111
|
+
|
|
112
|
+
if (active) {
|
|
113
|
+
trellis = connected;
|
|
114
|
+
} else {
|
|
115
|
+
await connected.connection.close();
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (!active) return;
|
|
119
|
+
if (error instanceof ClientAuthHandledError) return;
|
|
120
|
+
logConnectionError(error);
|
|
121
|
+
connectError = error;
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
active = false;
|
|
127
|
+
const connected = trellis;
|
|
128
|
+
trellis = null;
|
|
129
|
+
if (connected) {
|
|
130
|
+
void connected.connection.close();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
208
133
|
});
|
|
209
134
|
</script>
|
|
210
135
|
|
|
211
|
-
{#if
|
|
212
|
-
{
|
|
213
|
-
|
|
136
|
+
{#if trellis}
|
|
137
|
+
<TrellisContextProvider {trellisApp} {trellis} {children} />
|
|
138
|
+
{:else if connectError}
|
|
139
|
+
{#if errorSnippet}
|
|
140
|
+
{@render errorSnippet(connectError)}
|
|
214
141
|
{/if}
|
|
215
|
-
{:else}
|
|
216
|
-
{
|
|
217
|
-
{#if loading}
|
|
218
|
-
{@render loading()}
|
|
219
|
-
{/if}
|
|
220
|
-
{:then ctx}
|
|
221
|
-
{#if bindErrorResult}
|
|
222
|
-
{#if bindError}
|
|
223
|
-
{@render bindError(bindErrorResult)}
|
|
224
|
-
{/if}
|
|
225
|
-
{:else if ctx}
|
|
226
|
-
{@render children()}
|
|
227
|
-
{/if}
|
|
228
|
-
{/await}
|
|
142
|
+
{:else if loading}
|
|
143
|
+
{@render loading()}
|
|
229
144
|
{/if}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClientAuthOptions,
|
|
3
|
+
ClientAuthRequiredContext,
|
|
4
|
+
ClientOpts,
|
|
5
|
+
} from "@qlever-llc/trellis";
|
|
6
|
+
import type { Snippet } from "svelte";
|
|
7
|
+
import type {
|
|
8
|
+
TrellisAppOwner,
|
|
9
|
+
TrellisContractLike,
|
|
10
|
+
} from "../context.svelte.ts";
|
|
11
|
+
|
|
12
|
+
/** Props accepted by the Svelte Trellis provider component. */
|
|
13
|
+
export type TrellisProviderProps<
|
|
14
|
+
TContract extends TrellisContractLike = TrellisContractLike,
|
|
15
|
+
> = {
|
|
16
|
+
trellisApp: TrellisAppOwner<TContract>;
|
|
17
|
+
auth?: ClientAuthOptions;
|
|
18
|
+
client?: ClientOpts;
|
|
19
|
+
children: Snippet;
|
|
20
|
+
loading?: Snippet;
|
|
21
|
+
error?: Snippet<[unknown]>;
|
|
22
|
+
onAuthRequired?: (
|
|
23
|
+
loginUrl: string,
|
|
24
|
+
context: ClientAuthRequiredContext,
|
|
25
|
+
) => void | Promise<void>;
|
|
26
|
+
};
|
package/src/context.svelte.ts
CHANGED
|
@@ -1,50 +1,218 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
ClientTrellis,
|
|
3
|
+
RuntimeStateStoresForContract,
|
|
4
|
+
TrellisAPI,
|
|
5
|
+
TrellisConnection,
|
|
6
|
+
TrellisConnectionStatus,
|
|
7
|
+
TrellisContractV1,
|
|
8
|
+
} from "@qlever-llc/trellis";
|
|
3
9
|
import { createContext } from "svelte";
|
|
4
|
-
import
|
|
5
|
-
import type { NatsState } from "./state/nats.svelte.ts";
|
|
6
|
-
|
|
7
|
-
type TrellisContext = {
|
|
8
|
-
trellis: Promise<unknown>;
|
|
9
|
-
nats: Promise<NatsConnection>;
|
|
10
|
-
};
|
|
10
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
11
11
|
|
|
12
|
+
/** Minimal contract shape required to create a typed Trellis Svelte app context. */
|
|
12
13
|
export type TrellisContractLike<TA extends TrellisAPI = TrellisAPI> = {
|
|
14
|
+
CONTRACT: TrellisContractV1;
|
|
15
|
+
CONTRACT_DIGEST: string;
|
|
13
16
|
API: {
|
|
14
17
|
trellis: TA;
|
|
15
18
|
};
|
|
16
19
|
};
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
/** Real connected Trellis client type exposed by a Svelte app context. */
|
|
22
|
+
export type TrellisClientFor<TContract extends TrellisContractLike> =
|
|
23
|
+
ClientTrellis<
|
|
24
|
+
TContract["API"]["trellis"],
|
|
25
|
+
RuntimeStateStoresForContract<TContract>
|
|
26
|
+
>;
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
/** Minimal client surface required for Trellis Svelte context clients. */
|
|
29
|
+
export type TrellisContextClient = {
|
|
30
|
+
readonly connection: TrellisConnection;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** URL value accepted by a Trellis Svelte app owner. */
|
|
34
|
+
export type TrellisAppUrl = string | URL;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Trellis URL configuration for a Svelte app owner.
|
|
38
|
+
*
|
|
39
|
+
* A function resolver supports apps whose Trellis instance is selected at
|
|
40
|
+
* runtime. Returning `undefined` means no instance is currently available.
|
|
41
|
+
*/
|
|
42
|
+
export type TrellisAppUrlResolver =
|
|
43
|
+
| TrellisAppUrl
|
|
44
|
+
| (() => TrellisAppUrl | undefined);
|
|
45
|
+
|
|
46
|
+
/** Options used to create a Trellis Svelte app owner. */
|
|
47
|
+
export type CreateTrellisAppOptions<
|
|
48
|
+
TContract extends TrellisContractLike = TrellisContractLike,
|
|
49
|
+
> = {
|
|
50
|
+
/** Contract used by this app context and by `TrellisProvider` connections. */
|
|
51
|
+
contract: TContract;
|
|
52
|
+
|
|
53
|
+
/** Trellis URL, or a resolver for runtime-selected Trellis URLs. */
|
|
54
|
+
trellisUrl: TrellisAppUrlResolver;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Resolves a Trellis app URL config to the string shape required by the client. */
|
|
58
|
+
export function resolveTrellisAppUrl(
|
|
59
|
+
trellisUrl: TrellisAppUrlResolver,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
const resolved = typeof trellisUrl === "function" ? trellisUrl() : trellisUrl;
|
|
62
|
+
return resolved?.toString();
|
|
26
63
|
}
|
|
27
64
|
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
/** Svelte-reactive adapter around a framework-neutral Trellis connection. */
|
|
66
|
+
export class SvelteTrellisConnection {
|
|
67
|
+
#connection: TrellisConnection;
|
|
68
|
+
#subscribe: () => void;
|
|
69
|
+
|
|
70
|
+
/** Creates a reactive connection adapter for a connected Trellis runtime. */
|
|
71
|
+
constructor(connection: TrellisConnection) {
|
|
72
|
+
this.#connection = connection;
|
|
73
|
+
this.#subscribe = createSubscriber((update) => {
|
|
74
|
+
return this.#connection.subscribe(() => update());
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Latest connection status, reactive when read by Svelte effects or markup. */
|
|
79
|
+
get status(): TrellisConnectionStatus {
|
|
80
|
+
this.#subscribe();
|
|
81
|
+
return this.#connection.status;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Closes the underlying Trellis runtime connection. */
|
|
85
|
+
close(): Promise<void> {
|
|
86
|
+
return this.#connection.close();
|
|
87
|
+
}
|
|
30
88
|
}
|
|
31
89
|
|
|
32
|
-
|
|
33
|
-
|
|
90
|
+
type TrellisAppContext<TClient extends TrellisContextClient> = {
|
|
91
|
+
trellis: TClient;
|
|
92
|
+
connection: SvelteTrellisConnection;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const provideTrellisContext = Symbol("provideTrellisContext");
|
|
96
|
+
const trellisAppOwnerBrand: unique symbol = Symbol("trellisAppOwner");
|
|
97
|
+
|
|
98
|
+
/** Minimal branded app owner surface accepted by the Trellis Svelte provider. */
|
|
99
|
+
export type TrellisAppOwner<
|
|
100
|
+
TContract extends TrellisContractLike = TrellisContractLike,
|
|
101
|
+
> = {
|
|
102
|
+
readonly contract: TContract;
|
|
103
|
+
readonly trellisUrl: TrellisAppUrlResolver;
|
|
104
|
+
readonly [trellisAppOwnerBrand]: true;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Public app-scoped typed Svelte context owner for a Trellis browser app.
|
|
109
|
+
*
|
|
110
|
+
* `TClient` is a type-only facade over the runtime client that `TrellisProvider`
|
|
111
|
+
* installs. Use it with generated client facade types for the same contract.
|
|
112
|
+
*/
|
|
113
|
+
export interface TrellisApp<
|
|
114
|
+
TContract extends TrellisContractLike = TrellisContractLike,
|
|
115
|
+
TClient extends TrellisContextClient = TrellisClientFor<TContract>,
|
|
116
|
+
> extends TrellisAppOwner<TContract> {
|
|
117
|
+
/** Contract used by this app context and by `TrellisProvider` connections. */
|
|
118
|
+
readonly contract: TContract;
|
|
119
|
+
|
|
120
|
+
/** Trellis URL configuration used by `TrellisProvider` connections. */
|
|
121
|
+
readonly trellisUrl: TrellisAppUrlResolver;
|
|
122
|
+
|
|
123
|
+
/** Returns the contract-typed connected Trellis client from Svelte context synchronously. */
|
|
124
|
+
getTrellis(): TClient;
|
|
125
|
+
|
|
126
|
+
/** Returns a Svelte-reactive adapter for the real Trellis connection. */
|
|
127
|
+
getConnection(): SvelteTrellisConnection;
|
|
34
128
|
}
|
|
35
129
|
|
|
36
|
-
|
|
37
|
-
|
|
130
|
+
/** Internal app-scoped typed Svelte context implementation. */
|
|
131
|
+
class TrellisAppImpl<
|
|
132
|
+
TContract extends TrellisContractLike = TrellisContractLike,
|
|
133
|
+
TClient extends TrellisContextClient = TrellisClientFor<TContract>,
|
|
134
|
+
> {
|
|
135
|
+
readonly [trellisAppOwnerBrand] = true as const;
|
|
136
|
+
readonly #contract: TContract;
|
|
137
|
+
readonly #trellisUrl: TrellisAppUrlResolver;
|
|
138
|
+
readonly #getContext: () => TrellisAppContext<TClient>;
|
|
139
|
+
readonly #setContext: (
|
|
140
|
+
context: TrellisAppContext<TClient>,
|
|
141
|
+
) => TrellisAppContext<TClient>;
|
|
142
|
+
|
|
143
|
+
/** Creates an app-scoped context owner for a specific Trellis contract. */
|
|
144
|
+
constructor(options: CreateTrellisAppOptions<TContract>) {
|
|
145
|
+
const { contract, trellisUrl } = options;
|
|
146
|
+
this.#contract = contract;
|
|
147
|
+
this.#trellisUrl = trellisUrl;
|
|
148
|
+
const [getContext, setContext] = createContext<
|
|
149
|
+
TrellisAppContext<TClient>
|
|
150
|
+
>();
|
|
151
|
+
this.#getContext = getContext;
|
|
152
|
+
this.#setContext = setContext;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Contract used by this app context and by `TrellisProvider` connections. */
|
|
156
|
+
get contract(): TContract {
|
|
157
|
+
return this.#contract;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Trellis URL configuration used by `TrellisProvider` connections. */
|
|
161
|
+
get trellisUrl(): TrellisAppUrlResolver {
|
|
162
|
+
return this.#trellisUrl;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Returns the contract-typed connected Trellis client from Svelte context synchronously. */
|
|
166
|
+
getTrellis(): TClient {
|
|
167
|
+
return this.#getContext().trellis;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Returns a Svelte-reactive adapter for the real Trellis connection. */
|
|
171
|
+
getConnection(): SvelteTrellisConnection {
|
|
172
|
+
return this.#getContext().connection;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Installs the connected Trellis runtime into Svelte context synchronously. */
|
|
176
|
+
[provideTrellisContext](trellis: TrellisContextClient): void {
|
|
177
|
+
this.#setContext({
|
|
178
|
+
trellis: trellis as TClient,
|
|
179
|
+
connection: new SvelteTrellisConnection(trellis.connection),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
38
182
|
}
|
|
39
183
|
|
|
40
|
-
|
|
41
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Creates an app-scoped typed Svelte context owner for a Trellis contract and URL.
|
|
186
|
+
*
|
|
187
|
+
* The optional `TClient` type parameter is a type-only facade over the connected
|
|
188
|
+
* runtime client. It should be a generated client facade for `options.contract`.
|
|
189
|
+
*/
|
|
190
|
+
export function createTrellisApp<
|
|
191
|
+
TContract extends TrellisContractLike,
|
|
192
|
+
TClient extends TrellisContextClient = TrellisClientFor<TContract>,
|
|
193
|
+
>(
|
|
194
|
+
options: CreateTrellisAppOptions<TContract>,
|
|
195
|
+
): TrellisApp<TContract, TClient> {
|
|
196
|
+
return new TrellisAppImpl<TContract, TClient>(options);
|
|
42
197
|
}
|
|
43
198
|
|
|
44
|
-
|
|
45
|
-
|
|
199
|
+
function isTrellisAppImpl(
|
|
200
|
+
app: TrellisAppOwner,
|
|
201
|
+
): app is TrellisAppImpl<TrellisContractLike> {
|
|
202
|
+
return app instanceof TrellisAppImpl;
|
|
46
203
|
}
|
|
47
204
|
|
|
48
|
-
|
|
49
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Internal provider helper that synchronously installs connected Trellis context.
|
|
207
|
+
*
|
|
208
|
+
* This is intentionally not exported from `src/index.ts`.
|
|
209
|
+
*/
|
|
210
|
+
export function provideConnectedTrellisContext(
|
|
211
|
+
app: TrellisAppOwner,
|
|
212
|
+
trellis: TrellisContextClient,
|
|
213
|
+
): void {
|
|
214
|
+
if (!isTrellisAppImpl(app)) {
|
|
215
|
+
throw new TypeError("Expected an app created by createTrellisApp");
|
|
216
|
+
}
|
|
217
|
+
app[provideTrellisContext](trellis);
|
|
50
218
|
}
|