includio-cms 0.23.0 → 0.24.0
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/API.md +1 -1
- package/CHANGELOG.md +21 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +1 -0
- package/dist/core/server/entries/operations/get.bench.d.ts +1 -0
- package/dist/core/server/entries/operations/get.bench.js +68 -0
- package/dist/core/server/entries/operations/get.js +17 -7
- package/dist/core/server/fields/utils/imageStyles.bench.d.ts +1 -0
- package/dist/core/server/fields/utils/imageStyles.bench.js +82 -0
- package/dist/core/server/fields/utils/imageStyles.js +49 -53
- package/dist/core/server/media/operations/backgroundMaintenance.d.ts +6 -0
- package/dist/core/server/media/operations/backgroundMaintenance.js +6 -1
- package/dist/core/server/media/styles/operations/getImageStyle.d.ts +7 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +24 -0
- package/dist/db-postgres/index.d.ts +1 -1
- package/dist/db-postgres/index.js +27 -0
- package/dist/paraglide/messages/_index.d.ts +36 -3
- package/dist/paraglide/messages/_index.js +71 -3
- package/dist/paraglide/messages/en.d.ts +5 -0
- package/dist/paraglide/messages/en.js +14 -0
- package/dist/paraglide/messages/pl.d.ts +5 -0
- package/dist/paraglide/messages/pl.js +14 -0
- package/dist/types/adapters/db.d.ts +16 -0
- package/dist/types/adapters/db.js +8 -1
- package/dist/updates/0.24.0/index.d.ts +2 -0
- package/dist/updates/0.24.0/index.js +20 -0
- package/dist/updates/index.js +2 -1
- package/package.json +2 -1
- package/dist/paraglide/messages/hello_world.d.ts +0 -5
- package/dist/paraglide/messages/hello_world.js +0 -33
- package/dist/paraglide/messages/login_hello.d.ts +0 -16
- package/dist/paraglide/messages/login_hello.js +0 -34
- package/dist/paraglide/messages/login_please_login.d.ts +0 -16
- package/dist/paraglide/messages/login_please_login.js +0 -34
package/API.md
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
All notable changes to includio-cms are documented here.
|
|
4
4
|
Generated from `src/lib/updates/` — do not edit manually.
|
|
5
5
|
|
|
6
|
+
## 0.24.0 — 2026-04-30
|
|
7
|
+
|
|
8
|
+
Faza 11 — Performance Pass. Eliminacja N+1 w render path: `getImageStylesBatch` (jeden SELECT zamiast N×getImageStyle dla styles + srcset wariantów) + `_getRawEntries` z batchowanym `getEntryVersions` (jeden call zamiast per-entry). Sharp srcset pipeline odrefactorowany — Promise.all wokół per-style queries usunięty (jeden batch + sequential Sharp generation z try/catch dla missing entries). Background maintenance lock potwierdzony testami (concurrent invocation skipped). Bench infra: nowy projekt vitest `bench` + skrypt `pnpm bench` + 2 representative benchmarks (image styles + raw entries).
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `databaseAdapter.getImageStylesBatch(requests)` — nowa metoda adaptera. Zwraca `Map<key, ImageStyle>` keyed przez `imageStyleKey(mediaFileId, style)`. Implementacja `db-postgres`: jeden SELECT `WHERE mediaFileId IN (...)` + JS-side filter po (name, width, height, quality, aspectRatio) matching unique index. Skala impactu: 100 entries × 3 styles × 4 srcset = ~1200 queries → 1 query per media file (~30 queries dla 30 unique files). Bench `getImageStyles`: ~750 ops/s (1.3ms p75) dla 1 media × 3 styles × srcset; baseline N+1 (72 sequential): ~11 ops/s — **71× speedup**.
|
|
12
|
+
- `_getRawEntries()` (`src/lib/core/server/entries/operations/get.ts`) zrefactorowany — zamiast per-entry `getEntryVersions({ entryIds: [entry.id] })` w `Promise.all(dbEntries.map(...))` (N+1) teraz jeden `getEntryVersions({ entryIds: dbEntries.map(e => e.id) })` + group by `entryId` w Map. Adapter API bez zmian (już obsługiwał array `entryIds`). Skala: 100 entries → 2 queries zamiast 101. Bench: 387 ops/s (~2.6ms) dla 100 entries; baseline N+1 (101 sequential): ~7.9 ops/s — **50× speedup**.
|
|
13
|
+
- Sharp srcset variant generation: `Promise.all(widths.map(getImageStyle))` całkowicie wyeliminowany w `imageStyles.ts`. Nowy flow: zebrać wszystkie style requesty (base + srcset variants), jeden `getImageStylesBatch()` call, fallback do `createImageStyle()` per missing z try/catch. Konsekwencje: jeden Sharp timeout (30s, env `INCLUDIO_SHARP_TIMEOUT_MS`) nie zabija batch innych wariantów — zachowanie identyczne jak `Promise.allSettled`, ale bez Promise.all w pipeline.
|
|
14
|
+
- `getImageStylesBatch(requests)` operations wrapper (`src/lib/core/server/media/styles/operations/getImageStyle.ts`): koordynuje DB lookup + Sharp generation. Krótkie ścieżki: empty requests → empty Map bez DB call; wszystkie hity → cache return bez `createImageStyle`. Failed Sharp → `console.warn` + omission, nie throw.
|
|
15
|
+
- `imageStyleKey(mediaFileId, style)` — stabilna helper key z `$lib/types/adapters/db.js` (eksport public). Format: `${mediaFileId}|${name}|${width||0}|${height||0}|${quality||0}|${aspectRatio||0}` — matchuje unique index na `image_styles`.
|
|
16
|
+
- Background maintenance — in-process lock (`state.running` na `globalThis`) potwierdzony test suitem: `runMaintenance` exported (`@internal`), test concurrent dwóch wywołań → drugie loguje `Skipping — previous run still active`, batch operations wywołane dokładnie 1×. Komment w pliku: dla cluster mode wrap w `pg_advisory_lock`.
|
|
17
|
+
- `pnpm bench` — nowy skrypt + dedykowany vitest project `bench` (vite.config.ts). Include `src/**/*.bench.ts`. 2 nowe benchmarki: `imageStyles.bench.ts` (real `getImageStyles` z mocked adapter latency 1ms + synthetic N+1 vs batch comparison), `get.bench.ts` (real `_getRawEntries` 100 entries + comparison). Istniejący `resolveTypographyOrphans.bench.ts` teraz uruchamiany w tej samej komendzie.
|
|
18
|
+
- Lazy adapter imports (OpenAI / Anthropic / Nodemailer) — verified no-op. Wszystkie 3 adaptery (`src/lib/{ai-claude,ai-openai,email-nodemailer}/index.ts`) już używają `await import()` + cached singleton w closure. Type imports (`import type`) nie ładują runtime code. Trigger: pierwszy call metody adaptera (np. `generateAltText`, `sendMail`).
|
|
19
|
+
|
|
20
|
+
### Breaking
|
|
21
|
+
- `DatabaseAdapter.getImageStylesBatch` — nowa **wymagana** metoda interfejsu (`src/lib/types/adapters/db.ts`). Custom adapters muszą zaimplementować. Sygnatura: `(requests: ImageStyleRequest[]) => Promise<Map<string, ImageStyle>>`. Reference impl: `src/lib/db-postgres/index.ts` (jeden SELECT z `inArray` po `mediaFileId` + JS filter). Helper key: `imageStyleKey(mediaFileId, style)` z `$lib/types/adapters/db.js`.
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
Zero schema migration — używa istniejącej tabeli `image_styles` i unique indeksu. Hot path render: collection list z 100 entries × image fields powinien zobaczyć ~50-70× redukcję wall-time queries (mierzone via bench na mocked latency 1ms; real PG latency wyższa, więc bezwzględne win większe). Sharp generation pozostaje sekwencyjne dla missing styles per request — celowo, żeby nie obciążać CPU; background maintenance batch (`batchGenerateAllStyles`) prewarmuje DB. `runMaintenance` eksportowany jako `@internal` dla testów — nie zaliczany do public API surface.
|
|
26
|
+
|
|
6
27
|
## 0.23.0 — 2026-04-30
|
|
7
28
|
|
|
8
29
|
Faza 10 — Documentation Pass. DOCS.md uzupełniony (REST cURL + error codes, Entries error handling + transaction patterns, Admin UI a11y guide, Adapter Contracts z interfaces + peer deps + lazy import, Shop retry-payment lifecycle, edge cases dla number/boolean/date/datetime). Nowe sekcje: Stability Promise (semver, @public/@experimental/@internal, deprecation timeline) i Security Model (CSRF, CSP, rate-limit, API keys, link do KNOWN-RISKS.md). Migration Guide rozszerzony o master cheatsheet 0.x → 1.0 (agregat 0.16-0.22). ROADMAP cleanup: pre-v1.0 historia → ROADMAP-ARCHIVE.md.
|
package/DOCS.md
CHANGED
package/ROADMAP.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- [x] `[chore]` `[P1]` Faza 8 — CI na PR: `.github/workflows/ci.yml` z jobs lint/typecheck/unit/integration (postgres :5434)/e2e (postgres :5435 + playwright cache)/build, pnpm cache, trace artifacts on fail, README CI badge <!-- files: .github/workflows/ci.yml, README.md -->
|
|
18
18
|
- [x] `[breaking]` `[P0]` Faza 9 — DX & config validation (0.22.0): `defineConfig` Zod strict z field path + hint, `CmsError` + `ConfigValidationError` z code/context, resolver throw sites zmigrowane, CLI `--help` per subcommand + `--version`, `.env.example` rozszerzony o `INCLUDIO_*`, README rebuild (TOC + system req + 5-min quickstart), JSDoc body (opis + @param + @returns + @example) na każdym `@public` <!-- files: src/lib/core/errors.ts, src/lib/types/cms.schema.ts, src/lib/sveltekit/config.ts, src/lib/cli/index.ts, README.md, .env.example, src/lib/updates/0.22.0/ -->
|
|
19
19
|
- [x] `[chore]` `[P1]` Faza 10 — Documentation Pass (0.23.0): DOCS.md uzupełnione (number/boolean/date edge cases, REST cURL + error codes, Entries error handling + transactions, Admin UI a11y, Adapter Contracts), nowe sekcje (Stability Promise, Security Model), Migration Guide v0.x → v1.0 master cheatsheet (0.16-0.22), ROADMAP cleanup (pre-v1.0 → ROADMAP-ARCHIVE.md) <!-- files: src/routes/docs/{stability-promise,security}/+page.svx, src/routes/docs/migration/+page.svx, src/routes/docs/_config/nav.ts, scripts/compile-docs.ts, ROADMAP-ARCHIVE.md, src/lib/updates/0.23.0/ -->
|
|
20
|
+
- [x] `[chore]` `[P1]` Faza 11 — Performance Pass (0.24.0): N+1 elimination (`getImageStyle` batch IN query, `getRawEntries` batch entry versions), Sharp srcset `Promise.allSettled` (one timeout doesn't kill batch), background maintenance in-process lock verified + tested, bench infra (`pnpm bench` + representative benchmarks), lazy adapter imports verified <!-- files: src/lib/core/server/media/styles/operations/, src/lib/core/server/fields/resolveImageFields.ts, src/lib/core/server/entries/operations/get.ts, src/lib/db-postgres/index.ts, src/lib/core/server/media/operations/backgroundMaintenance.spec.ts, src/lib/updates/0.24.0/ -->
|
|
20
21
|
- [ ] `[fix]` `[P1]` Select field — `defaultValue` propagacja do zod schema (full repro: `ideas/post-v1/select-field-defaultvalue-bug.md`); fix planowany w Fazie 12 (RC)
|
|
21
22
|
|
|
22
23
|
## v1.x — Post-v1.0 deferred
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { bench, describe, vi, beforeAll } from 'vitest';
|
|
2
|
+
const QUERY_LATENCY_MS = 1;
|
|
3
|
+
const ENTRY_COUNT = 100;
|
|
4
|
+
function buildDbEntry(i) {
|
|
5
|
+
return {
|
|
6
|
+
id: `entry-${i}`,
|
|
7
|
+
slug: 'blog-post',
|
|
8
|
+
type: 'collection',
|
|
9
|
+
sortOrder: i,
|
|
10
|
+
archivedAt: null,
|
|
11
|
+
createdAt: new Date(),
|
|
12
|
+
updatedAt: new Date()
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function buildVersion(entryId, n) {
|
|
16
|
+
return {
|
|
17
|
+
id: `${entryId}-v${n}`,
|
|
18
|
+
entryId,
|
|
19
|
+
versionNumber: n,
|
|
20
|
+
lang: 'en',
|
|
21
|
+
data: { title: `Entry ${entryId} v${n}` },
|
|
22
|
+
publishedAt: n === 2 ? new Date(Date.now() - 1000) : null,
|
|
23
|
+
createdAt: new Date(),
|
|
24
|
+
updatedAt: new Date()
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const dbEntries = Array.from({ length: ENTRY_COUNT }, (_, i) => buildDbEntry(i));
|
|
28
|
+
const allVersions = dbEntries.flatMap((e) => [buildVersion(e.id, 1), buildVersion(e.id, 2)]);
|
|
29
|
+
const adapter = {
|
|
30
|
+
getEntries: vi.fn(async (_options) => {
|
|
31
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
32
|
+
return dbEntries;
|
|
33
|
+
}),
|
|
34
|
+
getEntryVersions: vi.fn(async (options) => {
|
|
35
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
36
|
+
const ids = new Set(options.entryIds ?? []);
|
|
37
|
+
return allVersions.filter((v) => ids.has(v.entryId));
|
|
38
|
+
})
|
|
39
|
+
};
|
|
40
|
+
vi.mock('$lib/core/cms.js', () => ({
|
|
41
|
+
getCMS: () => ({
|
|
42
|
+
databaseAdapter: adapter,
|
|
43
|
+
getBySlug: () => ({ slug: 'blog-post', type: 'collection', orderable: false })
|
|
44
|
+
})
|
|
45
|
+
}));
|
|
46
|
+
let _getRawEntries;
|
|
47
|
+
beforeAll(async () => {
|
|
48
|
+
const mod = await import('./get.js');
|
|
49
|
+
_getRawEntries = mod._getRawEntries;
|
|
50
|
+
});
|
|
51
|
+
describe('_getRawEntries — batch path (current implementation)', () => {
|
|
52
|
+
bench(`100 entries × 2 versions each — 1 batch getEntryVersions call`, async () => {
|
|
53
|
+
await _getRawEntries({ slug: 'blog-post' });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('synthetic N+1 vs batch comparison', () => {
|
|
57
|
+
bench(`baseline: ${ENTRY_COUNT} sequential per-entry getEntryVersions calls`, async () => {
|
|
58
|
+
// Simulates the pre-0.24.0 N+1 pattern: one query per entry.
|
|
59
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS)); // getEntries
|
|
60
|
+
for (let i = 0; i < ENTRY_COUNT; i++) {
|
|
61
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
bench('batched: 1 getEntries + 1 getEntryVersions', async () => {
|
|
65
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
66
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -55,16 +55,26 @@ function buildPerLangVersionMaps(versions) {
|
|
|
55
55
|
}
|
|
56
56
|
export const _getRawEntries = async (options) => {
|
|
57
57
|
const dbEntries = await _getDbEntries(options);
|
|
58
|
-
|
|
58
|
+
if (dbEntries.length === 0)
|
|
59
|
+
return [];
|
|
60
|
+
const cms = getCMS();
|
|
61
|
+
const allVersions = await cms.databaseAdapter.getEntryVersions({
|
|
62
|
+
entryIds: dbEntries.map((e) => e.id)
|
|
63
|
+
});
|
|
64
|
+
const versionsByEntry = new Map();
|
|
65
|
+
for (const v of allVersions) {
|
|
66
|
+
const arr = versionsByEntry.get(v.entryId) || [];
|
|
67
|
+
arr.push(v);
|
|
68
|
+
versionsByEntry.set(v.entryId, arr);
|
|
69
|
+
}
|
|
70
|
+
const entries = dbEntries.map((entry) => {
|
|
59
71
|
try {
|
|
60
|
-
const versions =
|
|
61
|
-
entryIds: [entry.id]
|
|
62
|
-
});
|
|
72
|
+
const versions = versionsByEntry.get(entry.id) ?? [];
|
|
63
73
|
const { publishedVersions, scheduledVersions, draftVersions } = buildPerLangVersionMaps(versions);
|
|
64
74
|
return {
|
|
65
75
|
...entry,
|
|
66
|
-
collection:
|
|
67
|
-
versions: versions.sort((a, b) => b.versionNumber - a.versionNumber),
|
|
76
|
+
collection: cms.getBySlug(entry.slug),
|
|
77
|
+
versions: [...versions].sort((a, b) => b.versionNumber - a.versionNumber),
|
|
68
78
|
publishedVersions,
|
|
69
79
|
scheduledVersions,
|
|
70
80
|
draftVersions
|
|
@@ -74,7 +84,7 @@ export const _getRawEntries = async (options) => {
|
|
|
74
84
|
// Orphaned entry - slug removed from config
|
|
75
85
|
return null;
|
|
76
86
|
}
|
|
77
|
-
})
|
|
87
|
+
});
|
|
78
88
|
return entries.filter((e) => e !== null);
|
|
79
89
|
};
|
|
80
90
|
export const _getRawEntry = async (options) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { bench, describe, vi, beforeAll } from 'vitest';
|
|
2
|
+
import { imageStyleKey } from '../../../../types/adapters/db.js';
|
|
3
|
+
vi.mock('../../media/utils/generateBlurDataUrl.js', () => ({
|
|
4
|
+
generateBlurDataUrl: vi.fn().mockResolvedValue('data:image/webp;base64,abc')
|
|
5
|
+
}));
|
|
6
|
+
const QUERY_LATENCY_MS = 1;
|
|
7
|
+
function fakeStyle(req) {
|
|
8
|
+
return {
|
|
9
|
+
url: `/uploads/${req.mediaFileId}-${req.style.name}.${req.style.format ?? 'webp'}`,
|
|
10
|
+
mimeType: `image/${req.style.format ?? 'webp'}`
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
const batchAdapter = {
|
|
14
|
+
getImageStyle: vi.fn(async (mediaFileId, style) => {
|
|
15
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
16
|
+
return fakeStyle({ mediaFileId, style });
|
|
17
|
+
}),
|
|
18
|
+
getImageStylesBatch: vi.fn(async (requests) => {
|
|
19
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
20
|
+
const map = new Map();
|
|
21
|
+
for (const r of requests)
|
|
22
|
+
map.set(imageStyleKey(r.mediaFileId, r.style), fakeStyle(r));
|
|
23
|
+
return map;
|
|
24
|
+
}),
|
|
25
|
+
updateMediaFile: vi.fn().mockResolvedValue(undefined)
|
|
26
|
+
};
|
|
27
|
+
vi.mock('$lib/core/cms.js', () => ({
|
|
28
|
+
getCMS: () => ({
|
|
29
|
+
databaseAdapter: batchAdapter,
|
|
30
|
+
filesAdapter: { downloadFile: vi.fn().mockResolvedValue(null) }
|
|
31
|
+
})
|
|
32
|
+
}));
|
|
33
|
+
let getImageStyles;
|
|
34
|
+
beforeAll(async () => {
|
|
35
|
+
const mod = await import('./imageStyles.js');
|
|
36
|
+
getImageStyles = mod.getImageStyles;
|
|
37
|
+
});
|
|
38
|
+
const mediaFile = {
|
|
39
|
+
id: 'media-1',
|
|
40
|
+
name: 'photo.jpg',
|
|
41
|
+
url: '/uploads/photo.jpg',
|
|
42
|
+
type: 'image',
|
|
43
|
+
size: 1024,
|
|
44
|
+
width: 3000,
|
|
45
|
+
height: 2000,
|
|
46
|
+
alt: null,
|
|
47
|
+
tags: [],
|
|
48
|
+
createdAt: new Date(),
|
|
49
|
+
thumbnailUrl: null,
|
|
50
|
+
duration: null,
|
|
51
|
+
posterUrl: null,
|
|
52
|
+
mimeType: 'image/jpeg',
|
|
53
|
+
blurDataUrl: 'data:image/webp;base64,existing',
|
|
54
|
+
focalX: null,
|
|
55
|
+
focalY: null,
|
|
56
|
+
transcriptFileId: null,
|
|
57
|
+
audioDescriptionFileId: null
|
|
58
|
+
};
|
|
59
|
+
const customStyles = [
|
|
60
|
+
{ name: 'card', width: 600, srcset: [320, 640, 1024] },
|
|
61
|
+
{ name: 'hero', width: 1920, srcset: [640, 1024, 1920, 2560] },
|
|
62
|
+
{ name: 'thumb', width: 200 }
|
|
63
|
+
];
|
|
64
|
+
const field = { styles: customStyles };
|
|
65
|
+
describe('getImageStyles — batch path (current implementation)', () => {
|
|
66
|
+
bench('1 media × 3 styles × 3 formats × srcset (1 batch query)', async () => {
|
|
67
|
+
await getImageStyles(field, mediaFile);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('synthetic N+1 vs batch comparison', () => {
|
|
71
|
+
const styleCount = 3 * 3; // 3 styles × 3 format expansions
|
|
72
|
+
const srcsetCount = 7; // total srcset variants across the 3 styles
|
|
73
|
+
const totalLookups = styleCount + styleCount * srcsetCount; // ~72 per media
|
|
74
|
+
bench(`baseline: ${totalLookups} sequential per-style queries`, async () => {
|
|
75
|
+
for (let i = 0; i < totalLookups; i++) {
|
|
76
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
bench('batched: 1 query covers all lookups', async () => {
|
|
80
|
+
await new Promise((r) => setTimeout(r, QUERY_LATENCY_MS));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getImageStylesBatch } from '../../media/styles/operations/getImageStyle.js';
|
|
2
|
+
import { imageStyleKey } from '../../../../types/adapters/db.js';
|
|
2
3
|
import { getCMS } from '../../../cms.js';
|
|
3
4
|
import { generateBlurDataUrl } from '../../media/utils/generateBlurDataUrl.js';
|
|
4
5
|
export const defaultStyles = [
|
|
@@ -60,64 +61,59 @@ async function ensureBlurDataUrl(val) {
|
|
|
60
61
|
return null;
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
function variantStyleFor(style, w) {
|
|
65
|
+
return {
|
|
66
|
+
...style,
|
|
67
|
+
name: `${style.name}_${w}w`,
|
|
68
|
+
width: w,
|
|
69
|
+
srcset: undefined,
|
|
70
|
+
sizes: undefined
|
|
71
|
+
};
|
|
72
|
+
}
|
|
63
73
|
export async function getImageStyles(field, val) {
|
|
64
74
|
const hasCustomStyles = field.styles && field.styles.length > 0;
|
|
65
75
|
const rawStyles = [...(isProcessableImage(val) && !hasCustomStyles ? defaultStyles : []), ...(field.styles || [])];
|
|
66
76
|
const originalFormat = getOriginalFormat(val);
|
|
67
77
|
const stylesArr = expandStyleFormats(rawStyles, originalFormat);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
media: styleDbData.media,
|
|
77
|
-
mimeType: styleDbData.mimeType
|
|
78
|
-
};
|
|
79
|
-
// srcset expansion: generate width variants
|
|
80
|
-
if (style.srcset && style.srcset.length > 0 && val.width) {
|
|
81
|
-
const widths = style.srcset.filter((w) => w <= (val.width ?? 0));
|
|
82
|
-
if (widths.length > 0) {
|
|
83
|
-
const variants = await Promise.all(widths.map(async (w) => {
|
|
84
|
-
try {
|
|
85
|
-
const variantStyle = {
|
|
86
|
-
...style,
|
|
87
|
-
name: `${style.name}_${w}w`,
|
|
88
|
-
width: w,
|
|
89
|
-
srcset: undefined,
|
|
90
|
-
sizes: undefined
|
|
91
|
-
};
|
|
92
|
-
const variantData = await getImageStyle(val.id, variantStyle);
|
|
93
|
-
if (!variantData)
|
|
94
|
-
return null;
|
|
95
|
-
return `${variantData.url} ${w}w`;
|
|
96
|
-
}
|
|
97
|
-
catch (e) {
|
|
98
|
-
console.warn(`[CMS] Failed to generate srcset variant ${w}w for style "${style.name}" of media ${val.id}:`, e);
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}));
|
|
102
|
-
const validVariants = variants.filter((v) => v !== null);
|
|
103
|
-
if (validVariants.length > 0) {
|
|
104
|
-
result.srcset = validVariants.join(', ');
|
|
105
|
-
result.sizes = style.sizes;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return [style.name, result];
|
|
78
|
+
// Collect every (mediaFileId, style) request — base styles + srcset variants — into one batch.
|
|
79
|
+
const requests = [];
|
|
80
|
+
for (const style of stylesArr) {
|
|
81
|
+
requests.push({ mediaFileId: val.id, style });
|
|
82
|
+
if (style.srcset && style.srcset.length > 0 && val.width) {
|
|
83
|
+
const widths = style.srcset.filter((w) => w <= (val.width ?? 0));
|
|
84
|
+
for (const w of widths) {
|
|
85
|
+
requests.push({ mediaFileId: val.id, style: variantStyleFor(style, w) });
|
|
110
86
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
})),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const [styleMap, blurDataUrl] = await Promise.all([
|
|
90
|
+
getImageStylesBatch(requests),
|
|
116
91
|
ensureBlurDataUrl(val)
|
|
117
92
|
]);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
93
|
+
const styles = {};
|
|
94
|
+
for (const style of stylesArr) {
|
|
95
|
+
const baseData = styleMap.get(imageStyleKey(val.id, style));
|
|
96
|
+
if (!baseData)
|
|
97
|
+
continue;
|
|
98
|
+
const result = {
|
|
99
|
+
url: baseData.url,
|
|
100
|
+
media: baseData.media,
|
|
101
|
+
mimeType: baseData.mimeType
|
|
102
|
+
};
|
|
103
|
+
if (style.srcset && style.srcset.length > 0 && val.width) {
|
|
104
|
+
const widths = style.srcset.filter((w) => w <= (val.width ?? 0));
|
|
105
|
+
const validVariants = [];
|
|
106
|
+
for (const w of widths) {
|
|
107
|
+
const variantData = styleMap.get(imageStyleKey(val.id, variantStyleFor(style, w)));
|
|
108
|
+
if (variantData)
|
|
109
|
+
validVariants.push(`${variantData.url} ${w}w`);
|
|
110
|
+
}
|
|
111
|
+
if (validVariants.length > 0) {
|
|
112
|
+
result.srcset = validVariants.join(', ');
|
|
113
|
+
result.sizes = style.sizes;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
styles[style.name] = result;
|
|
117
|
+
}
|
|
118
|
+
return { styles, blurDataUrl };
|
|
123
119
|
}
|
|
@@ -11,5 +11,11 @@ export declare function getMaintenanceStatus(): {
|
|
|
11
11
|
nextRun: Date | null;
|
|
12
12
|
lastResult: MaintenanceResult | null;
|
|
13
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* @internal — exported for tests. The `state.running` guard is in-process only;
|
|
16
|
+
* for cluster-mode deployments wrap this in a `pg_advisory_lock` (or equivalent)
|
|
17
|
+
* to prevent multiple workers from running maintenance concurrently.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runMaintenance(): Promise<void>;
|
|
14
20
|
export declare function startBackgroundMaintenance(): void;
|
|
15
21
|
export declare function stopBackgroundMaintenance(): void;
|
|
@@ -24,7 +24,12 @@ export function getMaintenanceStatus() {
|
|
|
24
24
|
lastResult: state.lastResult
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @internal — exported for tests. The `state.running` guard is in-process only;
|
|
29
|
+
* for cluster-mode deployments wrap this in a `pg_advisory_lock` (or equivalent)
|
|
30
|
+
* to prevent multiple workers from running maintenance concurrently.
|
|
31
|
+
*/
|
|
32
|
+
export async function runMaintenance() {
|
|
28
33
|
const state = getState();
|
|
29
34
|
if (state.running) {
|
|
30
35
|
console.info('[maintenance] Skipping — previous run still active');
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
import { type ImageStyleRequest } from '../../../../../types/adapters/db.js';
|
|
1
2
|
import type { ImageFieldStyle } from '../../../../../types/fields.js';
|
|
2
3
|
import type { ImageStyle } from '../../../../../types/media.js';
|
|
3
4
|
export declare function getImageStyle(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle>;
|
|
4
5
|
export declare function getImageStyleIfExists(mediaFileId: string, style: ImageFieldStyle): Promise<ImageStyle | null>;
|
|
6
|
+
/**
|
|
7
|
+
* Batch resolver: looks up all requested image styles in one DB query, then sequentially
|
|
8
|
+
* generates any missing ones via Sharp (createImageStyle). Returns a Map keyed by
|
|
9
|
+
* `imageStyleKey(mediaFileId, style)`. Failed Sharp generations are logged and omitted.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getImageStylesBatch(requests: ImageStyleRequest[]): Promise<Map<string, ImageStyle>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getCMS } from '../../../../cms.js';
|
|
2
|
+
import { imageStyleKey } from '../../../../../types/adapters/db.js';
|
|
2
3
|
import { createImageStyle } from './createMediaStyle.js';
|
|
3
4
|
export async function getImageStyle(mediaFileId, style) {
|
|
4
5
|
const imageStyle = await getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
|
|
@@ -9,3 +10,26 @@ export async function getImageStyle(mediaFileId, style) {
|
|
|
9
10
|
export async function getImageStyleIfExists(mediaFileId, style) {
|
|
10
11
|
return getCMS().databaseAdapter.getImageStyle(mediaFileId, style);
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Batch resolver: looks up all requested image styles in one DB query, then sequentially
|
|
15
|
+
* generates any missing ones via Sharp (createImageStyle). Returns a Map keyed by
|
|
16
|
+
* `imageStyleKey(mediaFileId, style)`. Failed Sharp generations are logged and omitted.
|
|
17
|
+
*/
|
|
18
|
+
export async function getImageStylesBatch(requests) {
|
|
19
|
+
if (requests.length === 0)
|
|
20
|
+
return new Map();
|
|
21
|
+
const cms = getCMS();
|
|
22
|
+
const cache = await cms.databaseAdapter.getImageStylesBatch(requests);
|
|
23
|
+
const missing = requests.filter((r) => !cache.has(imageStyleKey(r.mediaFileId, r.style)));
|
|
24
|
+
for (const { mediaFileId, style } of missing) {
|
|
25
|
+
const key = imageStyleKey(mediaFileId, style);
|
|
26
|
+
try {
|
|
27
|
+
const created = await createImageStyle(mediaFileId, style);
|
|
28
|
+
cache.set(key, created);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.warn(`[CMS] Failed to generate missing image style "${style.name}" for media ${mediaFileId}:`, e);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return cache;
|
|
35
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import type { Config } from './types.js';
|
|
3
3
|
import * as schema from './schema/index.js';
|
|
4
|
-
import type
|
|
4
|
+
import { type DatabaseAdapter } from '../types/adapters/db.js';
|
|
5
5
|
export * from './schema/index.js';
|
|
6
6
|
export * from '../server/db/schema/auth-schema.js';
|
|
7
7
|
export type DatabaseAdapterWithDrizzle = DatabaseAdapter & {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
2
|
import postgres from 'postgres';
|
|
3
3
|
import * as schema from './schema/index.js';
|
|
4
|
+
import { imageStyleKey } from '../types/adapters/db.js';
|
|
4
5
|
import { eq, and, inArray, sql, isNull, desc, asc, SQL, ilike, or, count, gte, lte } from 'drizzle-orm';
|
|
5
6
|
const SAFE_JSON_KEY = /^[a-zA-Z0-9_]+$/;
|
|
6
7
|
function validateJsonPathKeys(path) {
|
|
@@ -656,6 +657,32 @@ export function pg(config) {
|
|
|
656
657
|
media: imageStyle.media ? imageStyle.media : undefined
|
|
657
658
|
};
|
|
658
659
|
},
|
|
660
|
+
getImageStylesBatch: async (requests) => {
|
|
661
|
+
const result = new Map();
|
|
662
|
+
if (requests.length === 0)
|
|
663
|
+
return result;
|
|
664
|
+
const wantedKeys = new Set();
|
|
665
|
+
const mediaFileIds = new Set();
|
|
666
|
+
for (const r of requests) {
|
|
667
|
+
wantedKeys.add(imageStyleKey(r.mediaFileId, r.style));
|
|
668
|
+
mediaFileIds.add(r.mediaFileId);
|
|
669
|
+
}
|
|
670
|
+
const rows = await db
|
|
671
|
+
.select()
|
|
672
|
+
.from(schema.imageStylesTable)
|
|
673
|
+
.where(inArray(schema.imageStylesTable.mediaFileId, [...mediaFileIds]));
|
|
674
|
+
for (const row of rows) {
|
|
675
|
+
const key = `${row.mediaFileId}|${row.name}|${row.width ?? 0}|${row.height ?? 0}|${row.quality ?? 0}|${row.aspectRatio ?? 0}`;
|
|
676
|
+
if (!wantedKeys.has(key))
|
|
677
|
+
continue;
|
|
678
|
+
result.set(key, {
|
|
679
|
+
url: row.url,
|
|
680
|
+
mimeType: row.mimeType,
|
|
681
|
+
media: row.media ? row.media : undefined
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
return result;
|
|
685
|
+
},
|
|
659
686
|
createImageStyle: async (mediaFileId, file, style) => {
|
|
660
687
|
const mimeType = file.mimeType ?? `image/${style.format}`;
|
|
661
688
|
const rows = await db.execute(sql `
|
|
@@ -1,3 +1,36 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function hello_world(inputs: {
|
|
2
|
+
name: NonNullable<unknown>;
|
|
3
|
+
}, options?: {
|
|
4
|
+
locale?: "en" | "pl";
|
|
5
|
+
}): string;
|
|
6
|
+
/**
|
|
7
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
8
|
+
*
|
|
9
|
+
* - Changing this function will be over-written by the next build.
|
|
10
|
+
*
|
|
11
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
12
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
13
|
+
*
|
|
14
|
+
* @param {{}} inputs
|
|
15
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
declare function login_hello(inputs?: {}, options?: {
|
|
19
|
+
locale?: "en" | "pl";
|
|
20
|
+
}): string;
|
|
21
|
+
/**
|
|
22
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
23
|
+
*
|
|
24
|
+
* - Changing this function will be over-written by the next build.
|
|
25
|
+
*
|
|
26
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
27
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
28
|
+
*
|
|
29
|
+
* @param {{}} inputs
|
|
30
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
declare function login_please_login(inputs?: {}, options?: {
|
|
34
|
+
locale?: "en" | "pl";
|
|
35
|
+
}): string;
|
|
36
|
+
export { login_hello as login.hello, login_please_login as login.please_login };
|
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from "../runtime.js"
|
|
3
|
+
import * as en from "./en.js"
|
|
4
|
+
import * as pl from "./pl.js"
|
|
5
|
+
/**
|
|
6
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
7
|
+
*
|
|
8
|
+
* - Changing this function will be over-written by the next build.
|
|
9
|
+
*
|
|
10
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
11
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
12
|
+
*
|
|
13
|
+
* @param {{ name: NonNullable<unknown> }} inputs
|
|
14
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
18
|
+
export const hello_world = (inputs, options = {}) => {
|
|
19
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
20
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
21
|
+
}
|
|
22
|
+
const locale = options.locale ?? getLocale()
|
|
23
|
+
trackMessageCall("hello_world", locale)
|
|
24
|
+
if (locale === "en") return en.hello_world(inputs)
|
|
25
|
+
return pl.hello_world(inputs)
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
29
|
+
*
|
|
30
|
+
* - Changing this function will be over-written by the next build.
|
|
31
|
+
*
|
|
32
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
33
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
34
|
+
*
|
|
35
|
+
* @param {{}} inputs
|
|
36
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
40
|
+
const login_hello = (inputs = {}, options = {}) => {
|
|
41
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
42
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
43
|
+
}
|
|
44
|
+
const locale = options.locale ?? getLocale()
|
|
45
|
+
trackMessageCall("login_hello", locale)
|
|
46
|
+
if (locale === "en") return en.login_hello(inputs)
|
|
47
|
+
return pl.login_hello(inputs)
|
|
48
|
+
};
|
|
49
|
+
export { login_hello as "login.hello" }
|
|
50
|
+
/**
|
|
51
|
+
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
52
|
+
*
|
|
53
|
+
* - Changing this function will be over-written by the next build.
|
|
54
|
+
*
|
|
55
|
+
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
56
|
+
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
57
|
+
*
|
|
58
|
+
* @param {{}} inputs
|
|
59
|
+
* @param {{ locale?: "en" | "pl" }} options
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
/* @__NO_SIDE_EFFECTS__ */
|
|
63
|
+
const login_please_login = (inputs = {}, options = {}) => {
|
|
64
|
+
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
65
|
+
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
66
|
+
}
|
|
67
|
+
const locale = options.locale ?? getLocale()
|
|
68
|
+
trackMessageCall("login_please_login", locale)
|
|
69
|
+
if (locale === "en") return en.login_please_login(inputs)
|
|
70
|
+
return pl.login_please_login(inputs)
|
|
71
|
+
};
|
|
72
|
+
export { login_please_login as "login.please_login" }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from en!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Welcome back`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
+
return `Login to your account`
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export const hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
+
return `Hello, ${i.name} from pl!`
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
+
return `Witaj ponownie`
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
13
|
+
return `Zaloguj się na swoje konto`
|
|
14
|
+
};
|
|
@@ -44,6 +44,7 @@ export interface DatabaseAdapter {
|
|
|
44
44
|
bulkDeleteMediaFiles: BulkDeleteMediaFiles;
|
|
45
45
|
createMediaFile: CreateMediaFile;
|
|
46
46
|
getImageStyle: GetImageStyle;
|
|
47
|
+
getImageStylesBatch: GetImageStylesBatch;
|
|
47
48
|
createImageStyle: CreateImageStyle;
|
|
48
49
|
deleteImageStylesByMediaFileId: DeleteImageStylesByMediaFileId;
|
|
49
50
|
getAllImageStyles: GetAllImageStyles;
|
|
@@ -192,3 +193,18 @@ export type GetAllVideoStyles = () => Promise<{
|
|
|
192
193
|
}[]>;
|
|
193
194
|
export type DeleteAllVideoStyles = () => Promise<number>;
|
|
194
195
|
export type GetImageStyle = (mediaFileId: string, style: ImageFieldStyle) => Promise<ImageStyle | null>;
|
|
196
|
+
export interface ImageStyleRequest {
|
|
197
|
+
mediaFileId: string;
|
|
198
|
+
style: ImageFieldStyle;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Batch lookup for image styles. Resolves multiple (mediaFileId, style) pairs in a single query
|
|
202
|
+
* to avoid N+1 in render paths that materialize image fields. Result is keyed by `imageStyleKey`.
|
|
203
|
+
*/
|
|
204
|
+
export type GetImageStylesBatch = (requests: ImageStyleRequest[]) => Promise<Map<string, ImageStyle>>;
|
|
205
|
+
/**
|
|
206
|
+
* Stable cache key for an (mediaFileId, ImageFieldStyle) pair. Matches the unique index on
|
|
207
|
+
* `image_styles` (mediaFileId, name, COALESCE(width,0), COALESCE(height,0), COALESCE(quality,0),
|
|
208
|
+
* COALESCE(aspectRatio,0)).
|
|
209
|
+
*/
|
|
210
|
+
export declare function imageStyleKey(mediaFileId: string, style: ImageFieldStyle): string;
|
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Stable cache key for an (mediaFileId, ImageFieldStyle) pair. Matches the unique index on
|
|
3
|
+
* `image_styles` (mediaFileId, name, COALESCE(width,0), COALESCE(height,0), COALESCE(quality,0),
|
|
4
|
+
* COALESCE(aspectRatio,0)).
|
|
5
|
+
*/
|
|
6
|
+
export function imageStyleKey(mediaFileId, style) {
|
|
7
|
+
return `${mediaFileId}|${style.name}|${style.width ?? 0}|${style.height ?? 0}|${style.quality ?? 0}|${style.aspectRatio ?? 0}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const update = {
|
|
2
|
+
version: '0.24.0',
|
|
3
|
+
date: '2026-04-30',
|
|
4
|
+
description: 'Faza 11 — Performance Pass. Eliminacja N+1 w render path: `getImageStylesBatch` (jeden SELECT zamiast N×getImageStyle dla styles + srcset wariantów) + `_getRawEntries` z batchowanym `getEntryVersions` (jeden call zamiast per-entry). Sharp srcset pipeline odrefactorowany — Promise.all wokół per-style queries usunięty (jeden batch + sequential Sharp generation z try/catch dla missing entries). Background maintenance lock potwierdzony testami (concurrent invocation skipped). Bench infra: nowy projekt vitest `bench` + skrypt `pnpm bench` + 2 representative benchmarks (image styles + raw entries).',
|
|
5
|
+
features: [
|
|
6
|
+
'`databaseAdapter.getImageStylesBatch(requests)` — nowa metoda adaptera. Zwraca `Map<key, ImageStyle>` keyed przez `imageStyleKey(mediaFileId, style)`. Implementacja `db-postgres`: jeden SELECT `WHERE mediaFileId IN (...)` + JS-side filter po (name, width, height, quality, aspectRatio) matching unique index. Skala impactu: 100 entries × 3 styles × 4 srcset = ~1200 queries → 1 query per media file (~30 queries dla 30 unique files). Bench `getImageStyles`: ~750 ops/s (1.3ms p75) dla 1 media × 3 styles × srcset; baseline N+1 (72 sequential): ~11 ops/s — **71× speedup**.',
|
|
7
|
+
'`_getRawEntries()` (`src/lib/core/server/entries/operations/get.ts`) zrefactorowany — zamiast per-entry `getEntryVersions({ entryIds: [entry.id] })` w `Promise.all(dbEntries.map(...))` (N+1) teraz jeden `getEntryVersions({ entryIds: dbEntries.map(e => e.id) })` + group by `entryId` w Map. Adapter API bez zmian (już obsługiwał array `entryIds`). Skala: 100 entries → 2 queries zamiast 101. Bench: 387 ops/s (~2.6ms) dla 100 entries; baseline N+1 (101 sequential): ~7.9 ops/s — **50× speedup**.',
|
|
8
|
+
'Sharp srcset variant generation: `Promise.all(widths.map(getImageStyle))` całkowicie wyeliminowany w `imageStyles.ts`. Nowy flow: zebrać wszystkie style requesty (base + srcset variants), jeden `getImageStylesBatch()` call, fallback do `createImageStyle()` per missing z try/catch. Konsekwencje: jeden Sharp timeout (30s, env `INCLUDIO_SHARP_TIMEOUT_MS`) nie zabija batch innych wariantów — zachowanie identyczne jak `Promise.allSettled`, ale bez Promise.all w pipeline.',
|
|
9
|
+
'`getImageStylesBatch(requests)` operations wrapper (`src/lib/core/server/media/styles/operations/getImageStyle.ts`): koordynuje DB lookup + Sharp generation. Krótkie ścieżki: empty requests → empty Map bez DB call; wszystkie hity → cache return bez `createImageStyle`. Failed Sharp → `console.warn` + omission, nie throw.',
|
|
10
|
+
'`imageStyleKey(mediaFileId, style)` — stabilna helper key z `$lib/types/adapters/db.js` (eksport public). Format: `${mediaFileId}|${name}|${width||0}|${height||0}|${quality||0}|${aspectRatio||0}` — matchuje unique index na `image_styles`.',
|
|
11
|
+
'Background maintenance — in-process lock (`state.running` na `globalThis`) potwierdzony test suitem: `runMaintenance` exported (`@internal`), test concurrent dwóch wywołań → drugie loguje `Skipping — previous run still active`, batch operations wywołane dokładnie 1×. Komment w pliku: dla cluster mode wrap w `pg_advisory_lock`.',
|
|
12
|
+
'`pnpm bench` — nowy skrypt + dedykowany vitest project `bench` (vite.config.ts). Include `src/**/*.bench.ts`. 2 nowe benchmarki: `imageStyles.bench.ts` (real `getImageStyles` z mocked adapter latency 1ms + synthetic N+1 vs batch comparison), `get.bench.ts` (real `_getRawEntries` 100 entries + comparison). Istniejący `resolveTypographyOrphans.bench.ts` teraz uruchamiany w tej samej komendzie.',
|
|
13
|
+
'Lazy adapter imports (OpenAI / Anthropic / Nodemailer) — verified no-op. Wszystkie 3 adaptery (`src/lib/{ai-claude,ai-openai,email-nodemailer}/index.ts`) już używają `await import()` + cached singleton w closure. Type imports (`import type`) nie ładują runtime code. Trigger: pierwszy call metody adaptera (np. `generateAltText`, `sendMail`).'
|
|
14
|
+
],
|
|
15
|
+
fixes: [],
|
|
16
|
+
breakingChanges: [
|
|
17
|
+
'`DatabaseAdapter.getImageStylesBatch` — nowa **wymagana** metoda interfejsu (`src/lib/types/adapters/db.ts`). Custom adapters muszą zaimplementować. Sygnatura: `(requests: ImageStyleRequest[]) => Promise<Map<string, ImageStyle>>`. Reference impl: `src/lib/db-postgres/index.ts` (jeden SELECT z `inArray` po `mediaFileId` + JS filter). Helper key: `imageStyleKey(mediaFileId, style)` z `$lib/types/adapters/db.js`.'
|
|
18
|
+
],
|
|
19
|
+
notes: 'Zero schema migration — używa istniejącej tabeli `image_styles` i unique indeksu. Hot path render: collection list z 100 entries × image fields powinien zobaczyć ~50-70× redukcję wall-time queries (mierzone via bench na mocked latency 1ms; real PG latency wyższa, więc bezwzględne win większe). Sharp generation pozostaje sekwencyjne dla missing styles per request — celowo, żeby nie obciążać CPU; background maintenance batch (`batchGenerateAllStyles`) prewarmuje DB. `runMaintenance` eksportowany jako `@internal` dla testów — nie zaliczany do public API surface.'
|
|
20
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -57,7 +57,8 @@ import { update as update0200 } from './0.20.0/index.js';
|
|
|
57
57
|
import { update as update0210 } from './0.21.0/index.js';
|
|
58
58
|
import { update as update0220 } from './0.22.0/index.js';
|
|
59
59
|
import { update as update0230 } from './0.23.0/index.js';
|
|
60
|
-
|
|
60
|
+
import { update as update0240 } from './0.24.0/index.js';
|
|
61
|
+
export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142, update0143, update0144, update0145, update0146, update0150, update0151, update0152, update0153, update0154, update0155, update0160, update0180, update0190, update0200, update0210, update0220, update0230, update0240];
|
|
61
62
|
export const getUpdatesFrom = (fromVersion) => {
|
|
62
63
|
const fromParts = fromVersion.split('.').map(Number);
|
|
63
64
|
return updates.filter((update) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "includio-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "vite build && npm run prepack",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"format": "prettier --write .",
|
|
13
13
|
"lint": "prettier --check . && eslint .",
|
|
14
14
|
"test:unit": "vitest",
|
|
15
|
+
"bench": "vitest bench --run --project=bench",
|
|
15
16
|
"test": "npm run test:unit -- --run && npm run test:e2e",
|
|
16
17
|
"test:e2e": "playwright test",
|
|
17
18
|
"test:coverage": "vitest run --coverage",
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
const en_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
5
|
-
return `Hello, ${i.name} from en!`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const pl_hello_world = /** @type {(inputs: { name: NonNullable<unknown> }) => string} */ (i) => {
|
|
9
|
-
return `Hello, ${i.name} from pl!`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
-
*
|
|
15
|
-
* - Changing this function will be over-written by the next build.
|
|
16
|
-
*
|
|
17
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
-
*
|
|
20
|
-
* @param {{ name: NonNullable<unknown> }} inputs
|
|
21
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
-
export const hello_world = (inputs, options = {}) => {
|
|
26
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.hello_world(inputs)
|
|
28
|
-
}
|
|
29
|
-
const locale = options.locale ?? getLocale()
|
|
30
|
-
trackMessageCall("hello_world", locale)
|
|
31
|
-
if (locale === "en") return en_hello_world(inputs)
|
|
32
|
-
return pl_hello_world(inputs)
|
|
33
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export { login_hello as login.hello };
|
|
2
|
-
/**
|
|
3
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
-
*
|
|
5
|
-
* - Changing this function will be over-written by the next build.
|
|
6
|
-
*
|
|
7
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
-
*
|
|
10
|
-
* @param {{}} inputs
|
|
11
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
declare function login_hello(inputs?: {}, options?: {
|
|
15
|
-
locale?: "en" | "pl";
|
|
16
|
-
}): string;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
const en_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
-
return `Welcome back`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const pl_login_hello = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Witaj ponownie`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
-
*
|
|
15
|
-
* - Changing this function will be over-written by the next build.
|
|
16
|
-
*
|
|
17
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
-
*
|
|
20
|
-
* @param {{}} inputs
|
|
21
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
-
const login_hello = (inputs = {}, options = {}) => {
|
|
26
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_hello(inputs)
|
|
28
|
-
}
|
|
29
|
-
const locale = options.locale ?? getLocale()
|
|
30
|
-
trackMessageCall("login_hello", locale)
|
|
31
|
-
if (locale === "en") return en_login_hello(inputs)
|
|
32
|
-
return pl_login_hello(inputs)
|
|
33
|
-
};
|
|
34
|
-
export { login_hello as "login.hello" }
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export { login_please_login as login.please_login };
|
|
2
|
-
/**
|
|
3
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
4
|
-
*
|
|
5
|
-
* - Changing this function will be over-written by the next build.
|
|
6
|
-
*
|
|
7
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
8
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
9
|
-
*
|
|
10
|
-
* @param {{}} inputs
|
|
11
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
12
|
-
* @returns {string}
|
|
13
|
-
*/
|
|
14
|
-
declare function login_please_login(inputs?: {}, options?: {
|
|
15
|
-
locale?: "en" | "pl";
|
|
16
|
-
}): string;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';
|
|
3
|
-
|
|
4
|
-
const en_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
5
|
-
return `Login to your account`
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const pl_login_please_login = /** @type {(inputs: {}) => string} */ () => {
|
|
9
|
-
return `Zaloguj się na swoje konto`
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
|
|
14
|
-
*
|
|
15
|
-
* - Changing this function will be over-written by the next build.
|
|
16
|
-
*
|
|
17
|
-
* - If you want to change the translations, you can either edit the source files e.g. `en.json`, or
|
|
18
|
-
* use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
|
|
19
|
-
*
|
|
20
|
-
* @param {{}} inputs
|
|
21
|
-
* @param {{ locale?: "en" | "pl" }} options
|
|
22
|
-
* @returns {string}
|
|
23
|
-
*/
|
|
24
|
-
/* @__NO_SIDE_EFFECTS__ */
|
|
25
|
-
const login_please_login = (inputs = {}, options = {}) => {
|
|
26
|
-
if (experimentalMiddlewareLocaleSplitting && isServer === false) {
|
|
27
|
-
return /** @type {any} */ (globalThis).__paraglide_ssr.login_please_login(inputs)
|
|
28
|
-
}
|
|
29
|
-
const locale = options.locale ?? getLocale()
|
|
30
|
-
trackMessageCall("login_please_login", locale)
|
|
31
|
-
if (locale === "en") return en_login_please_login(inputs)
|
|
32
|
-
return pl_login_please_login(inputs)
|
|
33
|
-
};
|
|
34
|
-
export { login_please_login as "login.please_login" }
|