@blastlauncher/engine 0.0.0-stage → 3.0.0-alpha.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 +7 -0
- package/README.md +4 -2
- package/dist/ai.d.ts +8 -0
- package/dist/ai.js +98 -0
- package/dist/broker.d.ts +8 -0
- package/dist/broker.js +30 -0
- package/dist/bundle.d.ts +93 -0
- package/dist/bundle.js +620 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.js +11 -0
- package/dist/dependencies.d.ts +7 -0
- package/dist/dependencies.js +11 -0
- package/dist/engine.d.ts +37 -0
- package/dist/engine.js +51 -0
- package/dist/extension-manifest.d.ts +34 -0
- package/dist/extension-manifest.js +79 -0
- package/dist/extension.d.ts +82 -0
- package/dist/extension.js +287 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/install-dependencies.d.ts +30 -0
- package/dist/install-dependencies.js +203 -0
- package/dist/install-peers.d.ts +3 -0
- package/dist/install-peers.js +120 -0
- package/dist/oauth.d.ts +4 -0
- package/dist/oauth.js +99 -0
- package/dist/preferences.d.ts +15 -0
- package/dist/preferences.js +61 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +102 -0
- package/dist/session.d.ts +44 -0
- package/dist/session.js +256 -0
- package/dist/storage.d.ts +31 -0
- package/dist/storage.js +154 -0
- package/dist/tsconfig-paths.d.ts +14 -0
- package/dist/tsconfig-paths.js +329 -0
- package/package.json +26 -4
package/dist/session.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createLineDecoder, encodeMessage, parseHostMessage, parseRuntimeMessage, PROTOCOL_VERSION, } from "@blastlauncher/protocol";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { mkdir } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createAIRouter } from "./ai.js";
|
|
7
|
+
import { createStorage, defaultDataDir, storageFile } from "./storage.js";
|
|
8
|
+
// Variables a Node program needs to behave normally. Everything else in the
|
|
9
|
+
// host environment (tokens, credentials) is withheld from extension code.
|
|
10
|
+
const INHERITED_ENV = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "TZ", "TMPDIR", "TEMP", "TMP"];
|
|
11
|
+
export function runtimeEnvironment(source = process.env) {
|
|
12
|
+
const env = {};
|
|
13
|
+
for (const name of INHERITED_ENV) {
|
|
14
|
+
const value = source[name];
|
|
15
|
+
if (value !== undefined)
|
|
16
|
+
env[name] = value;
|
|
17
|
+
}
|
|
18
|
+
return env;
|
|
19
|
+
}
|
|
20
|
+
/** Resolves after the version handshake. Startup failures reject only after child cleanup. */
|
|
21
|
+
export async function startCommand(options) {
|
|
22
|
+
const ai = options.mode === "model-provider" ? undefined : createAIRouter(options);
|
|
23
|
+
const capabilities = new Map();
|
|
24
|
+
const entry = resolve(options.entry);
|
|
25
|
+
const supportPath = options.environment?.supportPath ??
|
|
26
|
+
join(resolve(options.dataDir ?? defaultDataDir()), "support", basename(storageFile("", options.extensionName), ".json").replace(/^\.{1,2}$/, (name) => name.replaceAll(".", "%2e")));
|
|
27
|
+
await mkdir(supportPath, { recursive: true });
|
|
28
|
+
// A stored Form draft is read before spawning, like preferences, so the
|
|
29
|
+
// command's first render already has `LaunchProps.draftValues` — the
|
|
30
|
+
// runtime never blocks its first scene on a capability round trip for it.
|
|
31
|
+
let draftValues;
|
|
32
|
+
if (options.mode === "view") {
|
|
33
|
+
const internal = createStorage({
|
|
34
|
+
extensionName: options.extensionName,
|
|
35
|
+
dataDir: join(options.dataDir ?? defaultDataDir(), "internal"),
|
|
36
|
+
});
|
|
37
|
+
const stored = await internal.get(`blast:draft:${options.commandName}`);
|
|
38
|
+
if (typeof stored === "string") {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(stored);
|
|
41
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
42
|
+
draftValues = parsed;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A corrupt draft must not block the launch; it is best-effort recall.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const init = {
|
|
51
|
+
type: "init",
|
|
52
|
+
protocol: PROTOCOL_VERSION,
|
|
53
|
+
command: {
|
|
54
|
+
entry,
|
|
55
|
+
extension: options.extensionName,
|
|
56
|
+
name: options.commandName,
|
|
57
|
+
mode: options.mode,
|
|
58
|
+
...(options.argumentValues === undefined ? {} : { arguments: options.argumentValues }),
|
|
59
|
+
...(options.fallbackText === undefined ? {} : { fallbackText: options.fallbackText }),
|
|
60
|
+
...(options.launchContext === undefined ? {} : { launchContext: options.launchContext }),
|
|
61
|
+
...(draftValues === undefined ? {} : { draftValues }),
|
|
62
|
+
},
|
|
63
|
+
preferences: { ...options.preferenceDefaults, ...options.preferences },
|
|
64
|
+
environment: options.environment ?? {
|
|
65
|
+
extensionName: options.extensionName,
|
|
66
|
+
commandName: options.commandName,
|
|
67
|
+
ownerOrAuthorName: options.ownerOrAuthorName ?? "",
|
|
68
|
+
assetsPath: options.assetsPath ?? join(dirname(entry), "assets"),
|
|
69
|
+
supportPath,
|
|
70
|
+
isDevelopment: true,
|
|
71
|
+
// The client can supply its UI theme.
|
|
72
|
+
appearance: "dark",
|
|
73
|
+
textSize: "medium",
|
|
74
|
+
availableAPIs: options.availableAPIs ?? [],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const checked = parseHostMessage(init);
|
|
78
|
+
if (!checked.ok)
|
|
79
|
+
throw new Error(checked.error);
|
|
80
|
+
const child = spawn(options.nodePath ?? "node", [fileURLToPath(import.meta.resolve("@blastlauncher/runtime/main"))], { stdio: ["pipe", "pipe", "pipe"], env: options.env ?? runtimeEnvironment() });
|
|
81
|
+
let ready = false;
|
|
82
|
+
let closed = false;
|
|
83
|
+
let failure;
|
|
84
|
+
let stderr = "";
|
|
85
|
+
let latestScene;
|
|
86
|
+
const logs = [];
|
|
87
|
+
const maxLogs = options.maxLogs ?? 1000;
|
|
88
|
+
const logListeners = new Set();
|
|
89
|
+
function addLog(log) {
|
|
90
|
+
logs.push(log);
|
|
91
|
+
if (logs.length > maxLogs)
|
|
92
|
+
logs.splice(0, logs.length - maxLogs);
|
|
93
|
+
for (const listener of logListeners)
|
|
94
|
+
listener(log);
|
|
95
|
+
}
|
|
96
|
+
const listeners = new Set();
|
|
97
|
+
const timers = new Set();
|
|
98
|
+
let resolveExit;
|
|
99
|
+
const exited = new Promise((done) => {
|
|
100
|
+
resolveExit = done;
|
|
101
|
+
});
|
|
102
|
+
let resolveReady;
|
|
103
|
+
const handshake = new Promise((done) => {
|
|
104
|
+
resolveReady = done;
|
|
105
|
+
});
|
|
106
|
+
function fail(error) {
|
|
107
|
+
failure ??= error;
|
|
108
|
+
child.kill("SIGKILL");
|
|
109
|
+
}
|
|
110
|
+
function protocolError(message) {
|
|
111
|
+
fail(Object.assign(new Error(message), { code: "PROTOCOL_ERROR" }));
|
|
112
|
+
}
|
|
113
|
+
function send(message) {
|
|
114
|
+
if (closed || child.stdin.destroyed)
|
|
115
|
+
return;
|
|
116
|
+
const parsed = parseHostMessage(message);
|
|
117
|
+
if (!parsed.ok)
|
|
118
|
+
throw new Error(parsed.error);
|
|
119
|
+
child.stdin.write(encodeMessage(parsed.message));
|
|
120
|
+
}
|
|
121
|
+
const decoder = createLineDecoder({
|
|
122
|
+
onError: protocolError,
|
|
123
|
+
onLine(value) {
|
|
124
|
+
if (failure)
|
|
125
|
+
return;
|
|
126
|
+
const parsed = parseRuntimeMessage(value);
|
|
127
|
+
if (!parsed.ok)
|
|
128
|
+
return protocolError(parsed.error);
|
|
129
|
+
const message = parsed.message;
|
|
130
|
+
if (!ready) {
|
|
131
|
+
if (message.type === "error")
|
|
132
|
+
return fail(Object.assign(new Error(message.message), { code: message.code }));
|
|
133
|
+
if (message.type !== "ready" || message.protocol !== PROTOCOL_VERSION)
|
|
134
|
+
return protocolError("Expected matching ready handshake");
|
|
135
|
+
ready = true;
|
|
136
|
+
clearTimeout(handshakeTimer);
|
|
137
|
+
resolveReady();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
switch (message.type) {
|
|
141
|
+
case "provider.models":
|
|
142
|
+
case "provider.part":
|
|
143
|
+
case "provider.end":
|
|
144
|
+
case "provider.error":
|
|
145
|
+
if (options.mode !== "model-provider")
|
|
146
|
+
return protocolError("Provider response in command session");
|
|
147
|
+
options.providerResponse?.(message);
|
|
148
|
+
break;
|
|
149
|
+
case "ready":
|
|
150
|
+
return protocolError("Duplicate ready handshake");
|
|
151
|
+
case "scene":
|
|
152
|
+
latestScene = message;
|
|
153
|
+
for (const listener of listeners)
|
|
154
|
+
listener(message);
|
|
155
|
+
break;
|
|
156
|
+
case "log":
|
|
157
|
+
addLog(message);
|
|
158
|
+
break;
|
|
159
|
+
case "error":
|
|
160
|
+
addLog({ type: "log", level: "error", message: `${message.code}: ${message.message}` });
|
|
161
|
+
if (message.fatal)
|
|
162
|
+
failure ??= Object.assign(new Error(message.message), { code: message.code });
|
|
163
|
+
break;
|
|
164
|
+
case "capability.cancel":
|
|
165
|
+
capabilities.get(message.id)?.abort();
|
|
166
|
+
break;
|
|
167
|
+
case "capability.request": {
|
|
168
|
+
const controller = new AbortController();
|
|
169
|
+
capabilities.set(message.id, controller);
|
|
170
|
+
void (ai?.broker ?? options.broker)(message, {
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
progress: (chunk) => {
|
|
173
|
+
if (!controller.signal.aborted)
|
|
174
|
+
send({ type: "capability.progress", id: message.id, value: chunk });
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
.then(send)
|
|
178
|
+
.catch((error) => fail(error instanceof Error ? error : new Error(String(error))))
|
|
179
|
+
.finally(() => capabilities.delete(message.id));
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const handshakeTimer = setTimeout(() => fail(new Error("Runtime handshake timed out")), options.handshakeTimeoutMs ?? 5000);
|
|
186
|
+
child.stdout.on("data", (chunk) => decoder.push(chunk));
|
|
187
|
+
child.stdout.on("end", () => decoder.end());
|
|
188
|
+
child.stderr.setEncoding("utf8").on("data", (message) => {
|
|
189
|
+
stderr += message;
|
|
190
|
+
addLog({ type: "log", level: "error", message });
|
|
191
|
+
});
|
|
192
|
+
child.stdin.on("error", fail);
|
|
193
|
+
child.on("error", fail);
|
|
194
|
+
child.on("close", async (code, signal) => {
|
|
195
|
+
closed = true;
|
|
196
|
+
clearTimeout(handshakeTimer);
|
|
197
|
+
for (const timer of timers)
|
|
198
|
+
clearTimeout(timer);
|
|
199
|
+
listeners.clear();
|
|
200
|
+
for (const controller of capabilities.values())
|
|
201
|
+
controller.abort();
|
|
202
|
+
await ai?.shutdown();
|
|
203
|
+
const startupError = failure ??
|
|
204
|
+
(!ready && stderr.trim() !== ""
|
|
205
|
+
? new Error(`Runtime exited before ready: ${stderr.trim().slice(-1000)}`)
|
|
206
|
+
: undefined);
|
|
207
|
+
resolveExit({ code, signal, ...(startupError ? { error: startupError } : {}) });
|
|
208
|
+
});
|
|
209
|
+
send(init);
|
|
210
|
+
await Promise.race([
|
|
211
|
+
handshake,
|
|
212
|
+
exited.then((exit) => {
|
|
213
|
+
throw exit.error ?? new Error("Runtime exited before ready");
|
|
214
|
+
}),
|
|
215
|
+
]);
|
|
216
|
+
let shutdown;
|
|
217
|
+
return {
|
|
218
|
+
get latestScene() {
|
|
219
|
+
return latestScene;
|
|
220
|
+
},
|
|
221
|
+
get logs() {
|
|
222
|
+
return logs;
|
|
223
|
+
},
|
|
224
|
+
onLog(callback) {
|
|
225
|
+
logListeners.add(callback);
|
|
226
|
+
return () => {
|
|
227
|
+
logListeners.delete(callback);
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
scenes(callback) {
|
|
231
|
+
if (!closed)
|
|
232
|
+
listeners.add(callback);
|
|
233
|
+
if (latestScene)
|
|
234
|
+
callback(latestScene);
|
|
235
|
+
return () => {
|
|
236
|
+
listeners.delete(callback);
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
sendProvider: send,
|
|
240
|
+
sendEvent(node, handler, args) {
|
|
241
|
+
send({ type: "event", node, handler, args });
|
|
242
|
+
},
|
|
243
|
+
shutdown({ timeoutMs = 1000 } = {}) {
|
|
244
|
+
if (shutdown || closed)
|
|
245
|
+
return shutdown ?? exited;
|
|
246
|
+
send({ type: "shutdown" });
|
|
247
|
+
timers.add(setTimeout(() => {
|
|
248
|
+
child.kill("SIGTERM");
|
|
249
|
+
timers.add(setTimeout(() => child.kill("SIGKILL"), timeoutMs));
|
|
250
|
+
}, timeoutMs));
|
|
251
|
+
shutdown = exited;
|
|
252
|
+
return shutdown;
|
|
253
|
+
},
|
|
254
|
+
exited,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CapabilityHandler } from "./broker.js";
|
|
2
|
+
/** The types `LocalStorage.Value` allows. */
|
|
3
|
+
export type StorageValue = string | number | boolean;
|
|
4
|
+
export type StorageItems = Record<string, StorageValue>;
|
|
5
|
+
export type StorageOptions = {
|
|
6
|
+
/** Owning extension; the host takes it from the session's manifest name. */
|
|
7
|
+
extensionName: string;
|
|
8
|
+
/** Root of Blast's data directory; defaults to {@link defaultDataDir}. */
|
|
9
|
+
dataDir?: string;
|
|
10
|
+
};
|
|
11
|
+
export type Storage = {
|
|
12
|
+
get(key: string): Promise<StorageValue | undefined>;
|
|
13
|
+
set(key: string, value: StorageValue): Promise<void>;
|
|
14
|
+
remove(key: string): Promise<void>;
|
|
15
|
+
all(): Promise<StorageItems>;
|
|
16
|
+
clear(): Promise<void>;
|
|
17
|
+
};
|
|
18
|
+
/** `$XDG_DATA_HOME/blast`, or `~/.local/share/blast` when unset or relative. */
|
|
19
|
+
export declare function defaultDataDir(env?: NodeJS.ProcessEnv): string;
|
|
20
|
+
/**
|
|
21
|
+
* The storage file of one extension. Names are untrusted, so anything outside
|
|
22
|
+
* the safe set is percent-encoded: a scoped or hostile name stays one path
|
|
23
|
+
* segment inside the storage directory.
|
|
24
|
+
*/
|
|
25
|
+
export declare function storageFile(dataDir: string, extensionName: string): string;
|
|
26
|
+
export declare function isStorageValue(value: unknown): value is StorageValue;
|
|
27
|
+
/** One store per session; the file is read on every call so two sessions of the
|
|
28
|
+
* same extension see each other's writes. */
|
|
29
|
+
export declare function createStorage(options: StorageOptions): Storage;
|
|
30
|
+
/** The `storage.*` capability handlers for one session. */
|
|
31
|
+
export declare function createStorageHandlers(options: StorageOptions): Record<string, CapabilityHandler>;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Host-side local storage for `LocalStorage` (docs/decisions/0001). Values live
|
|
2
|
+
// in one JSON file per extension below the host data directory. The extension
|
|
3
|
+
// name comes from the session, never from capability params, so a command can
|
|
4
|
+
// only reach its own namespace.
|
|
5
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
8
|
+
/** `$XDG_DATA_HOME/blast`, or `~/.local/share/blast` when unset or relative. */
|
|
9
|
+
export function defaultDataDir(env = process.env) {
|
|
10
|
+
const xdg = env.XDG_DATA_HOME;
|
|
11
|
+
const root = xdg !== undefined && xdg !== "" && isAbsolute(xdg) ? xdg : join(homedir(), ".local", "share");
|
|
12
|
+
return join(root, "blast");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The storage file of one extension. Names are untrusted, so anything outside
|
|
16
|
+
* the safe set is percent-encoded: a scoped or hostile name stays one path
|
|
17
|
+
* segment inside the storage directory.
|
|
18
|
+
*/
|
|
19
|
+
export function storageFile(dataDir, extensionName) {
|
|
20
|
+
if (extensionName === "")
|
|
21
|
+
throw new Error("An extension name is required for local storage");
|
|
22
|
+
const encoded = extensionName.replace(/[^A-Za-z0-9._-]/g, (char) => {
|
|
23
|
+
return `%${char.codePointAt(0).toString(16)}`;
|
|
24
|
+
});
|
|
25
|
+
return join(dataDir, "storage", `${encoded}.json`);
|
|
26
|
+
}
|
|
27
|
+
export function isStorageValue(value) {
|
|
28
|
+
return (typeof value === "string" ||
|
|
29
|
+
(typeof value === "number" && Number.isFinite(value)) ||
|
|
30
|
+
typeof value === "boolean");
|
|
31
|
+
}
|
|
32
|
+
function isStorageItems(value) {
|
|
33
|
+
return (typeof value === "object" &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
!Array.isArray(value) &&
|
|
36
|
+
Object.values(value).every(isStorageValue));
|
|
37
|
+
}
|
|
38
|
+
function checkKey(key) {
|
|
39
|
+
if (typeof key !== "string" || key === "")
|
|
40
|
+
throw new Error("Local storage keys must be non-empty strings");
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
/** One store per session; the file is read on every call so two sessions of the
|
|
44
|
+
* same extension see each other's writes. */
|
|
45
|
+
export function createStorage(options) {
|
|
46
|
+
const file = storageFile(options.dataDir ?? defaultDataDir(), options.extensionName);
|
|
47
|
+
// Mutations are read-modify-write, so they must not interleave; reads join the
|
|
48
|
+
// same queue and never observe a half-applied change.
|
|
49
|
+
let tail = Promise.resolve();
|
|
50
|
+
const serialize = (operation) => {
|
|
51
|
+
const next = tail.then(operation, operation);
|
|
52
|
+
tail = next.catch(() => undefined);
|
|
53
|
+
return next;
|
|
54
|
+
};
|
|
55
|
+
const read = async () => {
|
|
56
|
+
let text;
|
|
57
|
+
try {
|
|
58
|
+
text = await readFile(file, "utf8");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error.code === "ENOENT")
|
|
62
|
+
return {};
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const items = JSON.parse(text);
|
|
66
|
+
if (!isStorageItems(items)) {
|
|
67
|
+
throw new Error(`Local storage for ${options.extensionName} is not a JSON object of strings, numbers and booleans`);
|
|
68
|
+
}
|
|
69
|
+
return items;
|
|
70
|
+
};
|
|
71
|
+
let writes = 0;
|
|
72
|
+
const write = async (items) => {
|
|
73
|
+
await mkdir(dirname(file), { recursive: true });
|
|
74
|
+
// Atomic replace: a crash or a full disk must not leave a truncated file
|
|
75
|
+
// that the next session would read as corrupt.
|
|
76
|
+
const temporary = `${file}.${process.pid}-${++writes}.tmp`;
|
|
77
|
+
try {
|
|
78
|
+
await writeFile(temporary, `${JSON.stringify(items, null, 2)}\n`, "utf8");
|
|
79
|
+
await rename(temporary, file);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
await rm(temporary, { force: true });
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
get: (key) => serialize(async () => {
|
|
88
|
+
return (await read())[checkKey(key)];
|
|
89
|
+
}),
|
|
90
|
+
set: (key, value) => serialize(async () => {
|
|
91
|
+
if (!isStorageValue(value))
|
|
92
|
+
throw new Error("Local storage values must be strings, numbers or booleans");
|
|
93
|
+
const items = await read();
|
|
94
|
+
items[checkKey(key)] = value;
|
|
95
|
+
await write(items);
|
|
96
|
+
}),
|
|
97
|
+
remove: (key) => serialize(async () => {
|
|
98
|
+
const items = await read();
|
|
99
|
+
const name = checkKey(key);
|
|
100
|
+
if (!(name in items))
|
|
101
|
+
return;
|
|
102
|
+
delete items[name];
|
|
103
|
+
await write(items);
|
|
104
|
+
}),
|
|
105
|
+
all: () => serialize(read),
|
|
106
|
+
clear: () => serialize(async () => {
|
|
107
|
+
if (Object.keys(await read()).length === 0)
|
|
108
|
+
return;
|
|
109
|
+
await write({});
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function readParams(params, api) {
|
|
114
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
115
|
+
throw new Error(`${api}: params must be an object`);
|
|
116
|
+
}
|
|
117
|
+
return params;
|
|
118
|
+
}
|
|
119
|
+
// Params carry a key and a value only. Identity is never read from them: the
|
|
120
|
+
// extension a request belongs to is fixed when the session starts.
|
|
121
|
+
function requireKey(params, api) {
|
|
122
|
+
const key = readParams(params, api).key;
|
|
123
|
+
if (typeof key !== "string" || key === "")
|
|
124
|
+
throw new Error(`${api}: "key" must be a non-empty string`);
|
|
125
|
+
return key;
|
|
126
|
+
}
|
|
127
|
+
function requireValue(params, api) {
|
|
128
|
+
const value = readParams(params, api).value;
|
|
129
|
+
if (!isStorageValue(value))
|
|
130
|
+
throw new Error(`${api}: "value" must be a string, number or boolean`);
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
/** The `storage.*` capability handlers for one session. */
|
|
134
|
+
export function createStorageHandlers(options) {
|
|
135
|
+
const storage = createStorage(options);
|
|
136
|
+
// A separate root makes internal keys disjoint from every LocalStorage key.
|
|
137
|
+
const internal = createStorage({
|
|
138
|
+
...options,
|
|
139
|
+
dataDir: join(options.dataDir ?? defaultDataDir(), "internal"),
|
|
140
|
+
});
|
|
141
|
+
const store = (params, api) => readParams(params, api).scope === "blast:internal" ? internal : storage;
|
|
142
|
+
return {
|
|
143
|
+
// A missing key is `undefined` in the extension API; the wire says `null`.
|
|
144
|
+
"storage.get": async (params) => (await store(params, "storage.get").get(requireKey(params, "storage.get"))) ?? null,
|
|
145
|
+
"storage.set": async (params) => {
|
|
146
|
+
await store(params, "storage.set").set(requireKey(params, "storage.set"), requireValue(params, "storage.set"));
|
|
147
|
+
},
|
|
148
|
+
"storage.remove": async (params) => {
|
|
149
|
+
await store(params, "storage.remove").remove(requireKey(params, "storage.remove"));
|
|
150
|
+
},
|
|
151
|
+
"storage.all": () => storage.all(),
|
|
152
|
+
"storage.clear": () => storage.clear(),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type TsconfigAliasResolver = {
|
|
2
|
+
/**
|
|
3
|
+
* Absolute path of an existing file inside the extension directory that a
|
|
4
|
+
* bare specifier resolves to through `paths` or `baseUrl`, else `undefined`.
|
|
5
|
+
*/
|
|
6
|
+
resolve(specifier: string): Promise<string | undefined>;
|
|
7
|
+
/** One line per non-fatal problem found while loading the tsconfig chain. */
|
|
8
|
+
warnings: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads the extension's tsconfig chain and returns a resolver for bare
|
|
12
|
+
* specifiers, or `undefined` when the extension has no `baseUrl`/`paths`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadTsconfigAliases(extensionDir: string): Promise<TsconfigAliasResolver | undefined>;
|