@oomol-lab/open-flow 0.1.0-beta.89 → 0.1.0-beta.90
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 +29 -0
- package/dist/browser/{FlowCanvas-SqPUBqze.js → FlowCanvas-C4iuvVKB.js} +2 -2
- package/dist/browser/{exportPng-CLZKdNzR.js → exportPng-DMsB9ku_.js} +4 -4
- package/dist/browser/{flowChanges-CnOOPJJR.js → flowChanges-SKYJBr6X.js} +12 -11
- package/dist/browser/{flowWorkspace-d9Ef-aRK.js → flowWorkspace-4UHRaHcz.js} +10 -7
- package/dist/browser/{markdownContent-D0BBrFcF.js → markdownContent-B75ojl0E.js} +1 -1
- package/dist/browser/preview.css +14 -5
- package/dist/browser/preview.d.ts +4 -1
- package/dist/browser/preview.js +397 -95
- package/dist/browser/workbench-contract.d.ts +3 -0
- package/dist/browser/workbench.css +14 -5
- package/dist/browser/workbench.js +12 -9
- package/dist/browser/{wrapLayout-BB_qwy3k.js → wrapLayout-DU5HG1qH.js} +371 -71
- package/dist/command/index.js +13 -13
- package/dist/common/cache.d.ts +7 -0
- package/dist/common/catalogCache.d.ts +3 -7
- package/dist/common/control-api-conformance.js +10 -9
- package/dist/common/control-api.d.ts +3 -1
- package/dist/common/control-api.js +10 -9
- package/dist/common/mcp.js +10 -9
- package/dist/common/provider-icons.d.ts +35 -0
- package/dist/common/provider-icons.js +134 -0
- package/dist/render/browser/runtime.css +14 -5
- package/dist/render/browser/runtime.js +667 -365
- package/dist/render/common/png.d.ts +7 -0
- package/dist/render/node/index.d.ts +10 -2
- package/dist/render/node/index.js +1284 -37
- package/dist/render/node/licenses.md +24 -0
- package/package.json +5 -1
- package/src/base/common/cache.ts +8 -0
- package/src/connector/common/providerIconCache.ts +191 -0
- package/src/control/common/api.ts +10 -3
- package/src/control/common/catalogCache.ts +19 -11
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
The app bundles dependencies which contain the following licenses:
|
|
4
4
|
|
|
5
|
+
## @babel/parser - 8.0.4 (MIT)
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2012-2014 by various contributors (see AUTHORS)
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in
|
|
17
|
+
all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
25
|
+
THE SOFTWARE.
|
|
26
|
+
|
|
27
|
+
## @cfworker/json-schema - 4.1.1 (MIT)
|
|
28
|
+
|
|
5
29
|
## @wopjs/cast - 0.1.16 (MIT)
|
|
6
30
|
|
|
7
31
|
MIT License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oomol-lab/open-flow",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.90",
|
|
4
4
|
"description": "TypeScript contracts and Browser runtime for Open Flow.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"types": "./dist/common/decision.d.ts",
|
|
26
26
|
"import": "./dist/common/decision.js"
|
|
27
27
|
},
|
|
28
|
+
"./provider-icons": {
|
|
29
|
+
"types": "./dist/common/provider-icons.d.ts",
|
|
30
|
+
"import": "./dist/common/provider-icons.js"
|
|
31
|
+
},
|
|
28
32
|
"./flow-state": {
|
|
29
33
|
"types": "./dist/common/flow-state.d.ts",
|
|
30
34
|
"import": "./dist/common/flow-state.js"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonValue } from '../../types/index.ts'
|
|
2
|
+
|
|
3
|
+
/** Storage is host-owned. Keys and JSON records are opaque to the delegate. */
|
|
4
|
+
export interface CacheDelegate {
|
|
5
|
+
get(key: string): Promise<unknown>
|
|
6
|
+
set(key: string, value: JsonValue): Promise<void>
|
|
7
|
+
delete(key: string): Promise<void>
|
|
8
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { CacheDelegate } from '../../base/common/cache.ts'
|
|
2
|
+
|
|
3
|
+
export type { CacheDelegate } from '../../base/common/cache.ts'
|
|
4
|
+
|
|
5
|
+
export type ProviderIconRequest =
|
|
6
|
+
| { readonly kind: 'sprite'; readonly theme: 'light' | 'dark'; readonly url: string; readonly version: string }
|
|
7
|
+
| { readonly kind: 'image'; readonly providerId: string; readonly url: string }
|
|
8
|
+
|
|
9
|
+
export interface ProviderIconCacheOptions {
|
|
10
|
+
/** Non-secret identity of the artwork source or deployment. */
|
|
11
|
+
readonly scope: string
|
|
12
|
+
/** Defaults to instance-local memory. Records contain base64 image bytes. */
|
|
13
|
+
readonly delegate?: CacheDelegate
|
|
14
|
+
readonly mode?: 'default' | 'refresh' | 'offline'
|
|
15
|
+
readonly fetch?: typeof fetch
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProviderIconReadOptions {
|
|
19
|
+
/** Restricts this read even if another caller is loading the same resource online. */
|
|
20
|
+
readonly allowNetwork?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The browser renderer can consume the same interface over a Node bridge. */
|
|
24
|
+
export interface ProviderIconLoader {
|
|
25
|
+
get(request: ProviderIconRequest, options?: ProviderIconReadOptions): Promise<string>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface CachedIcon {
|
|
29
|
+
readonly formatVersion: 1
|
|
30
|
+
readonly sourceUrl: string
|
|
31
|
+
readonly spriteVersion: string | null
|
|
32
|
+
readonly contentType: string
|
|
33
|
+
readonly body: string
|
|
34
|
+
readonly etag: string | null
|
|
35
|
+
readonly validatedAt: number
|
|
36
|
+
/** Null means immutable artwork identified by a sprite version. */
|
|
37
|
+
readonly maxAgeMs: number | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultMaxAgeMs = 24 * 60 * 60 * 1000
|
|
41
|
+
const dataUrl = (entry: CachedIcon) => `data:${entry.contentType};base64,${entry.body}`
|
|
42
|
+
|
|
43
|
+
function decodeRecord(value: unknown): CachedIcon | undefined {
|
|
44
|
+
if (value == null || typeof value !== 'object') return
|
|
45
|
+
const v = value as CachedIcon
|
|
46
|
+
if (
|
|
47
|
+
v.formatVersion !== 1 ||
|
|
48
|
+
typeof v.sourceUrl !== 'string' ||
|
|
49
|
+
!(v.spriteVersion === null || typeof v.spriteVersion === 'string') ||
|
|
50
|
+
typeof v.contentType !== 'string' ||
|
|
51
|
+
!/^image\/[\w.+-]+$/i.test(v.contentType) ||
|
|
52
|
+
typeof v.body !== 'string' ||
|
|
53
|
+
!v.body ||
|
|
54
|
+
v.body.length % 4 !== 0 ||
|
|
55
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(v.body) ||
|
|
56
|
+
!(v.etag === null || typeof v.etag === 'string') ||
|
|
57
|
+
!Number.isFinite(v.validatedAt) ||
|
|
58
|
+
v.validatedAt < 0 ||
|
|
59
|
+
!(v.maxAgeMs === null || (Number.isFinite(v.maxAgeMs) && v.maxAgeMs >= 0))
|
|
60
|
+
)
|
|
61
|
+
return
|
|
62
|
+
return v
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function encodeBytes(bytes: Uint8Array): string {
|
|
66
|
+
let text = ''
|
|
67
|
+
for (let index = 0; index < bytes.length; index += 8192) text += String.fromCharCode(...bytes.subarray(index, index + 8192))
|
|
68
|
+
return btoa(text)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function freshness(response: Response, immutable: boolean, previous?: CachedIcon): number | null {
|
|
72
|
+
const control = response.headers.get('cache-control') ?? ''
|
|
73
|
+
if (/\bno-cache\b/i.test(control)) return 0
|
|
74
|
+
if (immutable) return null
|
|
75
|
+
const maxAge = /(?:^|,)\s*max-age\s*=\s*"?(\d+)/i.exec(control)
|
|
76
|
+
if (maxAge) return Math.max(0, Number(maxAge[1]) * 1000 - (Number(response.headers.get('age')) || 0) * 1000)
|
|
77
|
+
const expires = Date.parse(response.headers.get('expires') ?? '')
|
|
78
|
+
if (Number.isFinite(expires)) return Math.max(0, expires - Date.parse(response.headers.get('date') ?? new Date().toUTCString()))
|
|
79
|
+
return previous?.maxAgeMs ?? defaultMaxAgeMs
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Complete image responses in bounded, identity-checked slots; no catalog entities live here. */
|
|
83
|
+
export class ProviderIconCache implements ProviderIconLoader {
|
|
84
|
+
readonly #memory = new Map<string, CachedIcon | undefined>()
|
|
85
|
+
readonly #pending = new Map<string, Promise<string>>()
|
|
86
|
+
readonly #targets = new Map<string, string>()
|
|
87
|
+
readonly #writes = new Map<string, Promise<void>>()
|
|
88
|
+
|
|
89
|
+
constructor(private readonly options: ProviderIconCacheOptions) {
|
|
90
|
+
if (!options.scope.trim()) throw new TypeError('Provider icon cache scope must be nonempty.')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get(request: ProviderIconRequest, options: ProviderIconReadOptions = {}): Promise<string> {
|
|
94
|
+
if (request.url.startsWith('data:image/')) return Promise.resolve(request.url)
|
|
95
|
+
let protocol: string
|
|
96
|
+
try {
|
|
97
|
+
protocol = new URL(request.url).protocol
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return Promise.reject(error)
|
|
100
|
+
}
|
|
101
|
+
if (!['http:', 'https:'].includes(protocol)) return Promise.reject(new TypeError('Unsupported Provider icon URL.'))
|
|
102
|
+
const slot = JSON.stringify([
|
|
103
|
+
'open-flow:provider-icons',
|
|
104
|
+
1,
|
|
105
|
+
this.options.scope,
|
|
106
|
+
request.kind,
|
|
107
|
+
request.kind === 'sprite' ? request.theme : request.providerId,
|
|
108
|
+
])
|
|
109
|
+
const version = request.kind === 'sprite' ? request.version : null
|
|
110
|
+
const identity = JSON.stringify([request.url, version])
|
|
111
|
+
const offline = options.allowNetwork === false || this.options.mode === 'offline'
|
|
112
|
+
const key = JSON.stringify([slot, identity, offline])
|
|
113
|
+
const pending = this.#pending.get(key)
|
|
114
|
+
if (pending) return pending
|
|
115
|
+
if (!offline) this.#targets.set(slot, identity)
|
|
116
|
+
const promise = this.#load(slot, identity, request.url, version, offline).finally(() => this.#pending.delete(key))
|
|
117
|
+
this.#pending.set(key, promise)
|
|
118
|
+
return promise
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async #read(slot: string, url: string, version: string | null): Promise<CachedIcon | undefined> {
|
|
122
|
+
const memory = this.#memory.get(slot)
|
|
123
|
+
if (this.#memory.has(slot) && (!memory || (memory.sourceUrl === url && memory.spriteVersion === version))) return memory
|
|
124
|
+
if (!this.options.delegate) return this.#memory.get(slot)
|
|
125
|
+
try {
|
|
126
|
+
const stored = await this.options.delegate.get(slot)
|
|
127
|
+
if (stored === undefined) return this.#memory.get(slot)
|
|
128
|
+
const entry = decodeRecord(stored)
|
|
129
|
+
if (entry) {
|
|
130
|
+
this.#memory.set(slot, entry)
|
|
131
|
+
return entry
|
|
132
|
+
}
|
|
133
|
+
this.#memory.delete(slot)
|
|
134
|
+
await this.options.delegate.delete(slot)
|
|
135
|
+
} catch {
|
|
136
|
+
return this.#memory.get(slot)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async #store(slot: string, identity: string, entry: CachedIcon | undefined): Promise<void> {
|
|
141
|
+
const operation = (this.#writes.get(slot) ?? Promise.resolve()).then(async () => {
|
|
142
|
+
if (this.#targets.get(slot) !== identity) return
|
|
143
|
+
this.#memory.set(slot, entry)
|
|
144
|
+
if (!this.options.delegate) return
|
|
145
|
+
try {
|
|
146
|
+
if (entry) await this.options.delegate.set(slot, { ...entry })
|
|
147
|
+
else await this.options.delegate.delete(slot)
|
|
148
|
+
} catch {
|
|
149
|
+
// Successful image loads survive unavailable or full persistent storage.
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
this.#writes.set(slot, operation)
|
|
153
|
+
try {
|
|
154
|
+
await operation
|
|
155
|
+
} finally {
|
|
156
|
+
if (this.#writes.get(slot) === operation) this.#writes.delete(slot)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async #load(slot: string, identity: string, url: string, version: string | null, offline: boolean): Promise<string> {
|
|
161
|
+
const stored = await this.#read(slot, url, version)
|
|
162
|
+
const cached = stored?.sourceUrl === url && stored.spriteVersion === version ? stored : undefined
|
|
163
|
+
const age = cached == null ? Infinity : Date.now() - cached.validatedAt
|
|
164
|
+
if (cached && (offline || (this.options.mode !== 'refresh' && age >= 0 && (cached.maxAgeMs === null || age < cached.maxAgeMs)))) return dataUrl(cached)
|
|
165
|
+
if (offline) throw new Error(`Provider icon is not cached: ${url}`)
|
|
166
|
+
const response = await (this.options.fetch ?? globalThis.fetch)(url, {
|
|
167
|
+
credentials: 'omit',
|
|
168
|
+
referrerPolicy: 'no-referrer',
|
|
169
|
+
signal: AbortSignal.timeout(15_000),
|
|
170
|
+
headers: cached?.etag ? { 'if-none-match': cached.etag } : {},
|
|
171
|
+
})
|
|
172
|
+
if (response.status !== 304 && !response.ok) throw new Error(`Could not load Provider icon: ${response.status}`)
|
|
173
|
+
if (response.status === 304 && !cached) throw new Error('Provider icon returned 304 without a cached image.')
|
|
174
|
+
const contentType = response.status === 304 ? cached!.contentType : response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase()
|
|
175
|
+
if (!contentType || !/^image\/[\w.+-]+$/i.test(contentType)) throw new Error('Provider icon response is not an image.')
|
|
176
|
+
const body = response.status === 304 ? cached!.body : encodeBytes(new Uint8Array(await response.arrayBuffer()))
|
|
177
|
+
if (!body) throw new Error('Provider icon response is empty.')
|
|
178
|
+
const entry: CachedIcon = {
|
|
179
|
+
formatVersion: 1,
|
|
180
|
+
sourceUrl: url,
|
|
181
|
+
spriteVersion: version,
|
|
182
|
+
contentType,
|
|
183
|
+
body,
|
|
184
|
+
etag: response.headers.get('etag') ?? (response.status === 304 ? cached!.etag : null),
|
|
185
|
+
validatedAt: Date.now(),
|
|
186
|
+
maxAgeMs: freshness(response, version !== null, response.status === 304 ? cached : undefined),
|
|
187
|
+
}
|
|
188
|
+
await this.#store(slot, identity, /\bno-store\b/i.test(response.headers.get('cache-control') ?? '') ? undefined : entry)
|
|
189
|
+
return dataUrl(entry)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -1319,9 +1319,15 @@ export class ControlClient {
|
|
|
1319
1319
|
}
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
1322
|
-
async listConnectorProviders(
|
|
1322
|
+
async listConnectorProviders(
|
|
1323
|
+
signal?: AbortSignal,
|
|
1324
|
+
flowId?: string,
|
|
1325
|
+
locale?: string,
|
|
1326
|
+
options: { readonly cacheOnly?: boolean } = {},
|
|
1327
|
+
): Promise<readonly ConnectorProvider[]> {
|
|
1323
1328
|
const query = connectorProvidersQuery(flowId, locale)
|
|
1324
|
-
if (this.catalogCache != null) return (await this.catalogResponse(query, 300_000, signal)).data
|
|
1329
|
+
if (this.catalogCache != null) return (await this.catalogResponse(query, 300_000, signal, false, options.cacheOnly)).data
|
|
1330
|
+
if (options.cacheOnly) throw new ApiError(503, 'catalog.cache-miss', 'No Provider catalog cache is configured.')
|
|
1325
1331
|
return this.connectorRequest(query.path, 'providers', signal, query.decode)
|
|
1326
1332
|
}
|
|
1327
1333
|
|
|
@@ -1695,9 +1701,10 @@ export class ControlClient {
|
|
|
1695
1701
|
maxAgeMs: number,
|
|
1696
1702
|
signal?: AbortSignal,
|
|
1697
1703
|
fresh = false,
|
|
1704
|
+
cacheOnly = false,
|
|
1698
1705
|
) {
|
|
1699
1706
|
if (this.catalogCache != null)
|
|
1700
|
-
return this.catalogCache.read(query, (etag) => this.readCatalog({ path: query.path, decode: jsonValue }, etag), maxAgeMs, signal, fresh)
|
|
1707
|
+
return this.catalogCache.read(query, (etag) => this.readCatalog({ path: query.path, decode: jsonValue }, etag), maxAgeMs, signal, fresh, cacheOnly)
|
|
1701
1708
|
const response = await this.readCatalog(query, null, signal)
|
|
1702
1709
|
if (!response.modified) return invalidResponse()
|
|
1703
1710
|
return { data: response.data, etag: response.etag }
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
import type { CacheDelegate } from '../../base/common/cache.ts'
|
|
1
2
|
import type { JsonValue } from '../../flow/common/change.ts'
|
|
2
3
|
|
|
3
4
|
import { jsonValue } from './decoding.ts'
|
|
4
5
|
import { ApiError } from './errors.ts'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
export interface ControlCacheDelegate {
|
|
8
|
-
get(key: string): Promise<unknown>
|
|
9
|
-
set(key: string, value: JsonValue): Promise<void>
|
|
10
|
-
delete(key: string): Promise<void>
|
|
11
|
-
}
|
|
7
|
+
export type ControlCacheDelegate = CacheDelegate
|
|
12
8
|
|
|
13
9
|
export interface ControlCacheRead {
|
|
14
10
|
readonly path: string
|
|
@@ -85,10 +81,15 @@ export class ControlCatalogCache {
|
|
|
85
81
|
throw new TypeError('Control cache maxAgeMs must be finite and nonnegative.')
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
async read<Value>(query: CatalogQuery<Value>, load: CatalogLoad, maxAgeMs: number, signal?: AbortSignal, fresh = false) {
|
|
84
|
+
async read<Value>(query: CatalogQuery<Value>, load: CatalogLoad, maxAgeMs: number, signal?: AbortSignal, fresh = false, cacheOnly = false) {
|
|
89
85
|
signal?.throwIfAborted()
|
|
90
86
|
const key = JSON.stringify(['open-flow:catalog', 1, this.options.scope, query.path])
|
|
91
|
-
const result = await wait(
|
|
87
|
+
const result = await wait(
|
|
88
|
+
cacheOnly
|
|
89
|
+
? this.#load(key, query, load, this.options.maxAgeMs ?? maxAgeMs, false, true)
|
|
90
|
+
: this.#request(key, query, load, this.options.maxAgeMs ?? maxAgeMs, fresh || this.options.mode == 'refresh'),
|
|
91
|
+
signal,
|
|
92
|
+
)
|
|
92
93
|
const data = query.decode(structuredClone(result.body))
|
|
93
94
|
try {
|
|
94
95
|
this.options.onRead?.({ ...result, body: structuredClone(result.body) })
|
|
@@ -110,7 +111,14 @@ export class ControlCatalogCache {
|
|
|
110
111
|
return promise
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
async #load<Value>(
|
|
114
|
+
async #load<Value>(
|
|
115
|
+
key: string,
|
|
116
|
+
query: CatalogQuery<Value>,
|
|
117
|
+
load: CatalogLoad,
|
|
118
|
+
maxAgeMs: number,
|
|
119
|
+
fresh: boolean,
|
|
120
|
+
cacheOnly = false,
|
|
121
|
+
): Promise<ControlCacheRead> {
|
|
114
122
|
let cached: CachedResponse | undefined
|
|
115
123
|
try {
|
|
116
124
|
const stored = await this.options.delegate.get(key)
|
|
@@ -125,8 +133,8 @@ export class ControlCatalogCache {
|
|
|
125
133
|
}
|
|
126
134
|
const now = Date.now()
|
|
127
135
|
const stale = cached == null || cached.validatedAt > now || now - cached.validatedAt >= maxAgeMs
|
|
128
|
-
if (cached != null && (this.options.mode == 'offline' || (!fresh && !stale))) return { ...cached, path: query.path, source: 'cache', stale }
|
|
129
|
-
if (this.options.mode == 'offline') throw new ApiError(503, 'catalog.cache-miss', `No compatible cached response for ${query.path}.`)
|
|
136
|
+
if (cached != null && (cacheOnly || this.options.mode == 'offline' || (!fresh && !stale))) return { ...cached, path: query.path, source: 'cache', stale }
|
|
137
|
+
if (cacheOnly || this.options.mode == 'offline') throw new ApiError(503, 'catalog.cache-miss', `No compatible cached response for ${query.path}.`)
|
|
130
138
|
|
|
131
139
|
const result = await load(cached?.etag ?? null)
|
|
132
140
|
if (!result.modified && cached == null) throw new ApiError(502, 'response.invalid', 'A catalog returned 304 without a cached response.')
|