diff --git a/src/lib/EnvelopeEditor.svelte b/src/lib/EnvelopeEditor.svelte index 8162429..287f0f5 100644 --- a/src/lib/EnvelopeEditor.svelte +++ b/src/lib/EnvelopeEditor.svelte @@ -4,7 +4,7 @@ import { envelopeDocumentJSON } from '$lib/helpers/envelope' import EditorCode from './editor/code/EditorCode.svelte' import EditorForm from './editor/form/EditorForm.svelte' - import { isEnvelope, setApiBaseUrl, DEFAULT_API_BASE_URL } from '$lib/gobl/client' + import { isEnvelope, setApiBaseUrl } from '$lib/gobl/client' import { problemSeverityMap } from './editor/EditorProblem.js' import * as actions from './editor/actions' import type { BuildOptions, DocumentHeader, State } from './types/editor' @@ -19,7 +19,7 @@ let { jsonSchemaURL = '', - apiBaseUrl = DEFAULT_API_BASE_URL, + apiBaseUrl = '', data = $bindable(''), state: initialState = $bindable('init'), problems = $bindable([]), @@ -41,7 +41,9 @@ }: EnvelopeEditorProps = $props() // Configure the GOBL API endpoint before any operation runs. The initial - // value is applied eagerly during init; the effect keeps it in sync. + // value is applied eagerly during init; the effect keeps it in sync. An + // empty prop is a no-op so an editor without an explicit endpoint inherits + // the currently-configured one instead of resetting it to the default. // svelte-ignore state_referenced_locally setApiBaseUrl(apiBaseUrl) $effect(() => { diff --git a/src/lib/ObjectEditor.svelte b/src/lib/ObjectEditor.svelte index dc1fc2d..accb298 100644 --- a/src/lib/ObjectEditor.svelte +++ b/src/lib/ObjectEditor.svelte @@ -2,13 +2,13 @@ import DynamicForm from '$lib/editor/form/DynamicForm.svelte' import { getUIModel } from '$lib/editor/form/utils/model' import type { SchemaValue } from '$lib/editor/form/utils/schema' - import { setApiBaseUrl, DEFAULT_API_BASE_URL } from '$lib/gobl/client' + import { setApiBaseUrl } from '$lib/gobl/client' import { createBuilderContext } from './store/builder' import type { ObjectEditorProps } from './types/editor' let { jsonSchemaURL = '', - apiBaseUrl = DEFAULT_API_BASE_URL, + apiBaseUrl = '', data = undefined, id = `editor-${Math.random().toString(36).slice(2, 7)}`, readOnly = false, @@ -16,7 +16,9 @@ }: ObjectEditorProps = $props() // Configure the GOBL API endpoint before any schema is fetched. The initial - // value is applied eagerly during init; the effect keeps it in sync. + // value is applied eagerly during init; the effect keeps it in sync. An + // empty prop is a no-op so nested editors (e.g. the correct/headers modals) + // inherit the embedder's endpoint instead of resetting it to the default. // svelte-ignore state_referenced_locally setApiBaseUrl(apiBaseUrl) $effect(() => { diff --git a/src/lib/editor/form/utils/schema.ts b/src/lib/editor/form/utils/schema.ts index d03b93e..22159aa 100644 --- a/src/lib/editor/form/utils/schema.ts +++ b/src/lib/editor/form/utils/schema.ts @@ -35,16 +35,34 @@ function getRelativeSchema(parentSchema: Schema, id: string, del = '/'): Schema return path(parentSchema, id, del) } -async function fetchExternalSchema(id: string): Promise { - let schema = SchemaRegistry[id] - if (schema) return schema +// In-flight schema requests, keyed by URL, so that concurrent consumers +// (the form parser branches and the Monaco preload) share a single fetch +// instead of each missing the registry cache and requesting the same +// schema again. +const pendingSchemas: Record> = {} + +function fetchExternalSchemaStrict(id: string): Promise { + const schema = SchemaRegistry[id] + if (schema) return Promise.resolve(schema) + + let req = pendingSchemas[id] + if (!req) { + req = fetchJsonSchema(id) + .then((fetched) => { + SchemaRegistry[id] = fetched + return fetched + }) + .finally(() => { + delete pendingSchemas[id] + }) + pendingSchemas[id] = req + } + return req +} +async function fetchExternalSchema(id: string): Promise { try { - schema = await fetchJsonSchema(id) - - SchemaRegistry[id] = schema - - return schema + return await fetchExternalSchemaStrict(id) } catch (error) { return EMPTY_SCHEMA } @@ -72,9 +90,7 @@ async function fetchSchema(id: string): Promise { // degrade to an empty placeholder instead. export async function loadSchemaSet(url: string): Promise> { const rootId = url.split('#')[0] - if (!SchemaRegistry[rootId]) { - SchemaRegistry[rootId] = await fetchJsonSchema(rootId) - } + await fetchExternalSchemaStrict(rootId) const found = new Map() const queue = [rootId] diff --git a/src/lib/types/editor.ts b/src/lib/types/editor.ts index f7131ad..65cdb92 100644 --- a/src/lib/types/editor.ts +++ b/src/lib/types/editor.ts @@ -205,9 +205,11 @@ export interface EnvelopeEditorProps { // Used for JSON Schema validation within Monaco Editor. When set, this should be the JSON Schema URL of a GOBL document, e.g. an invoice. Not an envelope. jsonSchemaURL?: string // Base URL of the GOBL API used for build, sign, validate, correct, - // replicate, keygen and schema operations. Defaults to the public service at - // `https://gobl.dev/v0`. Embedders may point this at a same-origin path - // (e.g. `/api/gobl`) that proxies the GOBL API and adds authentication. + // replicate, keygen and schema operations. Embedders may point this at a + // same-origin path (e.g. `/api/gobl`) that proxies the GOBL API and adds + // authentication. When left unset the editor keeps whatever endpoint is + // already configured (the public `https://gobl.dev/v0` service initially), + // so nested editors never reset an embedder's choice. apiBaseUrl?: string // Data is used for setting editor contents. Note: there is "one way" binding; // e.g. you can set data but changes are not bound to the parent. Use the