@aglyn/tenant-runtime 1.0.0-beta.147 → 1.0.0-beta.149
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/package.json +3 -3
- package/src/lib/collection-fallback-nodes.d.ts +6 -1
- package/src/lib/collection-fallback-nodes.js +23 -4
- package/src/lib/collection-fallback-nodes.js.map +1 -1
- package/src/lib/compose-collection-page.js +8 -4
- package/src/lib/compose-collection-page.js.map +1 -1
- package/src/lib/get-collection-content.d.ts +72 -10
- package/src/lib/get-collection-content.js +174 -77
- package/src/lib/get-collection-content.js.map +1 -1
|
@@ -452,34 +452,6 @@ export async function scheduledPublishingPermission(hostId) {
|
|
|
452
452
|
return ((_ref = (_b_publishedAt = b.publishedAt) == null ? void 0 : _b_publishedAt.seconds) != null ? _ref : 0) - ((_ref1 = (_a_publishedAt = a.publishedAt) == null ? void 0 : _a_publishedAt.seconds) != null ? _ref1 : 0);
|
|
453
453
|
});
|
|
454
454
|
}
|
|
455
|
-
/**
|
|
456
|
-
* How many entries are live in the whole collection (AGL-3213).
|
|
457
|
-
*
|
|
458
|
-
* The aggregation counts `published`, which `isLive` admits unconditionally,
|
|
459
|
-
* and the live schedules are added from the docs already in hand — a due
|
|
460
|
-
* schedule is still stored as `scheduled` while this runs, because
|
|
461
|
-
* `flipDueEntry` writes behind the render, so neither half can count it twice.
|
|
462
|
-
*
|
|
463
|
-
* Falls back to the size of the read on failure. A pager that overstates by a
|
|
464
|
-
* page is a page a reader can see is empty; a pager that throws is a listing
|
|
465
|
-
* that does not render.
|
|
466
|
-
*/ async function countLiveEntries(entriesRef, docs, permission, fallback) {
|
|
467
|
-
try {
|
|
468
|
-
var _ref;
|
|
469
|
-
var _published_data;
|
|
470
|
-
const published = await entriesRef.where('status', '==', 'published').count().get();
|
|
471
|
-
const counted = Number((_ref = (_published_data = published.data()) == null ? void 0 : _published_data.count) != null ? _ref : Number.NaN);
|
|
472
|
-
if (!Number.isFinite(counted)) return fallback;
|
|
473
|
-
const liveSchedules = docs.filter((entryDoc)=>{
|
|
474
|
-
const value = entryDoc.data();
|
|
475
|
-
return value['status'] === 'scheduled' && isLive(value, permission);
|
|
476
|
-
}).length;
|
|
477
|
-
return counted + liveSchedules;
|
|
478
|
-
} catch (error) {
|
|
479
|
-
console.error(error);
|
|
480
|
-
return fallback;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
455
|
/**
|
|
484
456
|
* Fetches a collection's live entries (newest first), shared by the route
|
|
485
457
|
* loader and the compose-time Collection entries block (AGL-551).
|
|
@@ -507,42 +479,82 @@ export async function scheduledPublishingPermission(hostId) {
|
|
|
507
479
|
return {
|
|
508
480
|
entries,
|
|
509
481
|
reachedBound,
|
|
510
|
-
pendingSchedule
|
|
511
|
-
totalLive: reachedBound ? await countLiveEntries(entriesRef, docs, permission, entries.length) : entries.length
|
|
482
|
+
pendingSchedule
|
|
512
483
|
};
|
|
513
484
|
}
|
|
514
485
|
/**
|
|
515
|
-
* One page of a listing that starts PAST the cached read (AGL-3213)
|
|
486
|
+
* One page of a listing that starts PAST the cached read (AGL-3213),
|
|
487
|
+
* addressed by the document it continues from rather than by a position
|
|
488
|
+
* (AGL-3219).
|
|
516
489
|
*
|
|
517
490
|
* Uncached on purpose, and it is the only read on the collection path that is.
|
|
518
|
-
* The cached source exists because `/blog`, every
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
|
|
491
|
+
* The cached source exists because `/blog`, every listing address, the feed
|
|
492
|
+
* and every "Latest posts" rail want the same first hundred entries
|
|
493
|
+
* (AGL-1302); a page past that bound wants ten entries nobody else is asking
|
|
494
|
+
* for, and its own ISR entry is already the cache for them.
|
|
495
|
+
*
|
|
496
|
+
* ## Why a cursor, and not the offset this replaces
|
|
497
|
+
*
|
|
498
|
+
* `.offset(n)` asks for a POSITION, and a listing takes its inserts at the
|
|
499
|
+
* head, so every publish moves every position by one. The head pages and this
|
|
500
|
+
* read are cached under different policies and revalidated by different
|
|
501
|
+
* triggers — the publish fan-out refreshes the head eagerly and deliberately
|
|
502
|
+
* stops there — so the two sides were routinely describing the collection as
|
|
503
|
+
* it stood at two different moments, and the seam between them repeated an
|
|
504
|
+
* entry or hid one for as long as the slower side lagged. `v1.0.0-beta.147`
|
|
505
|
+
* shipped and `/changelog` showed `v1-0-0-beta-36` as both the last entry of
|
|
506
|
+
* page 10 and the first of page 11.
|
|
507
|
+
*
|
|
508
|
+
* `startAfter(doc)` asks a question whose answer does not move: what follows
|
|
509
|
+
* THIS entry. Publish a hundred entries at the head and this page returns the
|
|
510
|
+
* same ten. The seam cannot drift because there is no longer a number on
|
|
511
|
+
* either side of it to disagree about.
|
|
512
|
+
*
|
|
513
|
+
* `offset` also billed every document it skipped; a cursor bills none of them.
|
|
514
|
+
*/ async function readCollectionListingPage(options) {
|
|
528
515
|
try {
|
|
529
516
|
const collectionDoc = await findContentCollection(options.hostId, options.collectionSlug);
|
|
530
517
|
if (!collectionDoc) return null;
|
|
531
518
|
const entriesRef = collectionDoc.ref.collection('entries');
|
|
532
|
-
const
|
|
533
|
-
|
|
519
|
+
const cursorId = options.before || options.after;
|
|
520
|
+
if (!cursorId) return null;
|
|
521
|
+
// A direct get, not a `where('slug', ...)` query: the cursor IS the
|
|
522
|
+
// document name, which is the second field the read orders on, so the
|
|
523
|
+
// snapshot it needs is one document rather than an index lookup.
|
|
524
|
+
const cursorDoc = await entriesRef.doc(cursorId).get();
|
|
525
|
+
// A cursor naming a document that no longer exists — deleted, or a URL
|
|
526
|
+
// someone kept — cannot be positioned against. Null sends the caller back
|
|
527
|
+
// to the cached head, which is a page the reader can act on.
|
|
528
|
+
if (!cursorDoc.exists) return null;
|
|
529
|
+
// Backwards is the same query read the other way up, so both directions
|
|
530
|
+
// rest on an index the project already deploys: `(status, publishedAt)`
|
|
531
|
+
// exists ascending and descending both.
|
|
532
|
+
const backwards = Boolean(options.before);
|
|
533
|
+
const snapshot = await liveEntriesBase(entriesRef).orderBy('publishedAt', backwards ? 'asc' : 'desc').orderBy('__name__', backwards ? 'asc' : 'desc').startAfter(cursorDoc)// One more than the page: the extra row is the whole of "is there
|
|
534
|
+
// another page", and it replaces a `count()` over the collection.
|
|
535
|
+
.limit(options.limit + 1).get();
|
|
536
|
+
const hasMore = snapshot.docs.length > options.limit;
|
|
537
|
+
const pageDocs = snapshot.docs.slice(0, options.limit);
|
|
538
|
+
// Read backwards, the newest of the page came back last.
|
|
539
|
+
const docs = backwards ? [
|
|
540
|
+
...pageDocs
|
|
541
|
+
].reverse() : pageDocs;
|
|
542
|
+
const due = docs.filter((entryDoc)=>isDueScheduled(entryDoc.data()));
|
|
534
543
|
const permission = due.length ? await scheduledPublishingPermission(options.hostId) : 'allowed';
|
|
535
544
|
if (permission !== 'allowed') {
|
|
536
545
|
for (const entryDoc of due){
|
|
537
546
|
flipDueEntry(entryDoc.ref, entryDoc.data(), permission);
|
|
538
547
|
}
|
|
539
548
|
}
|
|
540
|
-
const entries = toLiveEntries(
|
|
549
|
+
const entries = toLiveEntries(docs, permission);
|
|
541
550
|
// The byline reads `authorName`, which a record-backed author only has
|
|
542
551
|
// once resolved — the cached source does this for the entries it holds,
|
|
543
552
|
// and a windowed page holds entries it never saw (AGL-2486).
|
|
544
553
|
await attachEntryAuthors(options.hostId, entries);
|
|
545
|
-
return
|
|
554
|
+
return {
|
|
555
|
+
entries,
|
|
556
|
+
hasMore
|
|
557
|
+
};
|
|
546
558
|
} catch (error) {
|
|
547
559
|
// Fail open to the cached head rather than to a 500: the caller keeps
|
|
548
560
|
// whatever it already had, which is a page the reader has seen before
|
|
@@ -551,6 +563,37 @@ export async function scheduledPublishingPermission(hostId) {
|
|
|
551
563
|
return null;
|
|
552
564
|
}
|
|
553
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* The cursor a retired `/{collection}/page/{n}` address redirects onto
|
|
568
|
+
* (AGL-3219).
|
|
569
|
+
*
|
|
570
|
+
* This is the ONE place a position is still resolved, and it is deliberately
|
|
571
|
+
* the only one: a 301 is served once, the reader lands on an address that
|
|
572
|
+
* cannot drift afterwards, and whatever the position meant at that instant is
|
|
573
|
+
* the page they would have got anyway. Everything downstream is cursors.
|
|
574
|
+
*
|
|
575
|
+
* Keys only — `select()` with no fields asks Firestore for document names and
|
|
576
|
+
* nothing else — so the skipped documents are billed at the cheapest rate the
|
|
577
|
+
* offset can be had for. Returns `''` when the position is past the end,
|
|
578
|
+
* which is a 404 rather than a redirect to nowhere.
|
|
579
|
+
*/ export async function resolveCollectionPageCursor(options) {
|
|
580
|
+
const skip = (options.page - 1) * options.perPage - 1;
|
|
581
|
+
if (!Number.isFinite(skip) || skip < 0) return '';
|
|
582
|
+
try {
|
|
583
|
+
var _ref;
|
|
584
|
+
var _snapshot_docs_;
|
|
585
|
+
const collectionDoc = await findContentCollection(options.hostId, options.collectionSlug);
|
|
586
|
+
if (!collectionDoc) return '';
|
|
587
|
+
const snapshot = await collectionDoc.ref.collection('entries').where('status', 'in', [
|
|
588
|
+
'published',
|
|
589
|
+
'scheduled'
|
|
590
|
+
]).select().orderBy('publishedAt', 'desc').orderBy('__name__', 'desc').offset(skip).limit(1).get();
|
|
591
|
+
return (_ref = (_snapshot_docs_ = snapshot.docs[0]) == null ? void 0 : _snapshot_docs_.id) != null ? _ref : '';
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.error(error);
|
|
594
|
+
return '';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
554
597
|
/**
|
|
555
598
|
* Published entries + category taxonomy for a collection resolved by slug —
|
|
556
599
|
* the data source of the Collection entries block on arbitrary screens
|
|
@@ -597,11 +640,10 @@ async function readPublishedCollectionSource(options) {
|
|
|
597
640
|
collection: null,
|
|
598
641
|
entries: [],
|
|
599
642
|
categories: [],
|
|
600
|
-
reachedBound: false
|
|
601
|
-
totalLive: 0
|
|
643
|
+
reachedBound: false
|
|
602
644
|
};
|
|
603
645
|
}
|
|
604
|
-
const { entries, reachedBound, pendingSchedule
|
|
646
|
+
const { entries, reachedBound, pendingSchedule } = await listLiveEntries(collectionDoc.ref.collection('entries'), options.hostId);
|
|
605
647
|
// The compose-time source feeds the Collection entries block, whose byline
|
|
606
648
|
// reads `authorName` — so a record-backed author has to be resolved here
|
|
607
649
|
// too, or the block prints nothing for the entries a list page shows
|
|
@@ -612,8 +654,7 @@ async function readPublishedCollectionSource(options) {
|
|
|
612
654
|
collection: mapCollectionDoc(collectionDoc, options.collectionSlug),
|
|
613
655
|
entries,
|
|
614
656
|
categories: mapCollectionCategories(collectionDoc.get('categories')),
|
|
615
|
-
reachedBound
|
|
616
|
-
totalLive
|
|
657
|
+
reachedBound
|
|
617
658
|
}, pendingSchedule ? {
|
|
618
659
|
pendingSchedule: true
|
|
619
660
|
} : {});
|
|
@@ -623,8 +664,7 @@ async function readPublishedCollectionSource(options) {
|
|
|
623
664
|
collection: null,
|
|
624
665
|
entries: [],
|
|
625
666
|
categories: [],
|
|
626
|
-
reachedBound: false
|
|
627
|
-
totalLive: 0
|
|
667
|
+
reachedBound: false
|
|
628
668
|
};
|
|
629
669
|
}
|
|
630
670
|
}
|
|
@@ -672,6 +712,7 @@ async function readPublishedCollectionSource(options) {
|
|
|
672
712
|
});
|
|
673
713
|
}
|
|
674
714
|
if (perPage && perPage > 0) {
|
|
715
|
+
var _ref1;
|
|
675
716
|
/*
|
|
676
717
|
* The total is the COLLECTION's, not the read's (AGL-3213).
|
|
677
718
|
*
|
|
@@ -687,15 +728,49 @@ async function readPublishedCollectionSource(options) {
|
|
|
687
728
|
* query and a `(categoryId, status, publishedAt DESC)` index; until then
|
|
688
729
|
* a category of a very large collection is capped, and says so by
|
|
689
730
|
* agreeing with what it shows.
|
|
690
|
-
*/ const
|
|
691
|
-
const
|
|
692
|
-
|
|
731
|
+
*/ const windowStart = Number(listing == null ? void 0 : listing.windowStart);
|
|
732
|
+
const windowed = Number.isFinite(windowStart) && windowStart > 0;
|
|
733
|
+
/*
|
|
734
|
+
* The cursors, which are the pager's real answer (AGL-3219).
|
|
735
|
+
*
|
|
736
|
+
* `data.entries` is either EXACTLY this page — a cursor read — or the
|
|
737
|
+
* whole cached head, which every listing address shares and each slices
|
|
738
|
+
* its own page out of. So the page's own last entry is at the end of the
|
|
739
|
+
* array in the first case and at `page * perPage - 1` in the second, and
|
|
740
|
+
* the cursor is that entry's document id.
|
|
741
|
+
*/ const pageEnd = windowed ? data.entries.length : page * perPage;
|
|
742
|
+
const last = data.entries[pageEnd - 1];
|
|
743
|
+
const first = data.entries[windowed ? 0 : (page - 1) * perPage];
|
|
744
|
+
/*
|
|
745
|
+
* Is there an older page?
|
|
746
|
+
*
|
|
747
|
+
* A cursor page KNOWS, because its read asked for one more entry than it
|
|
748
|
+
* needed and the extra row came back. The head has to reason: either it
|
|
749
|
+
* is holding more entries than this page shows, or it is holding all it
|
|
750
|
+
* could read and the read stopped at its own bound, which is the one
|
|
751
|
+
* thing `entries.length` can never tell you about the collection.
|
|
752
|
+
*/ const hasMore = (_ref1 = listing == null ? void 0 : listing.cursorHasMore) != null ? _ref1 : data.entries.length > pageEnd || Boolean(listing == null ? void 0 : listing.reachedBound) && !routedCategory;
|
|
753
|
+
/*
|
|
754
|
+
* The deprecated total, stated ONLY where it can be true.
|
|
755
|
+
*
|
|
756
|
+
* A collection that fits inside one read can be counted, and a template
|
|
757
|
+
* binding `{{pagination.totalPages}}` keeps rendering the number it
|
|
758
|
+
* always did. Past the bound there is no honest total — the count and
|
|
759
|
+
* the listing were reading the collection at two different moments, which
|
|
760
|
+
* is what made the seam repeat an entry — so it is left absent rather
|
|
761
|
+
* than asserted. A routed category counts its own narrowed set, which is
|
|
762
|
+
* bounded by the same head and therefore countable.
|
|
763
|
+
*/ const countable = routedCategory || !(listing == null ? void 0 : listing.reachedBound);
|
|
764
|
+
const totalEntries = countable ? data.entries.length : undefined;
|
|
693
765
|
data.pagination = _extends({
|
|
694
766
|
page,
|
|
695
767
|
perPage,
|
|
768
|
+
nextCursor: hasMore && (last == null ? void 0 : last.$id) ? last.$id : '',
|
|
769
|
+
prevCursor: page > 1 && (first == null ? void 0 : first.$id) ? first.$id : ''
|
|
770
|
+
}, totalEntries === undefined ? {} : {
|
|
696
771
|
totalEntries,
|
|
697
772
|
totalPages: collectionTotalPages(totalEntries, perPage)
|
|
698
|
-
},
|
|
773
|
+
}, windowed ? {
|
|
699
774
|
windowStart
|
|
700
775
|
} : {});
|
|
701
776
|
}
|
|
@@ -737,6 +812,7 @@ async function readPublishedCollectionSource(options) {
|
|
|
737
812
|
// An ENTRY route stays where it was: it reads ONE document by slug and
|
|
738
813
|
// has nothing to share.
|
|
739
814
|
if (!entrySlug) {
|
|
815
|
+
var _ref, _options_after, _options_categorySlug;
|
|
740
816
|
const source = await getPublishedCollectionSource({
|
|
741
817
|
hostId,
|
|
742
818
|
collectionSlug
|
|
@@ -769,30 +845,51 @@ async function readPublishedCollectionSource(options) {
|
|
|
769
845
|
* the same head, and a window read of the whole collection would hand
|
|
770
846
|
* it ten entries of which any number may belong to another category.
|
|
771
847
|
*/ const { page = 1, perPage } = options;
|
|
848
|
+
const cursor = ((_ref = (_options_after = options.after) != null ? _options_after : options.before) != null ? _ref : '').trim();
|
|
772
849
|
let windowStart = 0;
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
850
|
+
let cursored = null;
|
|
851
|
+
/*
|
|
852
|
+
* A cursor address is served by its own read (AGL-3219). Everything
|
|
853
|
+
* reachable without one comes out of the shared source, so the pages a
|
|
854
|
+
* reader actually visits stay free.
|
|
855
|
+
*
|
|
856
|
+
* Only the unfiltered listing: a category route narrows in memory over
|
|
857
|
+
* the same head, and a cursor read of the whole collection would hand it
|
|
858
|
+
* ten entries of which any number belong to another category.
|
|
859
|
+
*/ if (perPage && perPage > 0 && cursor && !((_options_categorySlug = options.categorySlug) != null ? _options_categorySlug : '').trim()) {
|
|
860
|
+
cursored = await readCollectionListingPage(_extends({
|
|
861
|
+
hostId,
|
|
862
|
+
collectionSlug
|
|
863
|
+
}, options.before ? {
|
|
864
|
+
before: options.before
|
|
865
|
+
} : {
|
|
866
|
+
after: cursor
|
|
867
|
+
}, {
|
|
868
|
+
limit: perPage
|
|
869
|
+
}));
|
|
870
|
+
// Null means the cursor read failed, or named a document that is
|
|
871
|
+
// gone. The cached head is the better answer than an empty page: the
|
|
872
|
+
// reader sees the newest entries rather than nothing at all.
|
|
873
|
+
if (cursored) {
|
|
874
|
+
data.entries = cursored.entries;
|
|
875
|
+
/*
|
|
876
|
+
* `entries` is now EXACTLY this page, and both windowing sites
|
|
877
|
+
* slice `[(page - 1) * perPage, …)`. Declaring the window to start
|
|
878
|
+
* at that same offset makes their subtraction come out at zero, so
|
|
879
|
+
* they take the page whole.
|
|
880
|
+
*
|
|
881
|
+
* Both sides of the subtraction are built from the same `page`, so
|
|
882
|
+
* a hand-edited counter in a URL cancels itself out: the label is
|
|
883
|
+
* wrong and the entries are still this cursor's page.
|
|
884
|
+
*/ windowStart = (page - 1) * perPage;
|
|
790
885
|
}
|
|
791
886
|
}
|
|
792
|
-
applyCategoryAndPagination(data, options, {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
}
|
|
887
|
+
applyCategoryAndPagination(data, options, _extends({
|
|
888
|
+
windowStart,
|
|
889
|
+
reachedBound: source.reachedBound
|
|
890
|
+
}, cursored ? {
|
|
891
|
+
cursorHasMore: cursored.hasMore
|
|
892
|
+
} : {}));
|
|
796
893
|
return data;
|
|
797
894
|
}
|
|
798
895
|
const collectionDoc = await findContentCollection(hostId, collectionSlug);
|