Compare commits

...

12 Commits

Author SHA1 Message Date
CJPeckover 6e0005acfd - add toggle to show/hide asset owner names 2025-08-25 19:55:25 -04:00
CJPeckover 55a196bfa0 Merge branch 'immich-main' into feat/shared-album-owner-labels 2025-08-25 18:27:51 -04:00
Jason Rasmussen d04675fb41 fix: dev-scripts (#21270)
fix/dev-scripts
2025-08-25 17:10:59 -04:00
Jason Rasmussen acfd40b77a fix: album start/end dates on shared links (#21268) 2025-08-25 17:10:31 -04:00
Jason Rasmussen 840e43430c fix: docs typo (#21269) 2025-08-25 22:43:37 +02:00
Jason Rasmussen a3e0c6cef5 fix: ignore abort errors (#21262) 2025-08-25 16:42:30 -04:00
CJPeckover d0b49846dc format 2025-08-23 01:12:37 -04:00
CJPeckover 8896b2dbf5 fix lint 2025-08-23 01:11:10 -04:00
CJPeckover 251e644b2a - cleanup albumUsers creation
- use font-light for the user's name
2025-08-23 01:09:51 -04:00
CJPeckover a02635f9a5 cleanup 2025-08-23 00:57:50 -04:00
CJPeckover 104f3dfcc3 - change owner to their name in white text instead of the avatar 2025-08-23 00:54:50 -04:00
CJPeckover b7e3b48a44 - pass available album users along to the thumbnail through the asset-date-group
- show a small user-avatar in bottom right of thumbnail
2025-08-22 17:29:03 -04:00
13 changed files with 172 additions and 35 deletions
@@ -12,8 +12,7 @@ services:
- ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload - ${UPLOAD_LOCATION:-upload2-devcontainer-volume}${UPLOAD_LOCATION:+/photos/upload}:/data/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm-store:/usr/src/app/.pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node-modules:/usr/src/app/server/node_modules - server-node_modules:/usr/src/app/server/node_modules
- server-dist:/usr/src/app/server/dist
- web-node_modules:/usr/src/app/web/node_modules - web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules - github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules - cli-node_modules:/usr/src/app/cli/node_modules
+4 -8
View File
@@ -33,8 +33,7 @@ services:
- ${UPLOAD_LOCATION}/photos/upload:/data/upload - ${UPLOAD_LOCATION}/photos/upload:/data/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- pnpm-store:/usr/src/app/.pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node-modules:/usr/src/app/server/node_modules - server-node_modules:/usr/src/app/server/node_modules
- server-dist:/usr/src/app/server/dist
- web-node_modules:/usr/src/app/web/node_modules - web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules - github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules - cli-node_modules:/usr/src/app/cli/node_modules
@@ -97,8 +96,7 @@ services:
volumes: volumes:
- ..:/usr/src/app - ..:/usr/src/app
- pnpm-store:/usr/src/app/.pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node-modules:/usr/src/app/server/node_modules - server-node_modules:/usr/src/app/server/node_modules
- server-dist:/usr/src/app/server/dist
- web-node_modules:/usr/src/app/web/node_modules - web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules - github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules - cli-node_modules:/usr/src/app/cli/node_modules
@@ -194,8 +192,7 @@ services:
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
volumes: volumes:
- pnpm-store:/usr/src/app/.pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node-modules:/usr/src/app/server/node_modules - server-node_modules:/usr/src/app/server/node_modules
- server-dist:/usr/src/app/server/dist
- web-node_modules:/usr/src/app/web/node_modules - web-node_modules:/usr/src/app/web/node_modules
- github-node_modules:/usr/src/app/.github/node_modules - github-node_modules:/usr/src/app/.github/node_modules
- cli-node_modules:/usr/src/app/cli/node_modules - cli-node_modules:/usr/src/app/cli/node_modules
@@ -210,8 +207,7 @@ volumes:
prometheus-data: prometheus-data:
grafana-data: grafana-data:
pnpm-store: pnpm-store:
server-node-modules: server-node_modules:
server-dist:
web-node_modules: web-node_modules:
github-node_modules: github-node_modules:
cli-node_modules: cli-node_modules:
+1 -1
View File
@@ -88,7 +88,7 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
## Auto Launch ## Auto Launch
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`. When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich. Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?autoLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
## Mobile Redirect URI ## Mobile Redirect URI
@@ -38,7 +38,11 @@ from
select select
"album".*, "album".*,
coalesce( coalesce(
json_agg("assets") filter ( json_agg(
"assets"
order by
"assets"."fileCreatedAt" asc
) filter (
where where
"assets"."id" is not null "assets"."id" is not null
), ),
@@ -86,7 +86,16 @@ export class SharedLinkRepository {
(join) => join.onTrue(), (join) => join.onTrue(),
) )
.select((eb) => .select((eb) =>
eb.fn.coalesce(eb.fn.jsonAgg('assets').filterWhere('assets.id', 'is not', null), sql`'[]'`).as('assets'), eb.fn
.coalesce(
eb.fn
.jsonAgg('assets')
.orderBy('assets.fileCreatedAt', 'asc')
.filterWhere('assets.id', 'is not', null),
sql`'[]'`,
)
.as('assets'),
) )
.select((eb) => eb.fn.toJson('owner').as('owner')) .select((eb) => eb.fn.toJson('owner').as('owner'))
.groupBy(['album.id', sql`"owner".*`]) .groupBy(['album.id', sql`"owner".*`])
+3 -1
View File
@@ -33,6 +33,7 @@ import { PartnerRepository } from 'src/repositories/partner.repository';
import { PersonRepository } from 'src/repositories/person.repository'; import { PersonRepository } from 'src/repositories/person.repository';
import { SearchRepository } from 'src/repositories/search.repository'; import { SearchRepository } from 'src/repositories/search.repository';
import { SessionRepository } from 'src/repositories/session.repository'; import { SessionRepository } from 'src/repositories/session.repository';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
import { StackRepository } from 'src/repositories/stack.repository'; import { StackRepository } from 'src/repositories/stack.repository';
import { StorageRepository } from 'src/repositories/storage.repository'; import { StorageRepository } from 'src/repositories/storage.repository';
import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository'; import { SyncCheckpointRepository } from 'src/repositories/sync-checkpoint.repository';
@@ -286,6 +287,7 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
case PersonRepository: case PersonRepository:
case SearchRepository: case SearchRepository:
case SessionRepository: case SessionRepository:
case SharedLinkRepository:
case StackRepository: case StackRepository:
case SyncRepository: case SyncRepository:
case SyncCheckpointRepository: case SyncCheckpointRepository:
@@ -391,7 +393,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
checksum: randomBytes(32), checksum: randomBytes(32),
type: AssetType.Image, type: AssetType.Image,
originalPath: '/path/to/something.jpg', originalPath: '/path/to/something.jpg',
ownerId: '@immich.cloud', ownerId: 'not-a-valid-uuid',
isFavorite: false, isFavorite: false,
fileCreatedAt: now, fileCreatedAt: now,
fileModifiedAt: now, fileModifiedAt: now,
@@ -0,0 +1,65 @@
import { Kysely } from 'kysely';
import { randomBytes } from 'node:crypto';
import { SharedLinkType } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
import { DB } from 'src/schema';
import { SharedLinkService } from 'src/services/shared-link.service';
import { newMediumService } from 'test/medium.factory';
import { factory } from 'test/small.factory';
import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>;
const setup = (db?: Kysely<DB>) => {
return newMediumService(SharedLinkService, {
database: db || defaultDatabase,
real: [AccessRepository, DatabaseRepository, SharedLinkRepository],
mock: [LoggingRepository, StorageRepository],
});
};
beforeAll(async () => {
defaultDatabase = await getKyselyDB();
});
describe(SharedLinkService.name, () => {
describe('get', () => {
it('should return the correct dates on the shared link album', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const auth = factory.auth({ user });
const { album } = await ctx.newAlbum({ ownerId: user.id });
const dates = ['2021-01-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z', '2020-01-01T00:00:00.000Z'];
for (const date of dates) {
const { asset } = await ctx.newAsset({ fileCreatedAt: date, localDateTime: date, ownerId: user.id });
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
}
const sharedLinkRepo = ctx.get(SharedLinkRepository);
const sharedLink = await sharedLinkRepo.create({
key: randomBytes(16),
id: factory.uuid(),
userId: user.id,
albumId: album.id,
allowUpload: true,
type: SharedLinkType.Album,
});
await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({
album: expect.objectContaining({
startDate: '2020-01-01T00:00:00+00:00',
endDate: '2022-01-01T00:00:00+00:00',
}),
});
});
});
});
@@ -5,7 +5,7 @@
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { timeToSeconds } from '$lib/utils/date-time'; import { timeToSeconds } from '$lib/utils/date-time';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, AssetVisibility } from '@immich/sdk'; import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
import { import {
mdiArchiveArrowDownOutline, mdiArchiveArrowDownOutline,
mdiCameraBurst, mdiCameraBurst,
@@ -45,6 +45,7 @@
imageClass?: ClassValue; imageClass?: ClassValue;
brokenAssetClass?: ClassValue; brokenAssetClass?: ClassValue;
dimmed?: boolean; dimmed?: boolean;
albumUsers?: UserResponseDto[];
onClick?: (asset: TimelineAsset) => void; onClick?: (asset: TimelineAsset) => void;
onSelect?: (asset: TimelineAsset) => void; onSelect?: (asset: TimelineAsset) => void;
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void; onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
@@ -63,6 +64,7 @@
readonly = false, readonly = false,
showArchiveIcon = false, showArchiveIcon = false,
showStackedIcon = true, showStackedIcon = true,
albumUsers = [],
onClick = undefined, onClick = undefined,
onSelect = undefined, onSelect = undefined,
onMouseEvent = undefined, onMouseEvent = undefined,
@@ -84,6 +86,8 @@
let width = $derived(thumbnailSize || thumbnailWidth || 235); let width = $derived(thumbnailSize || thumbnailWidth || 235);
let height = $derived(thumbnailSize || thumbnailHeight || 235); let height = $derived(thumbnailSize || thumbnailHeight || 235);
let assetOwner = $derived(albumUsers?.find((user) => user.id === asset.ownerId) ?? null);
const onIconClickedHandler = (e?: MouseEvent) => { const onIconClickedHandler = (e?: MouseEvent) => {
e?.stopPropagation(); e?.stopPropagation();
e?.preventDefault(); e?.preventDefault();
@@ -267,6 +271,12 @@
</div> </div>
{/if} {/if}
{#if !!assetOwner}
<div class="absolute bottom-0 end-1">
<span class="text-white font-light text-sm">{assetOwner.name}</span>
</div>
{/if}
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive} {#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}> <div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
<Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" /> <Icon path={mdiArchiveArrowDownOutline} size="24" class="text-white" />
@@ -9,6 +9,7 @@
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { uploadAssetsStore } from '$lib/stores/upload'; import { uploadAssetsStore } from '$lib/stores/upload';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import type { UserResponseDto } from '@immich/sdk';
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js'; import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
@@ -25,7 +26,7 @@
monthGroup: MonthGroup; monthGroup: MonthGroup;
timelineManager: TimelineManager; timelineManager: TimelineManager;
assetInteraction: AssetInteraction; assetInteraction: AssetInteraction;
albumUsers?: UserResponseDto[];
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void; onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
onSelectAssets: (asset: TimelineAsset) => void; onSelectAssets: (asset: TimelineAsset) => void;
onSelectAssetCandidates: (asset: TimelineAsset | null) => void; onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
@@ -40,6 +41,7 @@
monthGroup = $bindable(), monthGroup = $bindable(),
assetInteraction, assetInteraction,
timelineManager, timelineManager,
albumUsers = [],
onSelect, onSelect,
onSelectAssets, onSelectAssets,
onSelectAssetCandidates, onSelectAssetCandidates,
@@ -189,6 +191,7 @@
showStackedIcon={withStacked} showStackedIcon={withStacked}
{showArchiveIcon} {showArchiveIcon}
{asset} {asset}
{albumUsers}
{groupIndex} {groupIndex}
onClick={(asset) => onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset)} onClick={(asset) => onClick(timelineManager, dayGroup.getAssets(), dayGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)} onSelect={(asset) => assetSelectHandler(timelineManager, asset, dayGroup.getAssets(), dayGroup.groupTitle)}
@@ -30,7 +30,13 @@
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util'; import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; import {
AssetVisibility,
getAssetInfo,
type AlbumResponseDto,
type PersonResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { modalManager } from '@immich/ui'; import { modalManager } from '@immich/ui';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
@@ -59,6 +65,7 @@
showArchiveIcon?: boolean; showArchiveIcon?: boolean;
isShared?: boolean; isShared?: boolean;
album?: AlbumResponseDto | null; album?: AlbumResponseDto | null;
albumUsers?: UserResponseDto[];
person?: PersonResponseDto | null; person?: PersonResponseDto | null;
isShowDeleteConfirmation?: boolean; isShowDeleteConfirmation?: boolean;
onSelect?: (asset: TimelineAsset) => void; onSelect?: (asset: TimelineAsset) => void;
@@ -78,6 +85,7 @@
showArchiveIcon = false, showArchiveIcon = false,
isShared = false, isShared = false,
album = null, album = null,
albumUsers = [],
person = null, person = null,
isShowDeleteConfirmation = $bindable(false), isShowDeleteConfirmation = $bindable(false),
onSelect = () => {}, onSelect = () => {},
@@ -88,6 +96,12 @@
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget, mutex } = assetViewingStore; let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget, mutex } = assetViewingStore;
// const albumUsers = $derived(
// album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor)
// ? [album.owner, ...album.albumUsers.map(({ user }) => user)]
// : [],
// );
let element: HTMLElement | undefined = $state(); let element: HTMLElement | undefined = $state();
let timelineElement: HTMLElement | undefined = $state(); let timelineElement: HTMLElement | undefined = $state();
@@ -936,6 +950,7 @@
{isSelectionMode} {isSelectionMode}
{singleSelect} {singleSelect}
{monthGroup} {monthGroup}
{albumUsers}
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)} onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
onSelectAssetCandidates={handleSelectAssetCandidates} onSelectAssetCandidates={handleSelectAssetCandidates}
onSelectAssets={handleSelectAssets} onSelectAssets={handleSelectAssets}
@@ -71,6 +71,7 @@
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, IconButton, modalManager } from '@immich/ui'; import { Button, IconButton, modalManager } from '@immich/ui';
import { import {
mdiAccountEyeOutline,
mdiArrowLeft, mdiArrowLeft,
mdiCogOutline, mdiCogOutline,
mdiDeleteOutline, mdiDeleteOutline,
@@ -104,6 +105,7 @@
let isCreatingSharedAlbum = $state(false); let isCreatingSharedAlbum = $state(false);
let isShowActivity = $state(false); let isShowActivity = $state(false);
let albumOrder: AssetOrder | undefined = $state(data.album.order); let albumOrder: AssetOrder | undefined = $state(data.album.order);
let showAlbumUsers = $state(false);
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
const timelineInteraction = new AssetInteraction(); const timelineInteraction = new AssetInteraction();
@@ -321,6 +323,11 @@
let album = $derived(data.album); let album = $derived(data.album);
let albumId = $derived(album.id); let albumId = $derived(album.id);
const albumUsers = $derived(
showAlbumUsers && album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor)
? [album.owner, ...album.albumUsers.map(({ user }) => user)]
: [],
);
$effect(() => { $effect(() => {
if (!album.isActivityEnabled && activityManager.commentCount === 0) { if (!album.isActivityEnabled && activityManager.commentCount === 0) {
@@ -445,6 +452,7 @@
<AssetGrid <AssetGrid
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true} enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
{album} {album}
{albumUsers}
{timelineManager} {timelineManager}
assetInteraction={currentAssetIntersection} assetInteraction={currentAssetIntersection}
{isShared} {isShared}
@@ -615,6 +623,15 @@
{#snippet trailing()} {#snippet trailing()}
<CastButton /> <CastButton />
<IconButton
variant="ghost"
shape="round"
color="secondary"
aria-label="view asset owners"
icon={mdiAccountEyeOutline}
onclick={() => (showAlbumUsers = !showAlbumUsers)}
/>
{#if isEditor} {#if isEditor}
<IconButton <IconButton
variant="ghost" variant="ghost"
+14 -7
View File
@@ -1,4 +1,4 @@
import { cancelRequest, handleRequest } from './request'; import { handleCancel, handlePreload } from './request';
export const installBroadcastChannelListener = () => { export const installBroadcastChannelListener = () => {
const broadcast = new BroadcastChannel('immich'); const broadcast = new BroadcastChannel('immich');
@@ -7,12 +7,19 @@ export const installBroadcastChannelListener = () => {
if (!event.data) { if (!event.data) {
return; return;
} }
const urlString = event.data.url;
const url = new URL(urlString, event.origin); const url = new URL(event.data.url, event.origin);
if (event.data.type === 'cancel') {
cancelRequest(url); switch (event.data.type) {
} else if (event.data.type === 'preload') { case 'preload': {
handleRequest(url); handlePreload(url);
break;
}
case 'cancel': {
handleCancel(url);
break;
}
} }
}; };
}; };
+21 -11
View File
@@ -1,5 +1,7 @@
import { get, put } from './cache'; import { get, put } from './cache';
const pendingRequests = new Map<string, AbortController>();
const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined; const isURL = (request: URL | RequestInfo): request is URL => (request as URL).href !== undefined;
const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined; const isRequest = (request: RequestInfo): request is Request => (request as Request).url !== undefined;
@@ -21,11 +23,16 @@ const getCacheKey = (request: URL | Request) => {
throw new Error(`Invalid request: ${request}`); throw new Error(`Invalid request: ${request}`);
}; };
const pendingRequests = new Map<string, AbortController>(); export const handlePreload = async (request: URL | Request) => {
try {
return await handleRequest(request);
} catch (error) {
console.error(`Preload failed: ${error}`);
}
};
export const handleRequest = async (request: URL | Request) => { export const handleRequest = async (request: URL | Request) => {
const cacheKey = getCacheKey(request); const cacheKey = getCacheKey(request);
const cachedResponse = await get(cacheKey); const cachedResponse = await get(cacheKey);
if (cachedResponse) { if (cachedResponse) {
return cachedResponse; return cachedResponse;
@@ -41,23 +48,26 @@ export const handleRequest = async (request: URL | Request) => {
return response; return response;
} catch (error) { } catch (error) {
console.log(error); if (error.name === 'AbortError') {
return new Response(undefined, { // dummy response avoids network errors in the console for these requests
status: 499, return new Response(undefined, { status: 204 });
statusText: `Request canceled: Instructions unclear, accidentally interrupted myself (${error})`, }
});
console.log('Not an abort error', error);
throw error;
} finally { } finally {
pendingRequests.delete(cacheKey); pendingRequests.delete(cacheKey);
} }
}; };
export const cancelRequest = (url: URL) => { export const handleCancel = (url: URL) => {
const cacheKey = getCacheKey(url); const cacheKey = getCacheKey(url);
const pending = pendingRequests.get(cacheKey); const pendingRequest = pendingRequests.get(cacheKey);
if (!pending) { if (!pendingRequest) {
return; return;
} }
pending.abort(); pendingRequest.abort();
pendingRequests.delete(cacheKey); pendingRequests.delete(cacheKey);
}; };