@kirchdev/duxt 0.10.1-dev-main.5-62e738f → 0.10.1-dev-main.10-e43ad25
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/app/components/DuxtPageInfo.vue +60 -1
- package/app/types/duxt.d.ts +1 -1
- package/app/utils/source-file.ts +33 -0
- package/build/sources/sources-git.ts +31 -5
- package/build/sources/sources-resolve.ts +11 -3
- package/package.json +1 -1
- package/server/plugins/content-import-lifetime.ts +45 -0
- package/server/plugins/content-restore-recovery.ts +65 -0
- package/server/utils/content-restore.ts +91 -0
|
@@ -42,7 +42,8 @@ const props = defineProps<{
|
|
|
42
42
|
|
|
43
43
|
const page = computed(() => props.page);
|
|
44
44
|
|
|
45
|
-
const { source } = useDuxtCollection();
|
|
45
|
+
const { source, sources } = useDuxtCollection();
|
|
46
|
+
const path = useDuxtPath();
|
|
46
47
|
const { locale, t } = useI18n();
|
|
47
48
|
const duxt = useDuxtConfig();
|
|
48
49
|
|
|
@@ -73,6 +74,52 @@ const link = computed(() =>
|
|
|
73
74
|
: undefined
|
|
74
75
|
);
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Where a reader of a TAG edits this page: the same page on the branch the
|
|
79
|
+
* source also publishes — see `editableEdition` — and only where that edition
|
|
80
|
+
* still has it, so a page renamed since the tag gets no form on a 404.
|
|
81
|
+
*
|
|
82
|
+
* The stem is read off the branch's own row rather than reused from this one:
|
|
83
|
+
* a numbered folder can move between the two, and the stem is the only thing
|
|
84
|
+
* that names the file.
|
|
85
|
+
*
|
|
86
|
+
* Nothing is read after the `await` but values captured before it — the rule
|
|
87
|
+
* `tests/async-data-context.test.ts` holds.
|
|
88
|
+
*/
|
|
89
|
+
const { data: editLink } = useAsyncData(
|
|
90
|
+
() => `duxt-edit-link-${path.value}`,
|
|
91
|
+
async () => {
|
|
92
|
+
const own = source.value;
|
|
93
|
+
const branch = editableEdition(sources.value, own);
|
|
94
|
+
if (!own || !branch) return null;
|
|
95
|
+
|
|
96
|
+
const counterpart = versionPath(
|
|
97
|
+
path.value,
|
|
98
|
+
own.prefix,
|
|
99
|
+
branch.prefix || '/'
|
|
100
|
+
);
|
|
101
|
+
const hit = await queryCollection(branch.collection as DuxtCollectionArg)
|
|
102
|
+
.path(counterpart)
|
|
103
|
+
.select('stem')
|
|
104
|
+
.first();
|
|
105
|
+
if (!hit) return null;
|
|
106
|
+
|
|
107
|
+
const target = branch.generated
|
|
108
|
+
? branch.path
|
|
109
|
+
: sourceFilePath(
|
|
110
|
+
hit.stem,
|
|
111
|
+
branch.prefix,
|
|
112
|
+
branch.originPath ?? branch.path
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
sourceLink(branch.repositoryUrl, branch.ref, branch.refKind, target) ??
|
|
117
|
+
null
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
{ default: () => null, watch: [path] }
|
|
121
|
+
);
|
|
122
|
+
|
|
76
123
|
/**
|
|
77
124
|
* The day this page names, where it names one.
|
|
78
125
|
*
|
|
@@ -130,6 +177,18 @@ const actions = computed(() => {
|
|
|
130
177
|
});
|
|
131
178
|
}
|
|
132
179
|
|
|
180
|
+
// A tag has no edit form of its own, but its branch may — see `editLink`.
|
|
181
|
+
// Drawn first, beside the tag's own source rather than instead of it: the
|
|
182
|
+
// reader may want either the text as released or the place to fix it.
|
|
183
|
+
if (editLink.value) {
|
|
184
|
+
entries.push({
|
|
185
|
+
icon: 'lucide:pencil',
|
|
186
|
+
label: t('duxt.page.edit'),
|
|
187
|
+
to: editLink.value.url,
|
|
188
|
+
external: true
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
133
192
|
// A tag has no edit form — see `sourceLink`. The label follows the kind
|
|
134
193
|
// rather than promising a form that answers with a 404.
|
|
135
194
|
if (link.value) {
|
package/app/types/duxt.d.ts
CHANGED
|
@@ -287,7 +287,7 @@ declare global {
|
|
|
287
287
|
* `duxt-typesense@v0.1.0`. Applies to `latest` and `releases`; plain
|
|
288
288
|
* `vX.Y.Z` tags still count.
|
|
289
289
|
*/
|
|
290
|
-
tagComponent?: string;
|
|
290
|
+
tagComponent?: string | readonly string[];
|
|
291
291
|
/**
|
|
292
292
|
* The repository a source read off disk lives in, for links back to it.
|
|
293
293
|
* Not `repo`, which is what makes Content download a source.
|
package/app/utils/source-file.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sameArtefact } from './version-choices';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* From a page back to the file it was written in.
|
|
3
5
|
*
|
|
@@ -82,3 +84,34 @@ export function sourceLink(
|
|
|
82
84
|
|
|
83
85
|
return undefined;
|
|
84
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The edition a reader of a TAG can edit instead: the branch the same source
|
|
90
|
+
* also publishes.
|
|
91
|
+
*
|
|
92
|
+
* `sourceLink` refuses an edit form on a tag, and rightly — but a reader of the
|
|
93
|
+
* default version is on a tag nearly always, so the refusal alone left the
|
|
94
|
+
* whole default edition without an edit link. The branch is where a fix to the
|
|
95
|
+
* page actually lands. Whether the page still EXISTS there is the caller's
|
|
96
|
+
* question, answered against that edition's collection before the link is
|
|
97
|
+
* drawn: a file renamed since the tag would otherwise be an edit form on a 404.
|
|
98
|
+
*
|
|
99
|
+
* Same repository, same artefact and same language, so a German page of
|
|
100
|
+
* v0.9.0 is edited as the German page on `main` and an API reference as the
|
|
101
|
+
* API reference. The first such branch in manifest order wins.
|
|
102
|
+
*/
|
|
103
|
+
export function editableEdition(
|
|
104
|
+
sources: DuxtResolvedSource[],
|
|
105
|
+
current: DuxtResolvedSource | undefined
|
|
106
|
+
): DuxtResolvedSource | undefined {
|
|
107
|
+
if (current?.refKind !== 'tag' || !current.repositoryUrl) return undefined;
|
|
108
|
+
|
|
109
|
+
return sources.find(
|
|
110
|
+
(source) =>
|
|
111
|
+
source.refKind === 'branch' &&
|
|
112
|
+
Boolean(source.ref) &&
|
|
113
|
+
source.repositoryUrl === current.repositoryUrl &&
|
|
114
|
+
source.locale === current.locale &&
|
|
115
|
+
sameArtefact(source, current)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -159,28 +159,54 @@ function tagsFor(source: DuxtSource, what: string): string[] {
|
|
|
159
159
|
/**
|
|
160
160
|
* The version tags a source may publish from: every tag that reads as a
|
|
161
161
|
* version and belongs to the source's `tagComponent`, when it names one.
|
|
162
|
+
*
|
|
163
|
+
* A family of components shares one version line, so one release is often
|
|
164
|
+
* tagged once per member — `duxt@v0.10.0` beside `duxt-typesense@v0.10.0`.
|
|
165
|
+
* Those are one version, kept once, from the member listed first.
|
|
162
166
|
*/
|
|
163
167
|
function versionTags(
|
|
164
168
|
tags: string[],
|
|
165
|
-
component: string | undefined
|
|
169
|
+
component: string | readonly string[] | undefined
|
|
166
170
|
): { tag: string; version: DuxtVersionTag }[] {
|
|
167
|
-
|
|
171
|
+
const matching = tags
|
|
168
172
|
.map((tag) => ({ tag, version: parseVersionTag(tag) }))
|
|
169
173
|
.filter(
|
|
170
174
|
(entry): entry is { tag: string; version: DuxtVersionTag } =>
|
|
171
175
|
Boolean(entry.version) && tagBelongsTo(entry.version!, component)
|
|
172
176
|
);
|
|
177
|
+
if (typeof component === 'string' || !component) return matching;
|
|
178
|
+
|
|
179
|
+
const rank = (entry: { version: DuxtVersionTag }) =>
|
|
180
|
+
entry.version.component
|
|
181
|
+
? component.indexOf(entry.version.component)
|
|
182
|
+
: component.length;
|
|
183
|
+
const byVersion = new Map<string, { tag: string; version: DuxtVersionTag }>();
|
|
184
|
+
for (const entry of matching) {
|
|
185
|
+
const key = versionKey(entry.version);
|
|
186
|
+
const kept = byVersion.get(key);
|
|
187
|
+
if (!kept || rank(entry) < rank(kept)) byVersion.set(key, entry);
|
|
188
|
+
}
|
|
189
|
+
return [...byVersion.values()];
|
|
173
190
|
}
|
|
174
191
|
|
|
192
|
+
/** One version regardless of who tagged it or whether it carries a `v`. */
|
|
193
|
+
const versionKey = (version: DuxtVersionTag) =>
|
|
194
|
+
version.numbers.join('.') + (version.pre ? `-${version.pre}` : '');
|
|
195
|
+
|
|
175
196
|
/** How a source's tag restriction reads in an error, or nothing without one. */
|
|
176
|
-
const componentClause = (source: DuxtSource) =>
|
|
177
|
-
|
|
197
|
+
const componentClause = (source: DuxtSource) => {
|
|
198
|
+
const names = source.tagComponent;
|
|
199
|
+
if (!names || names.length === 0) return '';
|
|
200
|
+
return typeof names === 'string'
|
|
201
|
+
? ` for the component "${names}"`
|
|
202
|
+
: ` for the components ${names.map((name) => `"${name}"`).join(', ')}`;
|
|
203
|
+
};
|
|
178
204
|
|
|
179
205
|
/** Tags selected by a source's explicit release policy, newest first. */
|
|
180
206
|
function releaseTags(
|
|
181
207
|
tags: string[],
|
|
182
208
|
releases: DuxtSourceReleases,
|
|
183
|
-
component: string | undefined
|
|
209
|
+
component: string | readonly string[] | undefined
|
|
184
210
|
): string[] {
|
|
185
211
|
const parsed = versionTags(tags, component)
|
|
186
212
|
.filter((entry) => releases.prereleases || !entry.version.pre)
|
|
@@ -60,8 +60,13 @@ export interface DuxtSource {
|
|
|
60
60
|
* component tags, and dropping them would unpublish every earlier release.
|
|
61
61
|
*
|
|
62
62
|
* Unset, every tag that reads as a version is a candidate, prefixed or not.
|
|
63
|
+
*
|
|
64
|
+
* A LIST names a family released on one version line — the layer and its
|
|
65
|
+
* companion layers under `versioning: linked` — so `latest` and `releases`
|
|
66
|
+
* follow whichever of them released last. One version carried by several
|
|
67
|
+
* of them is one release, published from the tag of the name listed first.
|
|
63
68
|
*/
|
|
64
|
-
tagComponent?: string;
|
|
69
|
+
tagComponent?: string | readonly string[];
|
|
65
70
|
/**
|
|
66
71
|
* Languages this source is available in, beyond the one written in `path`.
|
|
67
72
|
*
|
|
@@ -964,9 +969,12 @@ export const versionTagName = (tag: string): string =>
|
|
|
964
969
|
*/
|
|
965
970
|
export function tagBelongsTo(
|
|
966
971
|
tag: DuxtVersionTag,
|
|
967
|
-
component: string | undefined
|
|
972
|
+
component: string | readonly string[] | undefined
|
|
968
973
|
): boolean {
|
|
969
|
-
|
|
974
|
+
if (!component || !tag.component) return true;
|
|
975
|
+
return typeof component === 'string'
|
|
976
|
+
? tag.component === component
|
|
977
|
+
: component.includes(tag.component);
|
|
970
978
|
}
|
|
971
979
|
|
|
972
980
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://www.schemastore.org/package.json",
|
|
3
3
|
"name": "@kirchdev/duxt",
|
|
4
|
-
"version": "0.10.1-dev-main.
|
|
4
|
+
"version": "0.10.1-dev-main.10-e43ad25",
|
|
5
5
|
"description": "duxt is a Nuxt layer: extend it and your docs/ folder becomes a site, with theme, search, API reference and llms.txt included. Point it at other repositories, or at tags of the same one, and each becomes a version.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"content-v3",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps a request alive on Cloudflare until it has produced its response, even
|
|
3
|
+
* after the reader has gone.
|
|
4
|
+
*
|
|
5
|
+
* Content restores a collection into D1 the first time a request reads it
|
|
6
|
+
* after a deploy: it writes the collection's `_content_info` row with
|
|
7
|
+
* `ready = 0`, replays the dump one statement at a time — about ten seconds a
|
|
8
|
+
* collection on duxt.app — and flips the row to ready as the last statement.
|
|
9
|
+
* A Worker CANCELS a request's work the moment its client disconnects. A
|
|
10
|
+
* reader who gives up, a crawler with a timeout, a prefetch the router drops:
|
|
11
|
+
* any of them stops that replay halfway, and the row is never flipped. Every
|
|
12
|
+
* later read of that collection then waits on `ready` for 90 seconds and
|
|
13
|
+
* fails, on every isolate, until `content-restore-recovery.ts` clears the row —
|
|
14
|
+
* and a docs page reads every version's collection to draw its version
|
|
15
|
+
* switcher, so one stuck collection hangs every page of every version.
|
|
16
|
+
* duxt.app ran into exactly that after the v0.10.0 deploy.
|
|
17
|
+
*
|
|
18
|
+
* `waitUntil` is the one thing a Worker lets outlive a disconnect, so every
|
|
19
|
+
* request hands it a promise that settles when the request is done. Anything
|
|
20
|
+
* it started — the restore included — then runs to completion inside the
|
|
21
|
+
* platform's grace period rather than being cut off. Other runtimes do not
|
|
22
|
+
* cancel on disconnect, so this only arms where `cloudflare` is in the context.
|
|
23
|
+
*/
|
|
24
|
+
const settle = Symbol('duxt.settle');
|
|
25
|
+
|
|
26
|
+
type Settling = { [settle]?: () => void };
|
|
27
|
+
|
|
28
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
29
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
30
|
+
if (!event.context.cloudflare || !event.waitUntil) return;
|
|
31
|
+
|
|
32
|
+
event.waitUntil(
|
|
33
|
+
new Promise<void>((resolve) => {
|
|
34
|
+
(event.context as Settling)[settle] = resolve;
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const done = (event: { context: Settling } | undefined) => {
|
|
40
|
+
event?.context[settle]?.();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
nitroApp.hooks.hook('afterResponse', (event) => done(event));
|
|
44
|
+
nitroApp.hooks.hook('error', (_error, { event }) => done(event));
|
|
45
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
duxtRecoverContentRestores,
|
|
3
|
+
type DuxtD1
|
|
4
|
+
} from '../utils/content-restore';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clears Content restores a cancelled Worker left half done, from inside the
|
|
8
|
+
* Worker, on D1.
|
|
9
|
+
*
|
|
10
|
+
* `content-import-lifetime.ts` keeps a restore running past a disconnect, but
|
|
11
|
+
* only for the platform's grace period, and a row that was stranded before it
|
|
12
|
+
* — or by a deploy that cut a running restore off — stays unready for good:
|
|
13
|
+
* every read of that collection then waits 90 seconds and fails, and a docs
|
|
14
|
+
* page reads every version's collection to draw its switcher. duxt.app sat
|
|
15
|
+
* like that after the v0.10.0 deploy until the row was removed by hand.
|
|
16
|
+
*
|
|
17
|
+
* It runs here rather than in the deploy because the Worker is the one thing
|
|
18
|
+
* that already holds the binding: a cleanup step in CI would need a token with
|
|
19
|
+
* D1 rights for every repository that reads it, and would help no site but the
|
|
20
|
+
* one whose workflow carries it.
|
|
21
|
+
*
|
|
22
|
+
* At most once a minute per isolate, after the response, so no reader waits on
|
|
23
|
+
* it. A failure is logged and dropped: this is a repair, and a missing table
|
|
24
|
+
* on a fresh database is not an error.
|
|
25
|
+
*/
|
|
26
|
+
const INTERVAL_MS = 60_000;
|
|
27
|
+
|
|
28
|
+
let last = 0;
|
|
29
|
+
|
|
30
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
31
|
+
nitroApp.hooks.hook('request', (event) => {
|
|
32
|
+
const env = event.context.cloudflare?.env as
|
|
33
|
+
| Record<string, unknown>
|
|
34
|
+
| undefined;
|
|
35
|
+
if (!env || !event.waitUntil) return;
|
|
36
|
+
|
|
37
|
+
const database = useRuntimeConfig(event).content?.database as
|
|
38
|
+
| { type?: string; bindingName?: string; binding?: string }
|
|
39
|
+
| undefined;
|
|
40
|
+
if (database?.type !== 'd1') return;
|
|
41
|
+
|
|
42
|
+
const db = env[database.bindingName || database.binding || 'DB'] as
|
|
43
|
+
| DuxtD1
|
|
44
|
+
| undefined;
|
|
45
|
+
if (!db) return;
|
|
46
|
+
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (now - last < INTERVAL_MS) return;
|
|
49
|
+
last = now;
|
|
50
|
+
|
|
51
|
+
event.waitUntil(
|
|
52
|
+
duxtRecoverContentRestores(db, now)
|
|
53
|
+
.then((removed) => {
|
|
54
|
+
if (removed.length) {
|
|
55
|
+
console.warn(
|
|
56
|
+
`[duxt] cleared abandoned Content restores: ${removed.join(', ')}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.catch((error) => {
|
|
61
|
+
console.warn('[duxt] Content restore check failed', error);
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of a D1 binding this file uses — written out rather than imported,
|
|
3
|
+
* because the layer does not depend on Cloudflare's types and no other
|
|
4
|
+
* runtime ever reaches this code.
|
|
5
|
+
*/
|
|
6
|
+
export interface DuxtD1 {
|
|
7
|
+
prepare(sql: string): DuxtD1Statement;
|
|
8
|
+
batch(statements: DuxtD1Statement[]): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DuxtD1Statement {
|
|
12
|
+
bind(...values: unknown[]): DuxtD1Statement;
|
|
13
|
+
all<T>(): Promise<{ results: T[] }>;
|
|
14
|
+
run(): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Twice the 90 seconds Content waits for another restore before giving up.
|
|
19
|
+
* A restore takes about ten seconds a collection on duxt.app, so a row still
|
|
20
|
+
* unready after this has nobody left writing it.
|
|
21
|
+
*/
|
|
22
|
+
export const DUXT_STALE_RESTORE_MS = 180_000;
|
|
23
|
+
|
|
24
|
+
/** Where a row was first seen unready. Content owns `_content_info`; this is ours. */
|
|
25
|
+
const SEEN = '_duxt_content_unready';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Removes every Content restore that was abandoned halfway, so the next read
|
|
29
|
+
* of that collection restores it again instead of waiting on it forever.
|
|
30
|
+
*
|
|
31
|
+
* Content marks a collection `ready = 0` in `_content_info`, replays its dump
|
|
32
|
+
* and flips the row as the dump's last statement. A Worker cancelled before
|
|
33
|
+
* that statement leaves the row unready for good, and every later read waits
|
|
34
|
+
* 90 seconds and fails — see `server/plugins/content-restore-recovery.ts`.
|
|
35
|
+
*
|
|
36
|
+
* The row carries no timestamp, and an unready row is also exactly what a
|
|
37
|
+
* restore IN PROGRESS looks like, so a single look cannot tell the two apart.
|
|
38
|
+
* Two looks can: the first records when a row was seen unready, and a later
|
|
39
|
+
* one removes it only if the SAME version is still unready past the threshold.
|
|
40
|
+
* A new deploy writes a new version, which starts the clock again.
|
|
41
|
+
*
|
|
42
|
+
* Returns the ids it removed.
|
|
43
|
+
*/
|
|
44
|
+
export async function duxtRecoverContentRestores(
|
|
45
|
+
db: DuxtD1,
|
|
46
|
+
now: number,
|
|
47
|
+
staleMs = DUXT_STALE_RESTORE_MS
|
|
48
|
+
): Promise<string[]> {
|
|
49
|
+
const unready = await db
|
|
50
|
+
.prepare('SELECT id, version FROM _content_info WHERE ready = 0')
|
|
51
|
+
.all<{ id: string; version: string }>();
|
|
52
|
+
|
|
53
|
+
await db.batch([
|
|
54
|
+
db.prepare(
|
|
55
|
+
`CREATE TABLE IF NOT EXISTS ${SEEN} (id TEXT PRIMARY KEY, version TEXT NOT NULL, since INTEGER NOT NULL)`
|
|
56
|
+
),
|
|
57
|
+
// A row that finished, or that Content replaced, is forgotten — otherwise
|
|
58
|
+
// its old clock would condemn the next restore of the same version.
|
|
59
|
+
db.prepare(
|
|
60
|
+
`DELETE FROM ${SEEN} WHERE NOT EXISTS (SELECT 1 FROM _content_info AS info WHERE info.id = ${SEEN}.id AND info.version = ${SEEN}.version AND info.ready = 0)`
|
|
61
|
+
),
|
|
62
|
+
...unready.results.map((row) =>
|
|
63
|
+
db
|
|
64
|
+
.prepare(
|
|
65
|
+
`INSERT INTO ${SEEN} (id, version, since) VALUES (?, ?, ?) ON CONFLICT (id) DO NOTHING`
|
|
66
|
+
)
|
|
67
|
+
.bind(row.id, row.version, now)
|
|
68
|
+
)
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const stale = await db
|
|
72
|
+
.prepare(`SELECT id, version FROM ${SEEN} WHERE since <= ?`)
|
|
73
|
+
.bind(now - staleMs)
|
|
74
|
+
.all<{ id: string; version: string }>();
|
|
75
|
+
if (!stale.results.length) return [];
|
|
76
|
+
|
|
77
|
+
// `ready = 0` and the version in the condition: a restore that finished, or
|
|
78
|
+
// a deploy that replaced the row, since the look above keeps its row.
|
|
79
|
+
await db.batch(
|
|
80
|
+
stale.results.flatMap((row) => [
|
|
81
|
+
db
|
|
82
|
+
.prepare(
|
|
83
|
+
'DELETE FROM _content_info WHERE id = ? AND version = ? AND ready = 0'
|
|
84
|
+
)
|
|
85
|
+
.bind(row.id, row.version),
|
|
86
|
+
db.prepare(`DELETE FROM ${SEEN} WHERE id = ?`).bind(row.id)
|
|
87
|
+
])
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return stale.results.map((row) => row.id);
|
|
91
|
+
}
|