@ternkit/tern 1.1.0 → 1.2.1
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/dist/goscript/index.svelte.d.ts +44 -0
- package/dist/goscript/index.svelte.js +204 -0
- package/dist/image/Image.svelte +32 -9
- package/dist/image/Image.svelte.d.ts +10 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/runtime/api.d.ts +18 -0
- package/dist/runtime/api.js +55 -0
- package/dist/runtime/base.d.ts +2 -0
- package/dist/runtime/base.js +10 -0
- package/dist/runtime/realtime.svelte.d.ts +78 -0
- package/dist/runtime/realtime.svelte.js +163 -0
- package/dist/runtime/router.svelte.js +2 -0
- package/dist/server/email.d.ts +18 -0
- package/dist/server/email.js +34 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/server/intl.js +1 -1
- package/dist/server/polyfills.d.ts +1 -2
- package/dist/server/polyfills.js +29 -2
- package/dist/server/url.d.ts +56 -0
- package/dist/server/url.js +594 -0
- package/dist/server/web.d.ts +74 -0
- package/dist/server/web.js +395 -0
- package/dist/vite/goscript.d.ts +12 -0
- package/dist/vite/goscript.js +226 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +83 -4
- package/package.json +9 -5
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type Instance = {
|
|
2
|
+
call(name: string, args: string): string | null;
|
|
3
|
+
destroy(): void;
|
|
4
|
+
};
|
|
5
|
+
type GoGlobal = {
|
|
6
|
+
mount(id: string, props: string, emit: (name: string, value: string) => void): Instance | Error;
|
|
7
|
+
};
|
|
8
|
+
declare global {
|
|
9
|
+
var __tern_go: GoGlobal | undefined;
|
|
10
|
+
var __tern_go_ready: (() => void) | undefined;
|
|
11
|
+
var Go: (new () => {
|
|
12
|
+
importObject: WebAssembly.Imports;
|
|
13
|
+
run(instance: WebAssembly.Instance): Promise<void>;
|
|
14
|
+
}) | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface GoComponent {
|
|
17
|
+
/** The values Go published with State and Derived. */
|
|
18
|
+
readonly state: Record<string, unknown>;
|
|
19
|
+
/** Whether the Go instance is running; until then the state is the build-time initial state. */
|
|
20
|
+
readonly ready: boolean;
|
|
21
|
+
/** Calls a Func; returns its result, or undefined before the module loads. */
|
|
22
|
+
call(name: string, ...args: unknown[]): unknown;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Mounts a Go component; code generated from <script lang="go"> calls it.
|
|
26
|
+
* initial is the state Setup produced at build time, so server rendering and
|
|
27
|
+
* the first paint match what Go will compute.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useGo(id: string, initial: Record<string, unknown>, runtime: () => Promise<unknown>, wasm: string, props: () => Record<string, unknown>): GoComponent;
|
|
30
|
+
declare global {
|
|
31
|
+
var __tern_live_initial: ((id: string, props: string) => string) | undefined;
|
|
32
|
+
}
|
|
33
|
+
export interface LiveComponent extends GoComponent {
|
|
34
|
+
/** The server-rendered state, embedded in the page for hydration. */
|
|
35
|
+
readonly stateScript: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
39
|
+
* calls it. On the server, Setup runs in Go for the render, and the state
|
|
40
|
+
* goes into the page; in the browser, the component connects to
|
|
41
|
+
* /_tern/live, where its Go instance runs.
|
|
42
|
+
*/
|
|
43
|
+
export declare function useLive(id: string, uid: string, props: () => Record<string, unknown>): LiveComponent;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// The browser side of the experimental <script lang="go">: loads the app's
|
|
2
|
+
// Go WebAssembly module once, mounts a Go instance per component, and keeps
|
|
3
|
+
// the values Go publishes in Svelte state. See docs/experimental-go-script.md.
|
|
4
|
+
import { getBase } from '../runtime/base.js';
|
|
5
|
+
let loading = null;
|
|
6
|
+
function load(runtime, wasm) {
|
|
7
|
+
loading ??= (async () => {
|
|
8
|
+
await runtime(); // defines globalThis.Go
|
|
9
|
+
const go = new globalThis.Go();
|
|
10
|
+
const ready = new Promise((resolve) => (globalThis.__tern_go_ready = resolve));
|
|
11
|
+
const res = await WebAssembly.instantiateStreaming(fetch(wasm), go.importObject);
|
|
12
|
+
void go.run(res.instance);
|
|
13
|
+
await ready;
|
|
14
|
+
return globalThis.__tern_go;
|
|
15
|
+
})();
|
|
16
|
+
return loading;
|
|
17
|
+
}
|
|
18
|
+
function toArg(a) {
|
|
19
|
+
if (typeof Event !== 'undefined' && a instanceof Event) {
|
|
20
|
+
const t = a.target;
|
|
21
|
+
return { type: a.type, value: t?.value ?? '', checked: !!t?.checked, key: a.key ?? '' };
|
|
22
|
+
}
|
|
23
|
+
return a;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Mounts a Go component; code generated from <script lang="go"> calls it.
|
|
27
|
+
* initial is the state Setup produced at build time, so server rendering and
|
|
28
|
+
* the first paint match what Go will compute.
|
|
29
|
+
*/
|
|
30
|
+
export function useGo(id, initial, runtime, wasm, props) {
|
|
31
|
+
const state = $state({ ...initial });
|
|
32
|
+
let instance = null;
|
|
33
|
+
let ready = $state(false);
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
$effect(() => {
|
|
36
|
+
let alive = true;
|
|
37
|
+
const json = JSON.stringify(props());
|
|
38
|
+
void load(runtime, wasm).then((go) => {
|
|
39
|
+
if (!alive)
|
|
40
|
+
return;
|
|
41
|
+
const m = go.mount(id, json, (name, value) => (state[name] = JSON.parse(value)));
|
|
42
|
+
if (m instanceof Error)
|
|
43
|
+
throw m;
|
|
44
|
+
instance = m;
|
|
45
|
+
ready = true;
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
alive = false;
|
|
49
|
+
instance?.destroy();
|
|
50
|
+
instance = null;
|
|
51
|
+
ready = false;
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
state,
|
|
57
|
+
get ready() {
|
|
58
|
+
return ready;
|
|
59
|
+
},
|
|
60
|
+
call(name, ...args) {
|
|
61
|
+
if (!instance)
|
|
62
|
+
return undefined;
|
|
63
|
+
const out = instance.call(name, JSON.stringify(args.map(toArg)));
|
|
64
|
+
return out === null ? undefined : JSON.parse(out);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
class LiveConnection {
|
|
69
|
+
#ws = null;
|
|
70
|
+
#refs = new Map();
|
|
71
|
+
#queue = [];
|
|
72
|
+
#nextRef = 1;
|
|
73
|
+
#seq = 1;
|
|
74
|
+
#attempt = 0;
|
|
75
|
+
#url;
|
|
76
|
+
constructor(url) {
|
|
77
|
+
this.#url = url;
|
|
78
|
+
}
|
|
79
|
+
#connect() {
|
|
80
|
+
const u = new URL(this.#url, location.href);
|
|
81
|
+
u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
82
|
+
const ws = new WebSocket(u);
|
|
83
|
+
this.#ws = ws;
|
|
84
|
+
ws.onopen = () => {
|
|
85
|
+
this.#attempt = 0;
|
|
86
|
+
// After a reconnect, the server has forgotten every instance.
|
|
87
|
+
for (const [ref, r] of this.#refs)
|
|
88
|
+
ws.send(JSON.stringify({ op: 'mount', ref, id: r.id, props: r.props() }));
|
|
89
|
+
for (const m of this.#queue.splice(0))
|
|
90
|
+
ws.send(m);
|
|
91
|
+
};
|
|
92
|
+
ws.onmessage = (e) => {
|
|
93
|
+
const m = JSON.parse(e.data);
|
|
94
|
+
const r = this.#refs.get(m.ref);
|
|
95
|
+
if (!r)
|
|
96
|
+
return;
|
|
97
|
+
if (m.op === 'state' && m.name)
|
|
98
|
+
r.state[m.name] = m.value;
|
|
99
|
+
else if (m.op === 'result') {
|
|
100
|
+
r.pending.get(m.seq)?.(m.error ? Promise.reject(new Error(m.error)) : m.value);
|
|
101
|
+
r.pending.delete(m.seq);
|
|
102
|
+
}
|
|
103
|
+
else if (m.op === 'error')
|
|
104
|
+
console.error(`[tern] live component ${r.id}: ${m.error}`);
|
|
105
|
+
};
|
|
106
|
+
ws.onclose = () => {
|
|
107
|
+
this.#ws = null;
|
|
108
|
+
if (this.#refs.size === 0)
|
|
109
|
+
return;
|
|
110
|
+
const delay = Math.min(10_000, 300 * 2 ** this.#attempt++);
|
|
111
|
+
setTimeout(() => this.#refs.size && this.#connect(), delay);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
#send(m) {
|
|
115
|
+
const data = JSON.stringify(m);
|
|
116
|
+
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
117
|
+
this.#ws.send(data);
|
|
118
|
+
else {
|
|
119
|
+
this.#queue.push(data);
|
|
120
|
+
if (!this.#ws)
|
|
121
|
+
this.#connect();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
mount(r) {
|
|
125
|
+
const ref = this.#nextRef++;
|
|
126
|
+
this.#refs.set(ref, r);
|
|
127
|
+
this.#send({ op: 'mount', ref, id: r.id, props: r.props() });
|
|
128
|
+
return ref;
|
|
129
|
+
}
|
|
130
|
+
unmount(ref) {
|
|
131
|
+
this.#refs.delete(ref);
|
|
132
|
+
if (this.#ws?.readyState === WebSocket.OPEN)
|
|
133
|
+
this.#ws.send(JSON.stringify({ op: 'unmount', ref }));
|
|
134
|
+
if (this.#refs.size === 0) {
|
|
135
|
+
this.#ws?.close(1000);
|
|
136
|
+
this.#ws = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
call(ref, name, args) {
|
|
140
|
+
const r = this.#refs.get(ref);
|
|
141
|
+
if (!r)
|
|
142
|
+
return Promise.resolve(undefined);
|
|
143
|
+
const seq = this.#seq++;
|
|
144
|
+
return new Promise((resolve) => {
|
|
145
|
+
r.pending.set(seq, resolve);
|
|
146
|
+
this.#send({ op: 'call', ref, seq, name, args });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let connection = null;
|
|
151
|
+
function escapeJSON(s) {
|
|
152
|
+
return s.replace(/</g, '\\u003c');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Mounts a live Go component; code generated from <script lang="go" live>
|
|
156
|
+
* calls it. On the server, Setup runs in Go for the render, and the state
|
|
157
|
+
* goes into the page; in the browser, the component connects to
|
|
158
|
+
* /_tern/live, where its Go instance runs.
|
|
159
|
+
*/
|
|
160
|
+
export function useLive(id, uid, props) {
|
|
161
|
+
const elementID = `tern-live-${uid}`;
|
|
162
|
+
let initial = {};
|
|
163
|
+
let stateScript = '';
|
|
164
|
+
if (typeof window === 'undefined') {
|
|
165
|
+
const raw = globalThis.__tern_live_initial?.(id, JSON.stringify(props())) ?? '{}';
|
|
166
|
+
initial = JSON.parse(raw);
|
|
167
|
+
stateScript = `<script type="application/json" id="${elementID}">${escapeJSON(raw)}</script>`;
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const el = document.getElementById(elementID);
|
|
171
|
+
if (el) {
|
|
172
|
+
initial = JSON.parse(el.textContent || '{}');
|
|
173
|
+
stateScript = el.outerHTML;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const state = $state(initial);
|
|
177
|
+
let ref = 0;
|
|
178
|
+
let ready = $state(false);
|
|
179
|
+
if (typeof window !== 'undefined') {
|
|
180
|
+
$effect(() => {
|
|
181
|
+
connection ??= new LiveConnection(`${getBase()}/_tern/live`);
|
|
182
|
+
const live = { id, props, state, pending: new Map() };
|
|
183
|
+
ref = connection.mount(live);
|
|
184
|
+
ready = true;
|
|
185
|
+
return () => {
|
|
186
|
+
connection?.unmount(ref);
|
|
187
|
+
ref = 0;
|
|
188
|
+
ready = false;
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
state,
|
|
194
|
+
stateScript,
|
|
195
|
+
get ready() {
|
|
196
|
+
return ready;
|
|
197
|
+
},
|
|
198
|
+
call(name, ...args) {
|
|
199
|
+
if (!ref || !connection)
|
|
200
|
+
return undefined;
|
|
201
|
+
return connection.call(ref, name, args.map(toArg));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
package/dist/image/Image.svelte
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
3
|
+
import { getBase } from '../runtime/base.js';
|
|
3
4
|
import type { Picture } from './types.js';
|
|
4
5
|
|
|
5
6
|
interface Props extends Omit<HTMLImgAttributes, 'src' | 'srcset' | 'width' | 'height'> {
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The image: from `import picture from './photo.jpg?image'`, or the URL
|
|
9
|
+
* of a remote image, which the images plugin resizes (give its width
|
|
10
|
+
* and height then).
|
|
11
|
+
*/
|
|
12
|
+
src: Picture | string;
|
|
8
13
|
/** Describes the image; use "" for decorative images. */
|
|
9
14
|
alt: string;
|
|
15
|
+
/** The width the image is shown at, in CSS pixels; required for URLs. */
|
|
16
|
+
width?: number;
|
|
17
|
+
/** The height for that width; required for URLs. */
|
|
18
|
+
height?: number;
|
|
10
19
|
/**
|
|
11
20
|
* How wide the image is displayed, so the browser picks the right file,
|
|
12
21
|
* such as "(min-width: 768px) 50vw, 100vw". Defaults to its full width.
|
|
@@ -16,20 +25,34 @@
|
|
|
16
25
|
priority?: boolean;
|
|
17
26
|
}
|
|
18
27
|
|
|
19
|
-
let { src, alt, sizes, priority = false, ...rest }: Props = $props();
|
|
20
|
-
|
|
28
|
+
let { src, alt, width, height, sizes, priority = false, ...rest }: Props = $props();
|
|
29
|
+
|
|
30
|
+
// The widths the images plugin serves (plugins/images.Widths).
|
|
31
|
+
const widths = [64, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
|
|
32
|
+
|
|
33
|
+
const picture = $derived.by((): Picture => {
|
|
34
|
+
if (typeof src !== 'string') return src;
|
|
35
|
+
const w = width ?? 1200;
|
|
36
|
+
const h = height ?? Math.round(w * 0.75);
|
|
37
|
+
// Up to twice the shown width, for high-density screens.
|
|
38
|
+
const chosen = widths.filter((x) => x <= w * 2);
|
|
39
|
+
if (!chosen.length || chosen.at(-1)! < w * 2) chosen.push(widths.find((x) => x >= w * 2) ?? widths.at(-1)!);
|
|
40
|
+
const url = (x: number) => `${getBase()}/_tern/image?url=${encodeURIComponent(src as string)}&w=${x}`;
|
|
41
|
+
return { src: url(chosen.find((x) => x >= w) ?? chosen.at(-1)!), width: w, height: h, srcset: chosen.map((x) => `${url(x)} ${x}w`).join(', '), sources: [] };
|
|
42
|
+
});
|
|
43
|
+
const fallbackSizes = $derived(`(min-width: ${picture.width}px) ${picture.width}px, 100vw`);
|
|
21
44
|
</script>
|
|
22
45
|
|
|
23
46
|
<picture>
|
|
24
|
-
{#each
|
|
47
|
+
{#each picture.sources as source (source.type)}
|
|
25
48
|
<source type={source.type} srcset={source.srcset} sizes={sizes ?? fallbackSizes} />
|
|
26
49
|
{/each}
|
|
27
50
|
<img
|
|
28
|
-
src={
|
|
29
|
-
srcset={
|
|
51
|
+
src={picture.src}
|
|
52
|
+
srcset={picture.srcset}
|
|
30
53
|
sizes={sizes ?? fallbackSizes}
|
|
31
|
-
width={
|
|
32
|
-
height={
|
|
54
|
+
width={picture.width}
|
|
55
|
+
height={picture.height}
|
|
33
56
|
{alt}
|
|
34
57
|
loading={priority ? 'eager' : 'lazy'}
|
|
35
58
|
fetchpriority={priority ? 'high' : undefined}
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { HTMLImgAttributes } from 'svelte/elements';
|
|
2
2
|
import type { Picture } from './types.js';
|
|
3
3
|
interface Props extends Omit<HTMLImgAttributes, 'src' | 'srcset' | 'width' | 'height'> {
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The image: from `import picture from './photo.jpg?image'`, or the URL
|
|
6
|
+
* of a remote image, which the images plugin resizes (give its width
|
|
7
|
+
* and height then).
|
|
8
|
+
*/
|
|
9
|
+
src: Picture | string;
|
|
6
10
|
/** Describes the image; use "" for decorative images. */
|
|
7
11
|
alt: string;
|
|
12
|
+
/** The width the image is shown at, in CSS pixels; required for URLs. */
|
|
13
|
+
width?: number;
|
|
14
|
+
/** The height for that width; required for URLs. */
|
|
15
|
+
height?: number;
|
|
8
16
|
/**
|
|
9
17
|
* How wide the image is displayed, so the browser picks the right file,
|
|
10
18
|
* such as "(min-width: 768px) 50vw, 100vw". Defaults to its full width.
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type { BeforeNavigateEvent, OnNavigateEvent } from './runtime/navigation.
|
|
|
14
14
|
export { enhance } from './runtime/forms.js';
|
|
15
15
|
export type { SubmitFunction, SubmitInput, SubmitResult } from './runtime/forms.js';
|
|
16
16
|
export type { ActionResult, GotoOptions, Navigation, NavigationTarget, NavigationType } from './runtime/types.js';
|
|
17
|
+
export { ApiError, createApi } from './runtime/api.js';
|
|
18
|
+
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
19
|
+
export type { ConnectionStatus, StreamOptions, SocketOptions } from './runtime/realtime.svelte.js';
|
|
17
20
|
export { default as Image } from './image/Image.svelte';
|
|
18
21
|
export type { Picture } from './image/types.js';
|
|
19
22
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
@@ -25,4 +28,4 @@ export type { MessageKey } from './runtime/i18n.svelte.js';
|
|
|
25
28
|
export interface Messages {
|
|
26
29
|
}
|
|
27
30
|
/** The version of the Tern client runtime. */
|
|
28
|
-
export declare const VERSION = "1.1
|
|
31
|
+
export declare const VERSION = "1.2.1";
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,9 @@ export { page, navigating, env } from './runtime/state.svelte.js';
|
|
|
11
11
|
export { goto, invalidate, invalidateAll, preloadData, applyAction } from './runtime/router.svelte.js';
|
|
12
12
|
export { beforeNavigate, onNavigate, afterNavigate } from './runtime/navigation.js';
|
|
13
13
|
export { enhance } from './runtime/forms.js';
|
|
14
|
+
export { ApiError, createApi } from './runtime/api.js';
|
|
15
|
+
export { stream, socket, EventStreamState, SocketState } from './runtime/realtime.svelte.js';
|
|
14
16
|
export { default as Image } from './image/Image.svelte';
|
|
15
17
|
export { t, i18n, setLocale } from './runtime/i18n.svelte.js';
|
|
16
18
|
/** The version of the Tern client runtime. */
|
|
17
|
-
export const VERSION = '1.1
|
|
19
|
+
export const VERSION = '1.2.1';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** An error answer from a typed endpoint. */
|
|
2
|
+
export declare class ApiError extends Error {
|
|
3
|
+
/** The HTTP status. */
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** The decoded response body, if it was JSON. */
|
|
6
|
+
readonly body: unknown;
|
|
7
|
+
constructor(status: number, message: string, body: unknown);
|
|
8
|
+
}
|
|
9
|
+
type Options = {
|
|
10
|
+
params?: Record<string, unknown>;
|
|
11
|
+
query?: Record<string, unknown>;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
};
|
|
16
|
+
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
17
|
+
export declare function createApi(route: (pattern: string, params?: Record<string, unknown>) => string): (endpoint: string, options?: Options) => Promise<unknown>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** An error answer from a typed endpoint. */
|
|
2
|
+
export class ApiError extends Error {
|
|
3
|
+
/** The HTTP status. */
|
|
4
|
+
status;
|
|
5
|
+
/** The decoded response body, if it was JSON. */
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, message, body) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ApiError';
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Creates the $api client from $routes' route(); generated code calls it. */
|
|
15
|
+
export function createApi(route) {
|
|
16
|
+
return async function api(endpoint, options = {}) {
|
|
17
|
+
const space = endpoint.indexOf(' ');
|
|
18
|
+
const method = endpoint.slice(0, space);
|
|
19
|
+
let url = route(endpoint.slice(space + 1), options.params ?? {});
|
|
20
|
+
const headers = { Accept: 'application/json' };
|
|
21
|
+
const init = { method, headers, credentials: 'same-origin', signal: options.signal };
|
|
22
|
+
if (method === 'GET' || method === 'HEAD' || method === 'DELETE') {
|
|
23
|
+
const q = new URLSearchParams();
|
|
24
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
25
|
+
if (v === undefined || v === null)
|
|
26
|
+
continue;
|
|
27
|
+
for (const item of Array.isArray(v) ? v : [v])
|
|
28
|
+
q.append(k, item instanceof Date ? item.toISOString() : String(item));
|
|
29
|
+
}
|
|
30
|
+
const qs = q.toString();
|
|
31
|
+
if (qs)
|
|
32
|
+
url += '?' + qs;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
headers['Content-Type'] = 'application/json';
|
|
36
|
+
init.body = JSON.stringify(options.body ?? {});
|
|
37
|
+
}
|
|
38
|
+
const res = await (options.fetch ?? fetch)(url, init);
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
let data = null;
|
|
41
|
+
if (text) {
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
data = text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const message = data?.message ?? res.statusText;
|
|
51
|
+
throw new ApiError(res.status, message, data);
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The app's base path, for components that build URLs outside $routes
|
|
2
|
+
// (such as <Image>). The router sets it in the browser and the renderer on
|
|
3
|
+
// the server, so both render the same URLs.
|
|
4
|
+
let base = '';
|
|
5
|
+
export function setBase(b) {
|
|
6
|
+
base = b;
|
|
7
|
+
}
|
|
8
|
+
export function getBase() {
|
|
9
|
+
return base;
|
|
10
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type ConnectionStatus = 'connecting' | 'open' | 'closed';
|
|
2
|
+
export interface StreamOptions<T> {
|
|
3
|
+
/** Event names to listen for. The default is the unnamed "message" event. */
|
|
4
|
+
events?: string[];
|
|
5
|
+
/** The value of current before the first event, such as data from a load. */
|
|
6
|
+
initial?: T;
|
|
7
|
+
/** Called for each event, with its decoded data and name. */
|
|
8
|
+
onMessage?: (data: T, event: string) => void;
|
|
9
|
+
/** Send cookies to another origin. */
|
|
10
|
+
withCredentials?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** A server-sent event stream; see stream(). */
|
|
13
|
+
export declare class EventStreamState<T> {
|
|
14
|
+
#private;
|
|
15
|
+
/** The data of the latest event, decoded from JSON when it is JSON. */
|
|
16
|
+
current: T | undefined;
|
|
17
|
+
/** The connection's status. The browser reconnects by itself after errors. */
|
|
18
|
+
status: ConnectionStatus;
|
|
19
|
+
constructor(url: string, options?: StreamOptions<T>);
|
|
20
|
+
/** Closes the stream. Streams created in a component close with it. */
|
|
21
|
+
close(): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Subscribes to server-sent events from an endpoint that uses
|
|
25
|
+
* ctx.EventStream or realtime.Stream:
|
|
26
|
+
*
|
|
27
|
+
* ```svelte
|
|
28
|
+
* <script lang="ts">
|
|
29
|
+
* import { stream } from '@ternkit/tern';
|
|
30
|
+
* let { data } = $props();
|
|
31
|
+
* const monitors = stream<Monitor[]>('/api/monitors/events', { events: ['monitors'], initial: data.monitors });
|
|
32
|
+
* </script>
|
|
33
|
+
*
|
|
34
|
+
* {#each monitors.current ?? [] as m}…{/each}
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* During server rendering it does not connect and current is the initial
|
|
38
|
+
* value. Created in a component, it closes when the component is destroyed.
|
|
39
|
+
*/
|
|
40
|
+
export declare function stream<T = unknown>(url: string, options?: StreamOptions<T>): EventStreamState<T>;
|
|
41
|
+
export interface SocketOptions<In> {
|
|
42
|
+
/** Called for each message, decoded from JSON when it is JSON. */
|
|
43
|
+
onMessage?: (message: In) => void;
|
|
44
|
+
/** WebSocket subprotocols. */
|
|
45
|
+
protocols?: string[];
|
|
46
|
+
/** Reconnect with backoff after the connection drops. The default is true. */
|
|
47
|
+
reconnect?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** A WebSocket connection; see socket(). */
|
|
50
|
+
export declare class SocketState<In = unknown, Out = unknown> {
|
|
51
|
+
#private;
|
|
52
|
+
/** The latest message received. */
|
|
53
|
+
last: In | undefined;
|
|
54
|
+
/** The connection's status. */
|
|
55
|
+
status: ConnectionStatus;
|
|
56
|
+
constructor(url: string, options?: SocketOptions<In>);
|
|
57
|
+
/** Sends a message as JSON (strings as is). Messages sent while connecting are queued. */
|
|
58
|
+
send(message: Out): void;
|
|
59
|
+
/** Closes the connection. Sockets created in a component close with it. */
|
|
60
|
+
close(): void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Opens a WebSocket to an endpoint that uses ctx.WebSocket, with JSON
|
|
64
|
+
* messages and automatic reconnection:
|
|
65
|
+
*
|
|
66
|
+
* ```svelte
|
|
67
|
+
* <script lang="ts">
|
|
68
|
+
* import { socket } from '@ternkit/tern';
|
|
69
|
+
* const chat = socket<ChatMessage, { text: string }>('/api/chat', { onMessage: (m) => messages.push(m) });
|
|
70
|
+
* </script>
|
|
71
|
+
*
|
|
72
|
+
* <button onclick={() => chat.send({ text })}>Send</button>
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* It does not connect during server rendering. Created in a component, it
|
|
76
|
+
* closes when the component is destroyed.
|
|
77
|
+
*/
|
|
78
|
+
export declare function socket<In = unknown, Out = unknown>(url: string, options?: SocketOptions<In>): SocketState<In, Out>;
|