@swiftbrowser/web 0.8.0 → 0.9.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.
@@ -0,0 +1,77 @@
1
+ // A WebAssembly module without its custom sections, as it streams in. A
2
+ // tool's or plugin's run reads none of them, but a browser keeps them beside
3
+ // the compiled code: the function names are a quarter of the compiler and of
4
+ // the linker, and a macro plugin carries its DWARF.
5
+
6
+ /** Passes a module through, dropping each custom section (id 0). */
7
+ export function withoutCustomSections(): TransformStream<Uint8Array, Uint8Array> {
8
+ // The preamble's bytes still to pass, then a section header's bytes so far
9
+ // (its id and LEB128 size), then the section's bytes to pass or to drop.
10
+ let preamble = 8;
11
+ let header: number[] = [];
12
+ let pass = 0;
13
+ let drop = 0;
14
+ return new TransformStream({
15
+ transform(chunk, controller) {
16
+ let at = 0;
17
+ while (at < chunk.length) {
18
+ if (preamble > 0 || pass > 0) {
19
+ const count = Math.min(preamble > 0 ? preamble : pass, chunk.length - at);
20
+ controller.enqueue(chunk.subarray(at, at + count));
21
+ if (preamble > 0) preamble -= count;
22
+ else pass -= count;
23
+ at += count;
24
+ } else if (drop > 0) {
25
+ const count = Math.min(drop, chunk.length - at);
26
+ drop -= count;
27
+ at += count;
28
+ } else {
29
+ const byte = chunk[at++]!;
30
+ header.push(byte);
31
+ if (header.length === 1 || byte & 0x80) {
32
+ if (header.length > 6) throw new Error('a WebAssembly section size is longer than five bytes');
33
+ continue;
34
+ }
35
+ let size = 0;
36
+ for (let i = header.length - 1; i >= 1; i--) size = size * 128 + (header[i]! & 0x7f);
37
+ if (header[0] === 0) drop = size;
38
+ else {
39
+ controller.enqueue(new Uint8Array(header));
40
+ pass = size;
41
+ }
42
+ header = [];
43
+ }
44
+ }
45
+ },
46
+ flush() {
47
+ if (preamble > 0 || pass > 0 || drop > 0 || header.length > 0) throw new Error('a WebAssembly module ended early');
48
+ },
49
+ });
50
+ }
51
+
52
+ /** `module` without its custom sections, at once: what `withoutCustomSections` streams. */
53
+ export function stripCustomSections(module: Uint8Array): Uint8Array {
54
+ const kept: Uint8Array[] = [module.subarray(0, 8)];
55
+ let at = 8;
56
+ while (at < module.length) {
57
+ const start = at;
58
+ const id = module[at++]!;
59
+ let size = 0;
60
+ for (let shift = 0; ; shift += 7) {
61
+ if (at >= module.length) throw new Error('a WebAssembly module ended early');
62
+ const byte = module[at++]!;
63
+ size += (byte & 0x7f) * 2 ** shift;
64
+ if (!(byte & 0x80)) break;
65
+ }
66
+ if (at + size > module.length) throw new Error('a WebAssembly module ended early');
67
+ if (id !== 0) kept.push(module.subarray(start, at + size));
68
+ at += size;
69
+ }
70
+ const out = new Uint8Array(kept.reduce((total, part) => total + part.length, 0));
71
+ let offset = 0;
72
+ for (const part of kept) {
73
+ out.set(part, offset);
74
+ offset += part.length;
75
+ }
76
+ return out;
77
+ }
@@ -0,0 +1,163 @@
1
+ // Each tool run in a worker of its own, which loads the tool and the SDK's
2
+ // and kit's files it reads from the cache (assets.ts), runs it over an
3
+ // in-memory file system, sends back what it wrote and is ended. A phone's
4
+ // browser runs out of memory where a desktop's does not: this way the
5
+ // compiler's code, its memory and the Swift modules are gone before the
6
+ // linker loads, with its own memory and the archives, and the page's worker
7
+ // holds only the small files. A run compiles its tool again, which a
8
+ // browser does fast enough (a WebAssembly.Module could be sent to the
9
+ // worker instead, but its code would stay as long as the page kept it).
10
+
11
+ import type { Directory } from '@bjorn3/browser_wasi_shim';
12
+ import { type MemoryHost, type RunResult, type ToolName, directoryFiles, memoryDirectory, sdkPaths } from '@swiftbrowser/wasm-compiler';
13
+ import type { Download } from './download';
14
+
15
+ /** What a tool's worker runs. */
16
+ export interface ToolJob {
17
+ /** argv, the program name first. */
18
+ args: string[];
19
+ env: Record<string, string>;
20
+ module: Download;
21
+ /** Guest directory → the files it holds, every mount the tool sees in order. */
22
+ files: Record<string, [string, Uint8Array][]>;
23
+ /** Guest directory → files of it the worker downloads. */
24
+ downloads: Record<string, [string, Download][]>;
25
+ /** The macro plugins, as guest paths of downloads. */
26
+ plugins: string[];
27
+ /** The mounts whose files the worker sends back: those a tool writes to. */
28
+ writable: string[];
29
+ /**
30
+ * Whether the downloads go once the tool has read them (read-once.ts): the
31
+ * linker's, which it keeps whole. Not those named in `keep` (file names).
32
+ */
33
+ readOnce: boolean;
34
+ keep: string[];
35
+ }
36
+
37
+ /** What a tool's worker is sent: a tool to run, or downloads to put in the cache. */
38
+ export type ToToolWorker = { type: 'run'; job: ToolJob } | { type: 'prefetch'; files: Download[] };
39
+ export type ToolReply =
40
+ /** `reopened`: a download the tool opened again after it was dropped (`readOnce`): it has to run again keeping them. */
41
+ | { type: 'done'; result: RunResult; files: Record<string, [string, Uint8Array][]>; reopened?: string }
42
+ /** One of a prefetch's files is in the cache, by its index. */
43
+ | { type: 'fetched'; index: number }
44
+ | { type: 'prefetched' }
45
+ | { type: 'error'; error: string };
46
+
47
+ /**
48
+ * Whether a tool reads a file of the SDK or the kit: the linker their
49
+ * archives and objects, the compiler the rest.
50
+ */
51
+ export const toolReads = (tool: ToolName, file: string): boolean => /\.[ao]$/.test(file) === (tool === 'wasm-ld');
52
+
53
+ export interface ToolHostOptions {
54
+ tools: Partial<Record<ToolName, Download>>;
55
+ /** Guest directory → the files held here. */
56
+ mounts: Record<string, Directory>;
57
+ /** Guest directory → files of it a tool's worker downloads, those it reads (`toolReads`). */
58
+ downloads: Record<string, [string, Download][]>;
59
+ /** Macro plugins by guest path, for the compiler: its worker downloads them too. */
60
+ plugins: Record<string, Download>;
61
+ /** The Swift SDK's `wasm32-unknown-wasip1` directory, as a guest path. */
62
+ sdkRoot: string;
63
+ /** Clang's module cache to start from (memoryHost's). */
64
+ moduleCache?: Iterable<readonly [string, Uint8Array]>;
65
+ }
66
+
67
+ /** The module cache's directory under the host's /tmp, as memoryHost has it. */
68
+ const MODULE_CACHE = 'module-cache';
69
+
70
+ /**
71
+ * Sends `message` to a new tool worker and answers with its last reply (a
72
+ * `done` or `prefetched`), ending the worker then: with it goes whatever it
73
+ * loaded, which no browser frees as surely otherwise.
74
+ */
75
+ function inWorker<Reply extends ToolReply>(message: ToToolWorker, onReply: (reply: ToolReply) => void = () => {}): Promise<Reply> {
76
+ const worker = new Worker(new URL('./tool-worker.ts', import.meta.url), { type: 'module' });
77
+ const name = message.type === 'run' ? message.job.args[0] : 'the download';
78
+ return new Promise<Reply>((resolve, reject) => {
79
+ worker.onmessage = (event: MessageEvent<ToolReply>) => {
80
+ const reply = event.data;
81
+ if (reply.type === 'error') reject(new Error(reply.error));
82
+ else if (reply.type === 'done' || reply.type === 'prefetched') resolve(reply as Reply);
83
+ else onReply(reply);
84
+ };
85
+ worker.onerror = (event) => {
86
+ event.preventDefault();
87
+ reject(new Error(`${name}'s worker failed: ${event.message || 'no message'}`));
88
+ };
89
+ worker.postMessage(message);
90
+ }).finally(() => worker.terminate());
91
+ }
92
+
93
+ /**
94
+ * Puts `files` in the cache from a worker of their own, calling `onFetched`
95
+ * with each one's index as it is in: the brotli decoder's memory, which only
96
+ * grows, goes with the worker.
97
+ */
98
+ export async function prefetchInWorker(files: Download[], onFetched: (index: number) => void = () => {}): Promise<void> {
99
+ await inWorker({ type: 'prefetch', files }, (reply) => reply.type === 'fetched' && onFetched(reply.index));
100
+ }
101
+
102
+ /**
103
+ * The archives and objects a link names more than once, by file name: the
104
+ * linker reads such a file again each time (the SDK's flags and the
105
+ * objects' autolink entries both name -lwasi-emulated-mman, for one).
106
+ */
107
+ export function namedTwice(args: readonly string[]): string[] {
108
+ const counts = new Map<string, number>();
109
+ for (const arg of args) {
110
+ const name = arg.startsWith('-l') ? `lib${arg.slice(2)}.a` : /\.[ao]$/.test(arg) ? arg.split('/').pop()! : null;
111
+ if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
112
+ }
113
+ return [...counts].filter(([, count]) => count > 1).map(([name]) => name).sort();
114
+ }
115
+
116
+ /** A host like memoryHost (wasm-compiler) that runs each tool in its own worker. */
117
+ export function toolHost(options: ToolHostOptions): MemoryHost {
118
+ const tmp = memoryDirectory(options.moduleCache ? [...options.moduleCache].map(([file, data]): [string, Uint8Array] => [`${MODULE_CACHE}/${file}`, data]) : []);
119
+ const plugins = Object.entries(options.plugins);
120
+ const pluginMounts = new Set(plugins.map(([guest]) => `/${guest.split('/')[1]}`));
121
+ const mounts: Record<string, Directory> = { '/': memoryDirectory([]), '/tmp': tmp, ...options.mounts };
122
+ for (const dir of [...Object.keys(options.downloads), ...pluginMounts]) mounts[dir] ??= memoryDirectory([]);
123
+ const downloads: Record<string, [string, Download][]> = { ...options.downloads };
124
+ for (const [guest, file] of plugins) {
125
+ const dir = `/${guest.split('/')[1]}`;
126
+ downloads[dir] = [...(downloads[dir] ?? []), [guest.slice(dir.length + 1), file]];
127
+ }
128
+ const writable = Object.keys(mounts).filter((dir) => !(dir in downloads));
129
+ return {
130
+ description: 'WebAssembly tools, each in a worker, over an in-memory file system',
131
+ sdk: sdkPaths(options.sdkRoot),
132
+ moduleCachePath: `/tmp/${MODULE_CACHE}`,
133
+ moduleCacheFiles() {
134
+ const prefix = `${MODULE_CACHE}/`;
135
+ return directoryFiles(mounts['/tmp']!).filter(([file]) => file.startsWith(prefix)).map(([file, data]) => [file.slice(prefix.length), data]);
136
+ },
137
+ async readFile(file: string): Promise<Uint8Array> {
138
+ const dir = Object.keys(mounts).filter((mount) => file.startsWith(mount === '/' ? '/' : `${mount}/`)).sort((a, b) => b.length - a.length)[0];
139
+ const data = dir === undefined ? undefined : directoryFiles(mounts[dir]!).find(([path]) => `${dir === '/' ? '' : dir}/${path}` === file)?.[1];
140
+ if (!data) throw new Error(`${file}: no such file`);
141
+ return data;
142
+ },
143
+ async run(tool: ToolName, args: readonly string[]): Promise<RunResult> {
144
+ const module = options.tools[tool];
145
+ if (!module) throw new Error(`no WebAssembly module for ${tool}`);
146
+ const job: ToolJob = {
147
+ args: [tool, ...args],
148
+ env: { TMPDIR: '/tmp' },
149
+ module,
150
+ files: Object.fromEntries(Object.entries(mounts).map(([dir, contents]) => [dir, directoryFiles(contents)])),
151
+ downloads: Object.fromEntries(Object.entries(downloads).map(([dir, list]) => [dir, list.filter(([file]) => toolReads(tool, file))])),
152
+ plugins: tool === 'swift-frontend' ? plugins.map(([guest]) => guest) : [],
153
+ writable,
154
+ readOnce: tool === 'wasm-ld',
155
+ keep: namedTwice(args),
156
+ };
157
+ let reply = await inWorker<Extract<ToolReply, { type: 'done' }>>({ type: 'run', job });
158
+ if (reply.reopened) reply = await inWorker<Extract<ToolReply, { type: 'done' }>>({ type: 'run', job: { ...job, readOnce: false } });
159
+ for (const [dir, files] of Object.entries(reply.files)) mounts[dir] = memoryDirectory(files);
160
+ return reply.result;
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,67 @@
1
+ // A tool's worker (tool-host.ts): loads the tool and the files it reads from
2
+ // the cache, runs it once, sends back the files of the mounts it writes to.
3
+ // Or it puts downloads in the cache. The page's worker ends it then, and with
4
+ // it everything it loaded.
5
+
6
+ import { Directory, File } from '@bjorn3/browser_wasi_shim';
7
+ import { directoryFiles, memoryDirectory, runMemoryCommand } from '@swiftbrowser/wasm-compiler';
8
+ import { CONCURRENCY, compileModule, download, downloadBytes, openCache, pooled, prefetch } from './download';
9
+ import { ReadOnceFile } from './read-once';
10
+ import type { ToToolWorker, ToolJob, ToolReply } from './tool-host';
11
+
12
+ const post = (message: ToolReply, transfer: Transferable[] = []): void => (self as unknown as Worker).postMessage(message, transfer);
13
+
14
+ /**
15
+ * The tool's mounts and its plugins compiled, with the downloads in their
16
+ * places; nothing else keeps the downloads' bytes once this returns. With
17
+ * `job.readOnce` the downloads (but `job.keep`) are dropped as soon as the
18
+ * tool has read them (read-once.ts), and `onReopen` hears of one opened
19
+ * again after.
20
+ */
21
+ async function load(job: ToolJob, cache: Cache | null, onReopen: (path: string) => void): Promise<{ mounts: Record<string, Directory>; plugins: Record<string, WebAssembly.Module> }> {
22
+ const wanted = Object.entries(job.downloads).flatMap(([dir, list]) => list.map(([file, from]) => ({ dir, file, from })));
23
+ const downloaded = await pooled(wanted, CONCURRENCY, async ({ dir, file, from }) => ({ dir, file, data: await downloadBytes(cache, from) }));
24
+ const mounts = Object.fromEntries(Object.entries(job.files).map(([dir, list]) => [dir, memoryDirectory(list)]));
25
+ for (const { dir, file, data } of downloaded) {
26
+ const parts = file.split('/');
27
+ let parent = (mounts[dir] ??= memoryDirectory([]));
28
+ for (const part of parts.slice(0, -1)) {
29
+ const next = parent.contents.get(part) ?? new Directory(new Map());
30
+ parent.contents.set(part, next);
31
+ parent = next as Directory;
32
+ }
33
+ // The buffer (a download's bytes are all of it), so the File keeps it rather than a copy.
34
+ const once = job.readOnce && !job.keep.includes(parts.at(-1)!);
35
+ parent.contents.set(parts.at(-1)!, once ? new ReadOnceFile(data, () => onReopen(`${dir}/${file}`)) : new File(data.buffer));
36
+ }
37
+ const plugins = Object.fromEntries(await Promise.all(job.plugins.map(async (guest) => {
38
+ const { data } = downloaded.find(({ dir, file }) => `${dir}/${file}` === guest)!;
39
+ return [guest, await WebAssembly.compile(data)] as const;
40
+ })));
41
+ return { mounts, plugins };
42
+ }
43
+
44
+ async function run(job: ToolJob): Promise<void> {
45
+ const cache = await openCache();
46
+ let reopened: string | undefined;
47
+ const [module, { mounts, plugins }] = await Promise.all([download(cache, job.module).then(compileModule), load(job, cache, (path) => (reopened ??= path))]);
48
+ const result = await runMemoryCommand({ module, args: job.args, mounts, env: job.env, plugins });
49
+ const written = Object.fromEntries(job.writable.map((dir) => [dir, directoryFiles(mounts[dir]!)]));
50
+ const buffers = new Set(Object.values(written).flatMap((list) => list.map(([, data]) => data.buffer as ArrayBuffer)));
51
+ post({ type: 'done', result, files: written, ...(reopened ? { reopened } : {}) }, [...buffers]);
52
+ }
53
+
54
+ self.onmessage = async (event: MessageEvent<ToToolWorker>) => {
55
+ const message = event.data;
56
+ try {
57
+ if (message.type === 'run') return await run(message.job);
58
+ const cache = await openCache();
59
+ await pooled(message.files.map((file, index) => ({ file, index })), CONCURRENCY, async ({ file, index }) => {
60
+ await prefetch(cache, file);
61
+ post({ type: 'fetched', index });
62
+ });
63
+ post({ type: 'prefetched' });
64
+ } catch (error) {
65
+ post({ type: 'error', error: error instanceof Error ? (error.stack ?? error.message) : String(error) });
66
+ }
67
+ };
@@ -1,44 +1,70 @@
1
1
  // The playground's compiler, in a worker: the page sends an app's sources,
2
- // the worker answers with the linked module or the diagnostics. The tools
3
- // and files are loaded once and kept, and each compile starts from the
4
- // module cache the previous one left. The macro plugins arrive after the
5
- // tools: a compile that expands a macro before then waits for them and
6
- // runs again.
2
+ // the worker answers with the linked module or the diagnostics. The files
3
+ // are downloaded once, and each compile starts from the module cache the
4
+ // previous one left; each tool runs in a worker of its own (tool-host.ts).
5
+ // The macro plugins load only when a compile expands a macro: it fails for
6
+ // want of them, waits for them and runs again. Until then they are only
7
+ // downloaded into the cache, once a compile is done.
7
8
 
8
- import { type AppSources, missingPlugin, buildApp } from './build';
9
+ import { memoryDirectory } from '@swiftbrowser/wasm-compiler';
9
10
  import { type LoadedToolchain, loadToolchain } from './assets';
10
- import type { PlaygroundFiles } from './files';
11
+ import { type AppSources, type Toolchain, buildApp, missingPlugin } from './build';
12
+ import type { PlaygroundSource } from './files';
13
+ import { toolHost } from './tool-host';
11
14
 
12
- export type ToWorker = { type: 'compile'; id: number; base: string; files: PlaygroundFiles; app: AppSources };
15
+ export type ToWorker = { type: 'compile'; id: number; source: PlaygroundSource; app: AppSources };
13
16
  export type FromWorker =
14
17
  | { type: 'progress'; text: string }
15
- | { type: 'result'; id: number; ok: boolean; output: string; wasm?: Uint8Array<ArrayBuffer>; steps: { tool: string; seconds: number }[] }
18
+ | { type: 'warning'; text: string }
19
+ | { type: 'result'; id: number; ok: boolean; output: string; wasm?: Uint8Array<ArrayBuffer>; steps: { tool: string; seconds: number; memoryBytes?: number }[] }
16
20
  | { type: 'failure'; id: number; error: string };
17
21
 
18
22
  const post = (message: FromWorker, transfer: Transferable[] = []): void => (self as unknown as Worker).postMessage(message, transfer);
19
23
  let loaded: Promise<LoadedToolchain> | null = null;
20
24
  let moduleCache: [string, Uint8Array][] | null = null;
25
+ /** Whether compiles load the macro plugins: once one has wanted them. */
26
+ let withPlugins = false;
27
+
28
+ /** The toolchain the compiles run on, with or without the plugins. */
29
+ function toolchain(loaded: LoadedToolchain, plugins: boolean): Toolchain {
30
+ const names = plugins ? Object.keys(loaded.plugins) : [];
31
+ return {
32
+ host: ({ mounts, moduleCache }) =>
33
+ toolHost({
34
+ tools: loaded.tools,
35
+ mounts: { ...mounts, '/sdk': memoryDirectory(loaded.sdk.files) },
36
+ downloads: { '/sdk': loaded.sdk.downloads, '/kit': loaded.kit },
37
+ plugins: Object.fromEntries(names.map((name) => [`/plugins/${name}.wasm`, loaded.plugins[name]!])),
38
+ sdkRoot: '/sdk',
39
+ ...(moduleCache ? { moduleCache } : {}),
40
+ }),
41
+ plugins: names,
42
+ kitManifest: loaded.kitManifest,
43
+ icu: loaded.icu,
44
+ };
45
+ }
21
46
 
22
47
  self.onmessage = async (event: MessageEvent<ToWorker>) => {
23
- const { id, base, files, app } = event.data;
48
+ const { id, source, app } = event.data;
24
49
  try {
25
- loaded ??= loadToolchain(base, files, (text) => post({ type: 'progress', text })).then((toolchain) => {
26
- // Later compiles use the plugins as soon as they are in.
27
- toolchain.plugins.then((plugins) => (toolchain.toolchain.plugins = plugins), () => {});
50
+ loaded ??= loadToolchain(source, (text) => post({ type: 'progress', text })).then((toolchain) => {
51
+ if (toolchain.warning) post({ type: 'warning', text: toolchain.warning });
28
52
  return toolchain;
29
53
  });
30
- const { toolchain, plugins, moduleCache: prebuilt } = await loaded;
31
- post({ type: 'progress', text: `compiling ${app.name}…` });
32
- let result = await buildApp(toolchain, app, { moduleCache: moduleCache ?? prebuilt });
54
+ const files = await loaded;
55
+ const onStep = (tool: string): void => post({ type: 'progress', text: `${tool === 'wasm-ld' ? 'linking' : 'compiling'} ${app.name}${withPlugins ? ' with the macro plugins' : ''}…` });
56
+ let result = await buildApp(toolchain(files, withPlugins), app, { moduleCache: moduleCache ?? files.moduleCache, onStep });
33
57
  moduleCache = result.moduleCache;
34
- if (!result.ok && Object.keys(toolchain.plugins).length === 0 && missingPlugin(result.output)) {
58
+ if (!result.ok && !withPlugins && missingPlugin(result.output)) {
35
59
  post({ type: 'progress', text: 'waiting for the macro plugins…' });
36
- toolchain.plugins = await plugins;
37
- post({ type: 'progress', text: `compiling ${app.name} with the macro plugins…` });
38
- result = await buildApp(toolchain, app, { moduleCache });
60
+ await files.prefetchPlugins();
61
+ withPlugins = true;
62
+ result = await buildApp(toolchain(files, withPlugins), app, { moduleCache, onStep });
39
63
  moduleCache = result.moduleCache;
40
64
  }
41
- const steps = result.steps.map(({ tool, seconds }) => ({ tool, seconds }));
65
+ // Behind the result: a macro later finds its plugin downloaded.
66
+ if (!withPlugins) void files.prefetchPlugins().catch(() => {});
67
+ const steps = result.steps.map(({ tool, seconds, memoryBytes }) => ({ tool, seconds, ...(memoryBytes !== undefined ? { memoryBytes } : {}) }));
42
68
  if (result.wasm) {
43
69
  const wasm = new Uint8Array(result.wasm);
44
70
  post({ type: 'result', id, ok: result.ok, output: result.output, wasm, steps }, [wasm.buffer]);
package/src/vite-env.d.ts CHANGED
@@ -26,10 +26,8 @@ interface SBLandingApp {
26
26
  declare const __SB_APPS__: readonly SBLandingApp[];
27
27
  /** The app the app page runs without `?app=` (`SiteOptions.defaultApp`, node/config.ts). */
28
28
  declare const __SB_DEFAULT_APP__: string;
29
- /** Where the playground page fetches its assets (plugins/playground.ts). */
30
- declare const __SB_PLAYGROUND_ASSETS__: string;
31
- /** The playground's downloads, from the assets' manifest (src/playground/files.ts). */
32
- declare const __SB_PLAYGROUND_FILES__: import('./playground/files').PlaygroundFiles;
29
+ /** Where the playground page fetches its npm packages, and their version (plugins/playground.ts); null without the page. */
30
+ declare const __SB_PLAYGROUND__: import('./playground/files').PlaygroundSource;
33
31
  /** What a wide screen shows: `auto` (the frame) or `device` (the full window, a built single-app site). See plugins/site-layout.ts. */
34
32
  declare const __SB_PAGE_MODE__: 'auto' | 'device';
35
33