@chipmobilesdk/rn-ai 0.2.0 → 0.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/CHANGELOG.md +71 -55
- package/README.md +151 -151
- package/package.json +36 -43
- package/skills/sdk-rn-ai/SKILL.md +56 -57
- package/src/config/behaviorConfig.ts +200 -200
- package/src/core/aiClient.ts +278 -281
- package/src/diagnostics/errors.ts +74 -75
- package/src/diagnostics/messageKeys.ts +27 -27
- package/src/index.ts +31 -35
- package/src/react/hooks.ts +123 -134
- package/src/transport/sseClient.ts +143 -143
- package/src/transport/timeouts.ts +53 -53
|
@@ -1,200 +1,200 @@
|
|
|
1
|
-
import { AiError } from '../diagnostics/errors';
|
|
2
|
-
import { DEFAULT_AI_TIMEOUTS, withTimeout } from '../transport/timeouts';
|
|
3
|
-
|
|
4
|
-
/** App behavior configuration (contracts/app-behavior-config.md). */
|
|
5
|
-
export interface AppBehaviorConfig {
|
|
6
|
-
schemaVersion: number;
|
|
7
|
-
appId: string;
|
|
8
|
-
configVersion: number;
|
|
9
|
-
refreshTtlSeconds: number;
|
|
10
|
-
api: { baseUrl: string; scopes: string[]; supportsStreaming: boolean };
|
|
11
|
-
limitsDisplay?: { requestsPerDay?: number; tokensPerDay?: number };
|
|
12
|
-
recovery: {
|
|
13
|
-
reloadOnHttpStatus: number[];
|
|
14
|
-
maxSilentRetries: number;
|
|
15
|
-
retryBackoffMs: number[];
|
|
16
|
-
};
|
|
17
|
-
features: { aiEnabled: boolean };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Persistence port; default is in-memory (inject a durable store for offline launch). */
|
|
21
|
-
export interface AiConfigCache {
|
|
22
|
-
get(key: string): Promise<string | null>;
|
|
23
|
-
set(key: string, value: string): Promise<void>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class MemoryConfigCache implements AiConfigCache {
|
|
27
|
-
private map = new Map<string, string>();
|
|
28
|
-
async get(key: string): Promise<string | null> {
|
|
29
|
-
return this.map.get(key) ?? null;
|
|
30
|
-
}
|
|
31
|
-
async set(key: string, value: string): Promise<void> {
|
|
32
|
-
this.map.set(key, value);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export type BehaviorState = 'unloaded' | 'ready-network' | 'ready-cache' | 'unavailable';
|
|
37
|
-
|
|
38
|
-
const numbers = (value: unknown): number[] | undefined =>
|
|
39
|
-
Array.isArray(value) ? value.filter((n): n is number => Number.isFinite(n)) : undefined;
|
|
40
|
-
|
|
41
|
-
/** Parse + validate; unknown fields ignored (forward compatibility). */
|
|
42
|
-
export function parseBehaviorConfig(json: string): AppBehaviorConfig {
|
|
43
|
-
let raw: Record<string, unknown>;
|
|
44
|
-
try {
|
|
45
|
-
raw = JSON.parse(json) as Record<string, unknown>;
|
|
46
|
-
} catch {
|
|
47
|
-
throw new AiError('config-unavailable', 'behavior config is not valid JSON');
|
|
48
|
-
}
|
|
49
|
-
const api = raw.api as Partial<AppBehaviorConfig['api']> | undefined;
|
|
50
|
-
if (
|
|
51
|
-
typeof raw.appId !== 'string' ||
|
|
52
|
-
typeof api?.baseUrl !== 'string' ||
|
|
53
|
-
!Array.isArray(api.scopes)
|
|
54
|
-
) {
|
|
55
|
-
throw new AiError('config-unavailable', 'behavior config is missing required fields');
|
|
56
|
-
}
|
|
57
|
-
const recovery = (raw.recovery ?? {}) as Partial<AppBehaviorConfig['recovery']>;
|
|
58
|
-
const features = (raw.features ?? {}) as Partial<AppBehaviorConfig['features']>;
|
|
59
|
-
return {
|
|
60
|
-
schemaVersion: Number(raw.schemaVersion ?? 1),
|
|
61
|
-
appId: raw.appId,
|
|
62
|
-
configVersion: Number(raw.configVersion ?? 0),
|
|
63
|
-
refreshTtlSeconds: Number(raw.refreshTtlSeconds ?? 21600),
|
|
64
|
-
api: {
|
|
65
|
-
baseUrl: api.baseUrl.replace(/\/$/, ''),
|
|
66
|
-
scopes: api.scopes.filter((s): s is string => typeof s === 'string'),
|
|
67
|
-
supportsStreaming: api.supportsStreaming !== false,
|
|
68
|
-
},
|
|
69
|
-
limitsDisplay: raw.limitsDisplay as AppBehaviorConfig['limitsDisplay'],
|
|
70
|
-
// Typed here, once: a remote document with `reloadOnHttpStatus: "404"` used to
|
|
71
|
-
// throw a TypeError inside the XHR error callback, where nothing caught it.
|
|
72
|
-
recovery: {
|
|
73
|
-
reloadOnHttpStatus: numbers(recovery.reloadOnHttpStatus) ?? [404, 410],
|
|
74
|
-
maxSilentRetries: Number.isFinite(recovery.maxSilentRetries)
|
|
75
|
-
? Number(recovery.maxSilentRetries)
|
|
76
|
-
: 1,
|
|
77
|
-
retryBackoffMs: numbers(recovery.retryBackoffMs) ?? [500, 2000],
|
|
78
|
-
},
|
|
79
|
-
features: { aiEnabled: features.aiEnabled !== false },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
interface CacheEnvelope {
|
|
84
|
-
configJson: string;
|
|
85
|
-
etag?: string;
|
|
86
|
-
fetchedAt: number;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface BehaviorConfigDeps {
|
|
90
|
-
configUrl: string;
|
|
91
|
-
appId: string;
|
|
92
|
-
cache?: AiConfigCache;
|
|
93
|
-
fetchFn?: typeof fetch;
|
|
94
|
-
now?: () => number;
|
|
95
|
-
/** Limit on one config fetch, body included. Defaults to 10 s. */
|
|
96
|
-
timeoutMs?: number;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Cache-first behavior config (FR-018): launch never blocks on the network;
|
|
101
|
-
* TTL revalidation uses ETag; designated errors force a reload.
|
|
102
|
-
*/
|
|
103
|
-
export class BehaviorConfigManager {
|
|
104
|
-
private cacheKey: string;
|
|
105
|
-
private cache: AiConfigCache;
|
|
106
|
-
private fetchFn: typeof fetch;
|
|
107
|
-
private now: () => number;
|
|
108
|
-
private current?: { config: AppBehaviorConfig; envelope: CacheEnvelope };
|
|
109
|
-
state: BehaviorState = 'unloaded';
|
|
110
|
-
|
|
111
|
-
constructor(private deps: BehaviorConfigDeps) {
|
|
112
|
-
this.cacheKey = `rn-ai:config:${deps.appId}`;
|
|
113
|
-
this.cache = deps.cache ?? new MemoryConfigCache();
|
|
114
|
-
this.fetchFn = deps.fetchFn ?? fetch;
|
|
115
|
-
this.now = deps.now ?? Date.now;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private isFresh(envelope: CacheEnvelope, config: AppBehaviorConfig): boolean {
|
|
119
|
-
return this.now() - envelope.fetchedAt < config.refreshTtlSeconds * 1000;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private async loadFromCache(): Promise<void> {
|
|
123
|
-
if (this.current) return;
|
|
124
|
-
const stored = await this.cache.get(this.cacheKey).catch(() => null);
|
|
125
|
-
if (!stored) return;
|
|
126
|
-
try {
|
|
127
|
-
const envelope = JSON.parse(stored) as CacheEnvelope;
|
|
128
|
-
this.current = { config: parseBehaviorConfig(envelope.configJson), envelope };
|
|
129
|
-
this.state = 'ready-cache';
|
|
130
|
-
} catch {
|
|
131
|
-
// corrupted cache entries are ignored, not fatal
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private async fetchNetwork(): Promise<AppBehaviorConfig | undefined> {
|
|
136
|
-
const headers: Record<string, string> = {};
|
|
137
|
-
if (this.current?.envelope.etag) {
|
|
138
|
-
headers['If-None-Match'] = this.current.envelope.etag;
|
|
139
|
-
}
|
|
140
|
-
// Bounded: with no cache, a hung fetch used to block every chat and quota call.
|
|
141
|
-
const { response, configJson } = await withTimeout(
|
|
142
|
-
this.deps.timeoutMs ?? DEFAULT_AI_TIMEOUTS.configMs,
|
|
143
|
-
async signal => {
|
|
144
|
-
const res = await this.fetchFn(this.deps.configUrl, { headers, signal });
|
|
145
|
-
return { response: res, configJson: res.ok && res.status !== 304 ? await res.text() : '' };
|
|
146
|
-
},
|
|
147
|
-
() => new AiError('config-unavailable', 'config fetch timed out'),
|
|
148
|
-
);
|
|
149
|
-
if (response.status === 304 && this.current) {
|
|
150
|
-
this.current.envelope.fetchedAt = this.now();
|
|
151
|
-
await this.persist(this.current.envelope);
|
|
152
|
-
this.state = 'ready-network';
|
|
153
|
-
return this.current.config;
|
|
154
|
-
}
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
throw new AiError('config-unavailable', `config fetch failed: ${response.status}`);
|
|
157
|
-
}
|
|
158
|
-
const config = parseBehaviorConfig(configJson);
|
|
159
|
-
const envelope: CacheEnvelope = {
|
|
160
|
-
configJson,
|
|
161
|
-
etag: response.headers.get('etag') ?? undefined,
|
|
162
|
-
fetchedAt: this.now(),
|
|
163
|
-
};
|
|
164
|
-
this.current = { config, envelope };
|
|
165
|
-
await this.persist(envelope);
|
|
166
|
-
this.state = 'ready-network';
|
|
167
|
-
return config;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private async persist(envelope: CacheEnvelope): Promise<void> {
|
|
171
|
-
await this.cache.set(this.cacheKey, JSON.stringify(envelope)).catch(() => undefined);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Cache-first read. Fresh cache returns immediately; stale cache returns
|
|
176
|
-
* immediately too while a background revalidation runs; no cache at all
|
|
177
|
-
* falls through to the network (or `config-unavailable`).
|
|
178
|
-
*/
|
|
179
|
-
async getConfig(options: { forceReload?: boolean } = {}): Promise<AppBehaviorConfig> {
|
|
180
|
-
await this.loadFromCache();
|
|
181
|
-
if (!options.forceReload && this.current) {
|
|
182
|
-
if (!this.isFresh(this.current.envelope, this.current.config)) {
|
|
183
|
-
this.fetchNetwork().catch(() => undefined); // background revalidate
|
|
184
|
-
}
|
|
185
|
-
return this.current.config;
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
const fetched = await this.fetchNetwork();
|
|
189
|
-
if (fetched) return fetched;
|
|
190
|
-
} catch (e) {
|
|
191
|
-
if (this.current) return this.current.config; // stale beats broken
|
|
192
|
-
this.state = 'unavailable';
|
|
193
|
-
throw e instanceof AiError ? e : new AiError('config-unavailable', String(e));
|
|
194
|
-
}
|
|
195
|
-
// fetchNetwork only returns undefined when a 304 raced an empty cache.
|
|
196
|
-
if (this.current) return (this.current as { config: AppBehaviorConfig }).config;
|
|
197
|
-
this.state = 'unavailable';
|
|
198
|
-
throw new AiError('config-unavailable', 'no configuration available');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
1
|
+
import { AiError } from '../diagnostics/errors';
|
|
2
|
+
import { DEFAULT_AI_TIMEOUTS, withTimeout } from '../transport/timeouts';
|
|
3
|
+
|
|
4
|
+
/** App behavior configuration (contracts/app-behavior-config.md). */
|
|
5
|
+
export interface AppBehaviorConfig {
|
|
6
|
+
schemaVersion: number;
|
|
7
|
+
appId: string;
|
|
8
|
+
configVersion: number;
|
|
9
|
+
refreshTtlSeconds: number;
|
|
10
|
+
api: { baseUrl: string; scopes: string[]; supportsStreaming: boolean };
|
|
11
|
+
limitsDisplay?: { requestsPerDay?: number; tokensPerDay?: number };
|
|
12
|
+
recovery: {
|
|
13
|
+
reloadOnHttpStatus: number[];
|
|
14
|
+
maxSilentRetries: number;
|
|
15
|
+
retryBackoffMs: number[];
|
|
16
|
+
};
|
|
17
|
+
features: { aiEnabled: boolean };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Persistence port; default is in-memory (inject a durable store for offline launch). */
|
|
21
|
+
export interface AiConfigCache {
|
|
22
|
+
get(key: string): Promise<string | null>;
|
|
23
|
+
set(key: string, value: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class MemoryConfigCache implements AiConfigCache {
|
|
27
|
+
private map = new Map<string, string>();
|
|
28
|
+
async get(key: string): Promise<string | null> {
|
|
29
|
+
return this.map.get(key) ?? null;
|
|
30
|
+
}
|
|
31
|
+
async set(key: string, value: string): Promise<void> {
|
|
32
|
+
this.map.set(key, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type BehaviorState = 'unloaded' | 'ready-network' | 'ready-cache' | 'unavailable';
|
|
37
|
+
|
|
38
|
+
const numbers = (value: unknown): number[] | undefined =>
|
|
39
|
+
Array.isArray(value) ? value.filter((n): n is number => Number.isFinite(n)) : undefined;
|
|
40
|
+
|
|
41
|
+
/** Parse + validate; unknown fields ignored (forward compatibility). */
|
|
42
|
+
export function parseBehaviorConfig(json: string): AppBehaviorConfig {
|
|
43
|
+
let raw: Record<string, unknown>;
|
|
44
|
+
try {
|
|
45
|
+
raw = JSON.parse(json) as Record<string, unknown>;
|
|
46
|
+
} catch {
|
|
47
|
+
throw new AiError('config-unavailable', 'behavior config is not valid JSON');
|
|
48
|
+
}
|
|
49
|
+
const api = raw.api as Partial<AppBehaviorConfig['api']> | undefined;
|
|
50
|
+
if (
|
|
51
|
+
typeof raw.appId !== 'string' ||
|
|
52
|
+
typeof api?.baseUrl !== 'string' ||
|
|
53
|
+
!Array.isArray(api.scopes)
|
|
54
|
+
) {
|
|
55
|
+
throw new AiError('config-unavailable', 'behavior config is missing required fields');
|
|
56
|
+
}
|
|
57
|
+
const recovery = (raw.recovery ?? {}) as Partial<AppBehaviorConfig['recovery']>;
|
|
58
|
+
const features = (raw.features ?? {}) as Partial<AppBehaviorConfig['features']>;
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: Number(raw.schemaVersion ?? 1),
|
|
61
|
+
appId: raw.appId,
|
|
62
|
+
configVersion: Number(raw.configVersion ?? 0),
|
|
63
|
+
refreshTtlSeconds: Number(raw.refreshTtlSeconds ?? 21600),
|
|
64
|
+
api: {
|
|
65
|
+
baseUrl: api.baseUrl.replace(/\/$/, ''),
|
|
66
|
+
scopes: api.scopes.filter((s): s is string => typeof s === 'string'),
|
|
67
|
+
supportsStreaming: api.supportsStreaming !== false,
|
|
68
|
+
},
|
|
69
|
+
limitsDisplay: raw.limitsDisplay as AppBehaviorConfig['limitsDisplay'],
|
|
70
|
+
// Typed here, once: a remote document with `reloadOnHttpStatus: "404"` used to
|
|
71
|
+
// throw a TypeError inside the XHR error callback, where nothing caught it.
|
|
72
|
+
recovery: {
|
|
73
|
+
reloadOnHttpStatus: numbers(recovery.reloadOnHttpStatus) ?? [404, 410],
|
|
74
|
+
maxSilentRetries: Number.isFinite(recovery.maxSilentRetries)
|
|
75
|
+
? Number(recovery.maxSilentRetries)
|
|
76
|
+
: 1,
|
|
77
|
+
retryBackoffMs: numbers(recovery.retryBackoffMs) ?? [500, 2000],
|
|
78
|
+
},
|
|
79
|
+
features: { aiEnabled: features.aiEnabled !== false },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface CacheEnvelope {
|
|
84
|
+
configJson: string;
|
|
85
|
+
etag?: string;
|
|
86
|
+
fetchedAt: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface BehaviorConfigDeps {
|
|
90
|
+
configUrl: string;
|
|
91
|
+
appId: string;
|
|
92
|
+
cache?: AiConfigCache;
|
|
93
|
+
fetchFn?: typeof fetch;
|
|
94
|
+
now?: () => number;
|
|
95
|
+
/** Limit on one config fetch, body included. Defaults to 10 s. */
|
|
96
|
+
timeoutMs?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Cache-first behavior config (FR-018): launch never blocks on the network;
|
|
101
|
+
* TTL revalidation uses ETag; designated errors force a reload.
|
|
102
|
+
*/
|
|
103
|
+
export class BehaviorConfigManager {
|
|
104
|
+
private cacheKey: string;
|
|
105
|
+
private cache: AiConfigCache;
|
|
106
|
+
private fetchFn: typeof fetch;
|
|
107
|
+
private now: () => number;
|
|
108
|
+
private current?: { config: AppBehaviorConfig; envelope: CacheEnvelope };
|
|
109
|
+
state: BehaviorState = 'unloaded';
|
|
110
|
+
|
|
111
|
+
constructor(private deps: BehaviorConfigDeps) {
|
|
112
|
+
this.cacheKey = `rn-ai:config:${deps.appId}`;
|
|
113
|
+
this.cache = deps.cache ?? new MemoryConfigCache();
|
|
114
|
+
this.fetchFn = deps.fetchFn ?? fetch;
|
|
115
|
+
this.now = deps.now ?? Date.now;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private isFresh(envelope: CacheEnvelope, config: AppBehaviorConfig): boolean {
|
|
119
|
+
return this.now() - envelope.fetchedAt < config.refreshTtlSeconds * 1000;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async loadFromCache(): Promise<void> {
|
|
123
|
+
if (this.current) return;
|
|
124
|
+
const stored = await this.cache.get(this.cacheKey).catch(() => null);
|
|
125
|
+
if (!stored) return;
|
|
126
|
+
try {
|
|
127
|
+
const envelope = JSON.parse(stored) as CacheEnvelope;
|
|
128
|
+
this.current = { config: parseBehaviorConfig(envelope.configJson), envelope };
|
|
129
|
+
this.state = 'ready-cache';
|
|
130
|
+
} catch {
|
|
131
|
+
// corrupted cache entries are ignored, not fatal
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async fetchNetwork(): Promise<AppBehaviorConfig | undefined> {
|
|
136
|
+
const headers: Record<string, string> = {};
|
|
137
|
+
if (this.current?.envelope.etag) {
|
|
138
|
+
headers['If-None-Match'] = this.current.envelope.etag;
|
|
139
|
+
}
|
|
140
|
+
// Bounded: with no cache, a hung fetch used to block every chat and quota call.
|
|
141
|
+
const { response, configJson } = await withTimeout(
|
|
142
|
+
this.deps.timeoutMs ?? DEFAULT_AI_TIMEOUTS.configMs,
|
|
143
|
+
async signal => {
|
|
144
|
+
const res = await this.fetchFn(this.deps.configUrl, { headers, signal });
|
|
145
|
+
return { response: res, configJson: res.ok && res.status !== 304 ? await res.text() : '' };
|
|
146
|
+
},
|
|
147
|
+
() => new AiError('config-unavailable', 'config fetch timed out'),
|
|
148
|
+
);
|
|
149
|
+
if (response.status === 304 && this.current) {
|
|
150
|
+
this.current.envelope.fetchedAt = this.now();
|
|
151
|
+
await this.persist(this.current.envelope);
|
|
152
|
+
this.state = 'ready-network';
|
|
153
|
+
return this.current.config;
|
|
154
|
+
}
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new AiError('config-unavailable', `config fetch failed: ${response.status}`);
|
|
157
|
+
}
|
|
158
|
+
const config = parseBehaviorConfig(configJson);
|
|
159
|
+
const envelope: CacheEnvelope = {
|
|
160
|
+
configJson,
|
|
161
|
+
etag: response.headers.get('etag') ?? undefined,
|
|
162
|
+
fetchedAt: this.now(),
|
|
163
|
+
};
|
|
164
|
+
this.current = { config, envelope };
|
|
165
|
+
await this.persist(envelope);
|
|
166
|
+
this.state = 'ready-network';
|
|
167
|
+
return config;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async persist(envelope: CacheEnvelope): Promise<void> {
|
|
171
|
+
await this.cache.set(this.cacheKey, JSON.stringify(envelope)).catch(() => undefined);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Cache-first read. Fresh cache returns immediately; stale cache returns
|
|
176
|
+
* immediately too while a background revalidation runs; no cache at all
|
|
177
|
+
* falls through to the network (or `config-unavailable`).
|
|
178
|
+
*/
|
|
179
|
+
async getConfig(options: { forceReload?: boolean } = {}): Promise<AppBehaviorConfig> {
|
|
180
|
+
await this.loadFromCache();
|
|
181
|
+
if (!options.forceReload && this.current) {
|
|
182
|
+
if (!this.isFresh(this.current.envelope, this.current.config)) {
|
|
183
|
+
this.fetchNetwork().catch(() => undefined); // background revalidate
|
|
184
|
+
}
|
|
185
|
+
return this.current.config;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const fetched = await this.fetchNetwork();
|
|
189
|
+
if (fetched) return fetched;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (this.current) return this.current.config; // stale beats broken
|
|
192
|
+
this.state = 'unavailable';
|
|
193
|
+
throw e instanceof AiError ? e : new AiError('config-unavailable', String(e));
|
|
194
|
+
}
|
|
195
|
+
// fetchNetwork only returns undefined when a 304 raced an empty cache.
|
|
196
|
+
if (this.current) return (this.current as { config: AppBehaviorConfig }).config;
|
|
197
|
+
this.state = 'unavailable';
|
|
198
|
+
throw new AiError('config-unavailable', 'no configuration available');
|
|
199
|
+
}
|
|
200
|
+
}
|