@bagelink/vue 1.16.0 → 1.17.2

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 (57) hide show
  1. package/dist/assetTransport.d.ts +50 -0
  2. package/dist/assetTransport.d.ts.map +1 -0
  3. package/dist/assetTransport.test.d.ts +2 -0
  4. package/dist/assetTransport.test.d.ts.map +1 -0
  5. package/dist/assets.d.ts +46 -0
  6. package/dist/assets.d.ts.map +1 -0
  7. package/dist/assets.test.d.ts +2 -0
  8. package/dist/assets.test.d.ts.map +1 -0
  9. package/dist/components/Image.vue.d.ts +7 -1
  10. package/dist/components/Image.vue.d.ts.map +1 -1
  11. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
  12. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
  13. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
  14. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
  15. package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
  16. package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
  17. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  18. package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
  19. package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
  20. package/dist/components/lightbox/index.d.ts.map +1 -1
  21. package/dist/components/lightbox/lightbox.types.d.ts +7 -0
  22. package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
  23. package/dist/composables/useImageSrc.d.ts +8 -4
  24. package/dist/composables/useImageSrc.d.ts.map +1 -1
  25. package/dist/core.css +1 -1
  26. package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
  27. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
  28. package/dist/form-flow/form-flow.d.ts +11 -6
  29. package/dist/form-flow/form-flow.d.ts.map +1 -1
  30. package/dist/form-flow/schema-fields.d.ts +3 -0
  31. package/dist/form-flow/schema-fields.d.ts.map +1 -1
  32. package/dist/index.cjs +62 -62
  33. package/dist/index.css +1 -1
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.mjs +19554 -18982
  37. package/dist/style.css +1 -1
  38. package/dist/types/BagelForm.d.ts +3 -1
  39. package/dist/types/BagelForm.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/assetTransport.test.ts +125 -0
  42. package/src/assetTransport.ts +164 -0
  43. package/src/assets.test.ts +123 -0
  44. package/src/assets.ts +245 -0
  45. package/src/components/Image.vue +19 -3
  46. package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
  47. package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
  48. package/src/components/lightbox/AssetPreview.vue +16 -0
  49. package/src/components/lightbox/Lightbox.vue +8 -3
  50. package/src/components/lightbox/LightboxImage.vue +4 -3
  51. package/src/components/lightbox/index.ts +29 -4
  52. package/src/components/lightbox/lightbox.types.ts +5 -1
  53. package/src/composables/useImageSrc.ts +47 -37
  54. package/src/form-flow/form-flow.ts +26 -7
  55. package/src/form-flow/schema-fields.ts +23 -1
  56. package/src/index.ts +3 -0
  57. package/src/types/BagelForm.ts +3 -1
@@ -98,6 +98,8 @@ export interface UploadInputValueObject {
98
98
  src?: string;
99
99
  }
100
100
  export interface UploadInputProps extends BagelInputShellProps {
101
+ /** Opt-in stable reference output; defaults to legacy path-key behavior. */
102
+ assetMode?: boolean;
101
103
  id?: string;
102
104
  name?: string;
103
105
  label?: string;
@@ -106,7 +108,7 @@ export interface UploadInputProps extends BagelInputShellProps {
106
108
  * Path-key string(s). Legacy/migrated records may also provide an image object
107
109
  * (e.g. `{ imageURL, imageName, extension, altText }`); it is normalized internally.
108
110
  */
109
- modelValue?: string | string[] | UploadInputValueObject | (string | UploadInputValueObject)[];
111
+ modelValue?: string | string[] | UploadInputValueObject | import('../assets').AssetReference | (string | UploadInputValueObject | import('../assets').AssetReference)[];
110
112
  /** Optional upload handler. When omitted, the shared file-storage uploader is used. */
111
113
  upload?: (file: File) => Promise<string>;
112
114
  width?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"BagelForm.d.ts","sourceRoot":"","sources":["../../src/types/BagelForm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BagelForm.d.ts","sourceRoot":"","sources":["../../src/types/BagelForm.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.16.0",
4
+ "version": "1.17.2",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -0,0 +1,125 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { configureAssetClient, createAssetHttpTransport, type AssetFetch } from './assetTransport'
3
+ import { createAssetClient } from './assets'
4
+ import type { ApiTransport } from '@bagelink/sdk'
5
+ import { createApp, ref } from 'vue'
6
+
7
+ const session = { asset_id: 'a', version_id: 'v', upload_id: 'u', status: 'ready' }
8
+ const json = (value: unknown, status = 200) => new Response(JSON.stringify(value), { status })
9
+ const signal = () => new AbortController().signal
10
+
11
+ describe('asset REST transport', () => {
12
+ it('reuses authenticated transport and synchronously invalidates on tenant changes', async () => {
13
+ const request = vi.fn().mockImplementation(() => new Promise(() => {}))
14
+ const transport = { axios: { defaults: { baseURL: 'https://api.example/api' }, request } } as unknown as ApiTransport
15
+ const sessionKey = ref('account:tenant-a')
16
+ const assets = configureAssetClient({ transport, sessionKey })
17
+ createApp({}).use(assets)
18
+ const result = assets.client.resolve({ asset_id: 'a', representation: 'website' })
19
+ const rejected = expect(result).rejects.toMatchObject({ name: 'AbortError' })
20
+ await Promise.resolve()
21
+ expect(request.mock.calls[0]![0].url).toBe('assets/resolve')
22
+ expect(JSON.parse(request.mock.calls[0]![0].data).references[0].representation).toBe('website')
23
+ sessionKey.value = 'account:tenant-b'
24
+ await rejected
25
+ expect(request.mock.calls[0]![0].signal.aborted).toBe(true)
26
+ })
27
+
28
+ it('publishes only explicit image purposes with stable version-pinned URLs', async () => {
29
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValue(json({
30
+ asset_id: 'a', version_id: 'v', published: true, name: 'photo',
31
+ representations: ['website', 'social'].map(representation => ({
32
+ representation, url: `/assets/a/public/v/${representation}`, media_type: 'image/jpeg', size: 42,
33
+ })),
34
+ }))
35
+ const transport = createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
36
+ const result = await transport.publish({ asset_id: 'a', version_id: 'v' })
37
+ expect(JSON.parse(String(fetcher.mock.calls[0]![1]?.body))).toEqual({
38
+ published: true, version_id: 'v', representations: ['website', 'social'],
39
+ })
40
+ expect(result.representations[1]?.url).toBe('https://api.example/assets/a/public/v/social')
41
+ })
42
+
43
+ it('rejects private grant URLs returned by publication', async () => {
44
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValue(json({
45
+ asset_id: 'a', version_id: 'v', published: true,
46
+ representations: [{ representation: 'website', url: '/assets/a/content?token=secret' }],
47
+ }))
48
+ await expect(createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
49
+ .publish({ asset_id: 'a', version_id: 'v' })).rejects.toThrow('Invalid public')
50
+ })
51
+
52
+ it('starts a session and sends unchanged raw bytes, returning pinned references', async () => {
53
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValueOnce(json(session)).mockResolvedValueOnce(json(session))
54
+ const transport = createAssetHttpTransport({ baseURL: 'https://api.example/api', fetch: fetcher })
55
+ const file = new File(['original'], 'test.heic')
56
+ expect(await transport.upload(file, signal())).toEqual({ asset_id: 'a', version_id: 'v' })
57
+ expect(String(fetcher.mock.calls[0]![0])).toBe('https://api.example/api/assets/uploads')
58
+ expect(fetcher.mock.calls[0]![1]?.body).toBe('{"name":"test.heic"}')
59
+ expect(fetcher.mock.calls[1]![1]?.body).toBe(file)
60
+ expect(new Headers(fetcher.mock.calls[1]![1]?.headers).get('Content-Type')).toBe('application/octet-stream')
61
+ })
62
+
63
+ it('fails closed for rejected completion and HTTP errors', async () => {
64
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValueOnce(json(session))
65
+ .mockResolvedValueOnce(json({ ...session, status: 'rejected' })).mockResolvedValueOnce(json({}, 403))
66
+ const transport = createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
67
+ await expect(transport.upload(new File(['x'], 'x'), signal())).rejects.toThrow('not ready')
68
+ await expect(transport.resolve([{ asset_id: 'a' }], signal())).rejects.toThrow('(403)')
69
+ })
70
+
71
+ it('chunks 101 references and resolves relative URLs against the API origin', async () => {
72
+ const fetcher = vi.fn<AssetFetch>(async (_url, init) => {
73
+ const { references } = JSON.parse(String(init?.body)) as { references: { asset_id: string }[] }
74
+ return json({ items: references.map(reference => ({
75
+ ...reference, version_id: 'v', representation: 'original',
76
+ url: '/assets/content?token=secret', expires_at: new Date(Date.now() + 60000).toISOString(),
77
+ })) })
78
+ })
79
+ const transport = createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
80
+ const items = await transport.resolve(Array.from({ length: 101 }, (_, i) => ({ asset_id: String(i) })), signal())
81
+ expect(fetcher.mock.calls.map(call => JSON.parse(String(call[1]?.body)).references.length)).toEqual([50, 50, 1])
82
+ expect(items).toHaveLength(101)
83
+ expect(items[0]?.url).toBe('https://api.example/assets/content?token=secret')
84
+ })
85
+
86
+ it('rejects untrusted delivery origins', async () => {
87
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValue(json({ items: [{
88
+ asset_id: 'a', version_id: 'v', representation: 'original', url: '//evil.example/file',
89
+ }] }))
90
+ await expect(createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
91
+ .resolve([{ asset_id: 'a' }], signal())).rejects.toThrow('Untrusted')
92
+ })
93
+
94
+ it('does not PUT if session creation fails', async () => {
95
+ const fetcher = vi.fn<AssetFetch>().mockResolvedValueOnce(json({}, 503))
96
+ const transport = createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher })
97
+ await expect(transport.upload(new File(['x'], 'x'), signal())).rejects.toThrow('(503)')
98
+ expect(fetcher).toHaveBeenCalledTimes(1)
99
+ })
100
+
101
+ it('does not complete an upload after session cancellation', async () => {
102
+ let finish!: (response: Response) => void
103
+ const fetcher = vi.fn<AssetFetch>().mockImplementationOnce(() => new Promise(resolve => { finish = resolve }))
104
+ const client = createAssetClient(createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher }))
105
+ const promise = client.upload(new File(['x'], 'x'))
106
+ client.clearSession()
107
+ finish(json(session))
108
+ await expect(promise).rejects.toMatchObject({ name: 'AbortError' })
109
+ expect(fetcher).toHaveBeenCalledTimes(1)
110
+ })
111
+
112
+ it('session cleanup aborts the HTTP request', async () => {
113
+ let requestSignal: AbortSignal | null | undefined
114
+ const fetcher = vi.fn<AssetFetch>((_url, init) => {
115
+ requestSignal = init?.signal
116
+ return new Promise((_resolve, reject) => requestSignal?.addEventListener('abort', () => reject(requestSignal?.reason)))
117
+ })
118
+ const client = createAssetClient(createAssetHttpTransport({ baseURL: 'https://api.example', fetch: fetcher }))
119
+ const promise = client.resolve({ asset_id: 'a' })
120
+ await Promise.resolve()
121
+ client.clearSession()
122
+ await expect(promise).rejects.toMatchObject({ name: 'AbortError' })
123
+ expect(requestSignal?.aborted).toBe(true)
124
+ })
125
+ })
@@ -0,0 +1,164 @@
1
+ import { createAssetClient, ASSET_CLIENT_KEY, type AssetDelivery, type AssetTransport, type AssetReference, type AssetPurpose } from './assets'
2
+ import type { ApiTransport } from '@bagelink/sdk'
3
+ import { watch, toValue, type App, type MaybeRefOrGetter } from 'vue'
4
+
5
+ export type AssetFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
6
+
7
+ export interface AssetHttpOptions {
8
+ /** Absolute API base, optionally with a path prefix. */
9
+ baseURL: string
10
+ headers?: () => HeadersInit
11
+ fetch?: AssetFetch
12
+ credentials?: RequestCredentials
13
+ /** Explicitly trusted delivery origins besides the API origin. */
14
+ deliveryOrigins?: string[]
15
+ /** Reuse the app's authenticated axios instance, including tenant interceptors. */
16
+ apiTransport?: ApiTransport
17
+ }
18
+
19
+ interface UploadResponse {
20
+ asset_id: string
21
+ version_id: string
22
+ upload_id: string
23
+ status: string
24
+ }
25
+ interface DeliveryResponse extends AssetDelivery {
26
+ asset_id: string
27
+ representation: string
28
+ }
29
+
30
+ /** Concrete REST adapter, separate from generated SDK namespaces. */
31
+ export function createAssetHttpTransport(options: AssetHttpOptions): AssetTransport & {
32
+ publish(reference: Required<AssetReference>, signal?: AbortSignal): Promise<AssetPublication>
33
+ } {
34
+ const base = new URL(options.baseURL.endsWith('/') ? options.baseURL : `${options.baseURL}/`)
35
+ if (!['http:', 'https:'].includes(base.protocol) || base.username || base.password) throw new Error('Invalid API base')
36
+ const trusted = new Set([base.origin, ...(options.deliveryOrigins ?? []).map(origin => new URL(origin).origin)])
37
+ const request = options.fetch ?? globalThis.fetch
38
+ async function send<T>(path: string, init: RequestInit, signal: AbortSignal): Promise<T> {
39
+ signal.throwIfAborted()
40
+ const headers = new Headers(options.headers?.())
41
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value))
42
+ if (options.apiTransport) {
43
+ const requestHeaders: Record<string, string> = {}
44
+ headers.forEach((value, key) => { requestHeaders[key] = value })
45
+ const response = await options.apiTransport.axios.request<T>({
46
+ url: `assets/${path}`, method: init.method, data: init.body,
47
+ headers: requestHeaders, signal,
48
+ })
49
+ signal.throwIfAborted()
50
+ return response.data
51
+ }
52
+ const response = await request(new URL(`assets/${path}`, base), {
53
+ ...init, headers, signal, credentials: options.credentials ?? 'include', cache: 'no-store', redirect: 'error',
54
+ })
55
+ signal.throwIfAborted()
56
+ if (!response.ok) throw new Error(`Asset request failed (${response.status})`)
57
+ return await response.json() as T
58
+ }
59
+ return {
60
+ async publish(reference: Required<AssetReference>, signal = new AbortController().signal): Promise<AssetPublication> {
61
+ const result = await send<AssetPublication>(`${encodeURIComponent(reference.asset_id)}/publication`, {
62
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ published: true, version_id: reference.version_id, representations: ['website', 'social'] }),
64
+ }, signal)
65
+ if (result.asset_id !== reference.asset_id || result.version_id !== reference.version_id || !result.published) {
66
+ throw new Error('Mismatched asset publication')
67
+ }
68
+ for (const purpose of ['website', 'social'] as const) {
69
+ const item = result.representations.find(item => item.representation === purpose)
70
+ if (!item) throw new Error('Incomplete asset publication')
71
+ const url = new URL(item.url, base)
72
+ const suffix = `/assets/${encodeURIComponent(reference.asset_id)}/public/${encodeURIComponent(reference.version_id)}/${purpose}`
73
+ if (!trusted.has(url.origin) || !['http:', 'https:'].includes(url.protocol)
74
+ || url.username || url.password || url.search || url.hash || !url.pathname.endsWith(suffix)) {
75
+ throw new Error('Invalid public asset URL')
76
+ }
77
+ item.url = url.href
78
+ }
79
+ return result
80
+ },
81
+ async upload(file, signal) {
82
+ const started = await send<UploadResponse>('uploads', {
83
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: file.name }),
84
+ }, signal)
85
+ if (!started.upload_id || !started.asset_id || !started.version_id) throw new Error('Invalid upload session')
86
+ const completed = await send<UploadResponse>(`uploads/${encodeURIComponent(started.upload_id)}`, {
87
+ method: 'PUT', headers: { 'Content-Type': 'application/octet-stream' }, body: file,
88
+ }, signal)
89
+ if (completed.status !== 'ready' || completed.asset_id !== started.asset_id
90
+ || completed.version_id !== started.version_id || completed.upload_id !== started.upload_id) {
91
+ throw new Error('Asset upload is not ready')
92
+ }
93
+ return { asset_id: completed.asset_id, version_id: completed.version_id }
94
+ },
95
+ async resolve(references, signal) {
96
+ const result: AssetDelivery[] = []
97
+ for (let offset = 0; offset < references.length; offset += 50) {
98
+ const chunk = references.slice(offset, offset + 50)
99
+ const response = await send<{ items: DeliveryResponse[] }>('resolve', {
100
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ references: chunk }),
101
+ }, signal)
102
+ if (!Array.isArray(response.items) || response.items.length !== chunk.length) throw new Error('Incomplete delivery response')
103
+ for (const reference of chunk) {
104
+ const item = response.items.find(candidate => candidate.asset_id === reference.asset_id
105
+ && (!reference.version_id || candidate.version_id === reference.version_id)
106
+ && candidate.representation === (reference.representation ?? 'original'))
107
+ if (!item) throw new Error('Mismatched delivery response')
108
+ const url = new URL(item.url, base)
109
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password
110
+ || !trusted.has(url.origin)) throw new Error('Untrusted asset delivery URL')
111
+ result.push({ url: url.href, version_id: item.version_id, expires_at: item.expires_at })
112
+ }
113
+ }
114
+ return result
115
+ },
116
+ }
117
+ }
118
+
119
+ /** Public URLs are deliberately separate from expiring private deliveries. */
120
+ export interface AssetPublication {
121
+ asset_id: string
122
+ name: string
123
+ version_id: string
124
+ published: boolean
125
+ representations: { representation: AssetPurpose; url: string; media_type: string; size: number }[]
126
+ }
127
+
128
+ export interface ConfigureAssetClientOptions {
129
+ transport: ApiTransport
130
+ /** Existing reactive identity/session + tenant key. Never inferred from auth globals. */
131
+ sessionKey: MaybeRefOrGetter<string | undefined>
132
+ deliveryOrigins?: string[]
133
+ }
134
+
135
+ /** Vue plugin; owns its session watcher and clears on app teardown. */
136
+ export function configureAssetClient(options: ConfigureAssetClientOptions) {
137
+ const baseURL = options.transport.axios.defaults.baseURL
138
+ if (!baseURL) throw new Error('Asset transport needs an API baseURL')
139
+ const transport = createAssetHttpTransport({
140
+ baseURL: new URL(baseURL, typeof location === 'undefined' ? undefined : location.origin).href,
141
+ apiTransport: options.transport, deliveryOrigins: options.deliveryOrigins,
142
+ })
143
+ const client = createAssetClient(transport)
144
+ const publications = new Set<AbortController>()
145
+ const clearSession = () => {
146
+ client.clearSession()
147
+ for (const controller of publications) controller.abort()
148
+ publications.clear()
149
+ }
150
+ return {
151
+ client,
152
+ async publish(reference: Required<AssetReference>) {
153
+ const controller = new AbortController()
154
+ publications.add(controller)
155
+ try { return await transport.publish(reference, controller.signal) }
156
+ finally { publications.delete(controller) }
157
+ },
158
+ install(app: App) {
159
+ app.provide(ASSET_CLIENT_KEY, client)
160
+ const stop = watch(() => toValue(options.sessionKey), clearSession, { flush: 'sync' })
161
+ app.onUnmount(() => { stop(); clearSession() })
162
+ },
163
+ }
164
+ }
@@ -0,0 +1,123 @@
1
+ // @vitest-environment happy-dom
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { createAssetClient, ASSET_CLIENT_KEY, useAssetDelivery, type AssetDelivery, type AssetTransport } from './assets'
4
+ import { mount } from '@vue/test-utils'
5
+ import { defineComponent, nextTick } from 'vue'
6
+
7
+ const delivery = (version_id = 'v1'): AssetDelivery => ({
8
+ url: 'https://delivery.example/private',
9
+ expires_at: new Date(Date.now() + 60_000).toISOString(),
10
+ version_id,
11
+ })
12
+
13
+ function setup() {
14
+ const resolve = vi.fn<AssetTransport['resolve']>(async references => references.map(() => delivery()))
15
+ const upload = vi.fn<AssetTransport['upload']>(async () => ({ asset_id: 'a', version_id: 'v1' }))
16
+ return { resolve, upload, client: createAssetClient({ resolve, upload }) }
17
+ }
18
+
19
+ describe('asset client', () => {
20
+ it('keeps the displayed URL while refreshing, but clears immediately on session change', async () => {
21
+ vi.useFakeTimers()
22
+ const { client, resolve } = setup()
23
+ let finish!: (value: AssetDelivery[]) => void
24
+ let state!: ReturnType<typeof useAssetDelivery>
25
+ const component = mount(defineComponent({
26
+ setup() { state = useAssetDelivery(() => ({ asset_id: 'a' })); return () => null },
27
+ }), { global: { provide: { [ASSET_CLIENT_KEY as symbol]: client } } })
28
+ try {
29
+ await vi.advanceTimersByTimeAsync(0)
30
+ expect(state.url.value).toBe(delivery().url)
31
+ resolve.mockImplementationOnce(() => new Promise(done => { finish = done }))
32
+ await vi.advanceTimersByTimeAsync(48_000)
33
+ expect(resolve).toHaveBeenCalledTimes(2)
34
+ expect(state.url.value).toBe(delivery().url)
35
+ client.clearSession()
36
+ expect(state.url.value).toBeUndefined()
37
+ finish([delivery()])
38
+ await nextTick()
39
+ expect(state.url.value).toBeUndefined()
40
+ } finally {
41
+ component.unmount()
42
+ vi.useRealTimers()
43
+ }
44
+ })
45
+
46
+ it('deduplicates in-flight requests without sharing subscriber cancellation', async () => {
47
+ const { client, resolve } = setup()
48
+ let finish!: (value: AssetDelivery[]) => void
49
+ resolve.mockImplementationOnce(() => new Promise(done => { finish = done }))
50
+ const controller = new AbortController()
51
+ const first = client.resolve({ asset_id: 'a' }, controller.signal)
52
+ const rejected = expect(first).rejects.toMatchObject({ name: 'AbortError' })
53
+ await Promise.resolve()
54
+ const second = client.resolve({ asset_id: 'a', representation: 'original' })
55
+ controller.abort()
56
+ await rejected
57
+ expect(resolve.mock.calls[0]![1].aborted).toBe(false)
58
+ finish([delivery()])
59
+ await expect(second).resolves.toMatchObject({ version_id: 'v1' })
60
+ expect(resolve).toHaveBeenCalledTimes(1)
61
+ })
62
+
63
+ it('aborts unused requests and starts fresh after a session change', async () => {
64
+ const { client, resolve } = setup()
65
+ resolve.mockImplementationOnce(() => new Promise(() => {}))
66
+ const first = client.resolve({ asset_id: 'a' })
67
+ const rejected = expect(first).rejects.toMatchObject({ name: 'AbortError' })
68
+ await Promise.resolve()
69
+ client.clearSession()
70
+ await rejected
71
+ expect(resolve.mock.calls[0]![1].aborted).toBe(true)
72
+ await expect(client.resolve({ asset_id: 'a' })).resolves.toMatchObject({ version_id: 'v1' })
73
+ expect(resolve).toHaveBeenCalledTimes(2)
74
+ })
75
+
76
+ it('batches same-turn resolutions and preserves pins', async () => {
77
+ const { client, resolve } = setup()
78
+ await Promise.all([client.resolve({ asset_id: 'a', version_id: 'v1' }), client.resolve({ asset_id: 'b' })])
79
+ expect(resolve).toHaveBeenCalledTimes(1)
80
+ expect(resolve.mock.calls[0]![0][0]).toEqual({ asset_id: 'a', version_id: 'v1' })
81
+ })
82
+
83
+ it('does not cache grants between calls', async () => {
84
+ const { client, resolve } = setup()
85
+ await client.resolve({ asset_id: 'a' })
86
+ await client.resolve({ asset_id: 'a' })
87
+ expect(resolve).toHaveBeenCalledTimes(2)
88
+ })
89
+
90
+ it('rejects expired grants and mismatched pinned versions', async () => {
91
+ const { client, resolve } = setup()
92
+ await expect(client.resolve({ asset_id: 'a', version_id: 'v2' })).rejects.toThrow('Invalid asset delivery')
93
+ resolve.mockResolvedValueOnce([{ ...delivery(), expires_at: '2000-01-01T00:00:00Z' }])
94
+ await expect(client.resolve({ asset_id: 'a' })).rejects.toThrow('Invalid asset delivery')
95
+ })
96
+
97
+ it('cancels queued requests on session change', async () => {
98
+ const { client, resolve } = setup()
99
+ const request = client.resolve({ asset_id: 'a' })
100
+ client.clearSession()
101
+ await expect(request).rejects.toMatchObject({ name: 'AbortError' })
102
+ expect(resolve).not.toHaveBeenCalled()
103
+ })
104
+
105
+ it('rejects stale in-flight results even if transport ignores abort', async () => {
106
+ const { client, resolve } = setup()
107
+ let finish!: (value: AssetDelivery[]) => void
108
+ resolve.mockImplementationOnce(() => new Promise(done => { finish = done }))
109
+ const request = client.resolve({ asset_id: 'a' })
110
+ await Promise.resolve()
111
+ client.clearSession()
112
+ finish([delivery()])
113
+ await expect(request).rejects.toMatchObject({ name: 'AbortError' })
114
+ })
115
+
116
+ it('does not dispatch already-cancelled requests', async () => {
117
+ const { client, resolve } = setup()
118
+ const controller = new AbortController()
119
+ controller.abort()
120
+ await expect(client.resolve({ asset_id: 'a' }, controller.signal)).rejects.toMatchObject({ name: 'AbortError' })
121
+ expect(resolve).not.toHaveBeenCalled()
122
+ })
123
+ })