Skip to content

Commit 907ac56

Browse files
authored
fix: position sorts should dictate the order that files are uploaded (#1449)
| 🚥 Resolves CX-3309, #1405, #1367 | | :------------------- | ## 🧰 Changes With ReadMe Refactored the `position` value that we use in our API is not _quite_ the same as it was pre-refactored because it dictates the position that your page should be sorted within the `_order.yaml` file that we create for you in Git. If `page-2` has a `position` of `2` and is created _before_ `page-1`, which has a position of `1`, then the `_order.yaml` file will have `page-2` sorted before `page-1` because it came in first. Pre-Refactored these `position` values were stored as the value you had alongside the page in our database so a file with a `position` of `2` would forever have that position until it explicitly changed. The work I've done here is to change the way that rdme batches up its API requests to ensure that we make these requests in the order of the `position` value that we have present. If we don't have a `position` then we'll fall back to the `slug` available, sorting it naturally (ensuring that `page-10` is not sorted between `page-1 and `page-2`)
1 parent 4887531 commit 907ac56

19 files changed

Lines changed: 865 additions & 47 deletions

File tree

src/lib/syncPagePath.ts

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import path from 'node:path';
66

77
import chalk from 'chalk';
88
import ora from 'ora';
9-
import toposort from 'toposort';
109

1110
import { APIv2Error } from './apiError.js';
1211
import { oraOptions } from './logger.js';
@@ -203,16 +202,42 @@ async function pushPage(
203202
}
204203
}
205204

206-
function byParentPage(
205+
function numericPosition(data: GuidesOrReferenceRequestSchema): number {
206+
const p = data.position;
207+
if (typeof p === 'number' && Number.isFinite(p)) return p;
208+
if (typeof p === 'string') {
209+
const n = Number(p);
210+
if (Number.isFinite(n)) return n;
211+
}
212+
213+
return Number.POSITIVE_INFINITY;
214+
}
215+
216+
function compareUploadPriority(
207217
left: PageMetadata<GuidesOrReferenceRequestSchema>,
208218
right: PageMetadata<GuidesOrReferenceRequestSchema>,
209-
) {
210-
return (right.data.parent?.uri ? 1 : 0) - (left.data.parent?.uri ? 1 : 0);
219+
): number {
220+
const leftPos = numericPosition(left.data);
221+
const rightPos = numericPosition(right.data);
222+
if (leftPos !== rightPos) {
223+
// Avoid `Infinity - Infinity` -> `NaN` which would poison sort when both sides do not have a
224+
// `position`.
225+
return leftPos < rightPos ? -1 : 1;
226+
}
227+
228+
return left.slug.localeCompare(right.slug, undefined, {
229+
numeric: true,
230+
sensitivity: 'base',
231+
});
211232
}
212233

213234
/**
214-
* Sorts files based on their `parent.uri` attribute. If a file has a `parent.uri` attribute,
215-
* it will be sorted after the file it references.
235+
* Sort files for uploading.
236+
*
237+
* Every page is uploaded after its parent as defined by `parent.uri` when present and when the
238+
* parent is in the same upload batch. Pages within these batches are then sorted by ascending
239+
* `position` when present; ties are broken by natural order of the page `slug` (numeric substrings
240+
* compare as numbers, e.g. `page-4` before `page-10`).
216241
*
217242
* @see {@link https://github.com/readmeio/rdme/pull/973}
218243
* @returns An array of sorted PageMetadata objects
@@ -226,17 +251,47 @@ function sortFiles(
226251
return bySlug;
227252
}, {});
228253

229-
const dependencies = Object.values(filesBySlug).reduce<
230-
[PageMetadata<GuidesOrReferenceRequestSchema>, PageMetadata<GuidesOrReferenceRequestSchema>][]
231-
>((edges, obj) => {
232-
if (obj.data.parent?.uri && filesBySlug[obj.data.parent.uri]) {
233-
edges.push([filesBySlug[obj.data.parent.uri], filesBySlug[obj.slug]]);
254+
const slugs = Object.keys(filesBySlug);
255+
const inDegree = new Map<string, number>();
256+
const childrenByParent = new Map<string, string[]>();
257+
258+
for (const obj of Object.values(filesBySlug)) {
259+
const parentKey = obj.data.parent?.uri;
260+
if (parentKey && filesBySlug[parentKey]) {
261+
if (!childrenByParent.has(parentKey)) {
262+
childrenByParent.set(parentKey, []);
263+
}
264+
265+
childrenByParent.get(parentKey)!.push(obj.slug);
266+
inDegree.set(obj.slug, (inDegree.get(obj.slug) ?? 0) + 1);
267+
} else {
268+
inDegree.set(obj.slug, 0);
234269
}
270+
}
271+
272+
const sorted: PageMetadata<GuidesOrReferenceRequestSchema>[] = [];
273+
const ready = slugs.filter(slug => (inDegree.get(slug) ?? 0) === 0);
274+
ready.sort((a, b) => compareUploadPriority(filesBySlug[a], filesBySlug[b]));
235275

236-
return edges;
237-
}, []);
276+
while (ready.length > 0) {
277+
const nextSlug = ready.shift()!;
278+
sorted.push(filesBySlug[nextSlug]);
279+
280+
for (const childSlug of childrenByParent.get(nextSlug) ?? []) {
281+
const newDeg = (inDegree.get(childSlug) ?? 0) - 1;
282+
inDegree.set(childSlug, newDeg);
283+
if (newDeg === 0) {
284+
ready.push(childSlug);
285+
ready.sort((a, b) => compareUploadPriority(filesBySlug[a], filesBySlug[b]));
286+
}
287+
}
288+
}
289+
290+
if (sorted.length !== files.length) {
291+
throw new Error('Cyclic dependency');
292+
}
238293

239-
return toposort.array(files, dependencies);
294+
return sorted;
240295
}
241296

242297
/**
@@ -289,7 +344,7 @@ export default async function syncPagePath(this: APIv2PageUploadCommands): Promi
289344
const sortedFiles =
290345
this.route === 'changelogs' || this.route === 'custom_pages'
291346
? (unsortedFiles as PageMetadata<PageRequestSchema<typeof this.route>>[])
292-
: sortFiles((unsortedFiles as PageMetadata<PageRequestSchema<typeof this.route>>[]).toSorted(byParentPage));
347+
: sortFiles(unsortedFiles as PageMetadata<GuidesOrReferenceRequestSchema>[]);
293348

294349
// push the files to ReadMe
295350
const rawResults: PromiseSettledResult<PushResult>[] = [];
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Page 1 (position #1)'
3+
category:
4+
uri: category-slug
5+
---
6+
7+
Body
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Page 10 (position #10)'
3+
category:
4+
uri: category-slug
5+
---
6+
7+
Body
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Page 2 (position #2)'
3+
category:
4+
uri: category-slug
5+
---
6+
7+
Body
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Page 3 (position #3)'
3+
category:
4+
uri: category-slug
5+
---
6+
7+
Body
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
title: 'Page 4 (position #4)'
3+
category:
4+
uri: category-slug
5+
---
6+
7+
Body
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: 'Page 1 (position #1)'
3+
category:
4+
uri: category-slug
5+
position: 1
6+
---
7+
8+
Body
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: 'Page 10 (position #10)'
3+
category:
4+
uri: category-slug
5+
position: 10
6+
---
7+
8+
Body
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: 'Page 2 (position #2)'
3+
category:
4+
uri: category-slug
5+
position: 2
6+
---
7+
8+
Body
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
title: 'Page 3 (position #3)'
3+
category:
4+
uri: category-slug
5+
position: 3
6+
---
7+
8+
Body

0 commit comments

Comments
 (0)