@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.
Files changed (33) hide show
  1. package/README.md +29 -0
  2. package/dist/browser/{FlowCanvas-SqPUBqze.js → FlowCanvas-C4iuvVKB.js} +2 -2
  3. package/dist/browser/{exportPng-CLZKdNzR.js → exportPng-DMsB9ku_.js} +4 -4
  4. package/dist/browser/{flowChanges-CnOOPJJR.js → flowChanges-SKYJBr6X.js} +12 -11
  5. package/dist/browser/{flowWorkspace-d9Ef-aRK.js → flowWorkspace-4UHRaHcz.js} +10 -7
  6. package/dist/browser/{markdownContent-D0BBrFcF.js → markdownContent-B75ojl0E.js} +1 -1
  7. package/dist/browser/preview.css +14 -5
  8. package/dist/browser/preview.d.ts +4 -1
  9. package/dist/browser/preview.js +397 -95
  10. package/dist/browser/workbench-contract.d.ts +3 -0
  11. package/dist/browser/workbench.css +14 -5
  12. package/dist/browser/workbench.js +12 -9
  13. package/dist/browser/{wrapLayout-BB_qwy3k.js → wrapLayout-DU5HG1qH.js} +371 -71
  14. package/dist/command/index.js +13 -13
  15. package/dist/common/cache.d.ts +7 -0
  16. package/dist/common/catalogCache.d.ts +3 -7
  17. package/dist/common/control-api-conformance.js +10 -9
  18. package/dist/common/control-api.d.ts +3 -1
  19. package/dist/common/control-api.js +10 -9
  20. package/dist/common/mcp.js +10 -9
  21. package/dist/common/provider-icons.d.ts +35 -0
  22. package/dist/common/provider-icons.js +134 -0
  23. package/dist/render/browser/runtime.css +14 -5
  24. package/dist/render/browser/runtime.js +667 -365
  25. package/dist/render/common/png.d.ts +7 -0
  26. package/dist/render/node/index.d.ts +10 -2
  27. package/dist/render/node/index.js +1284 -37
  28. package/dist/render/node/licenses.md +24 -0
  29. package/package.json +5 -1
  30. package/src/base/common/cache.ts +8 -0
  31. package/src/connector/common/providerIconCache.ts +191 -0
  32. package/src/control/common/api.ts +10 -3
  33. 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.89",
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(signal?: AbortSignal, flowId?: string, locale?: string): Promise<readonly ConnectorProvider[]> {
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
- /** Storage is host-owned. Keys and records are opaque to the delegate. */
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(this.#request(key, query, load, this.options.maxAgeMs ?? maxAgeMs, fresh || this.options.mode == 'refresh'), signal)
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>(key: string, query: CatalogQuery<Value>, load: CatalogLoad, maxAgeMs: number, fresh: boolean): Promise<ControlCacheRead> {
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.')