@aglyn/tenant-runtime 1.0.0-beta.198 → 1.0.0-beta.200

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.
@@ -32,12 +32,13 @@ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from
32
32
  * cache already holding exactly that.
33
33
  *
34
34
  * The other half of that argument was real and is answered rather than
35
- * dropped: `flipDueEntry` is a write, and nothing else publishes a content
36
- * entry, so a cache that stored a collection with a schedule still pending
37
- * would suppress the render that publishes it. `getPublishedCollectionSource`
38
- * therefore declines to STORE exactly those collections — see its `store`
39
- * predicate — which leaves scheduled publishing on the render window it has
40
- * always been on, and puts everything else on this TTL.
35
+ * dropped: `flipDueEntry` is a write, and a render is the backstop that
36
+ * publishes a content entry when the `apply-entry-schedules` beat has not
37
+ * (AGL-3340), so a cache that stored a collection with a schedule still
38
+ * pending would suppress the render that publishes it.
39
+ * `getPublishedCollectionSource` therefore declines to STORE exactly those
40
+ * collections — see its `store` predicate — which keeps that backstop on the
41
+ * render window, and puts everything else on this TTL.
41
42
  */ const COLLECTION_SOURCE_TTL_SECONDS = PUBLISHED_SITE_DATA_TTL_SECONDS;
42
43
  /**
43
44
  * Resolve a public content-collection slug (AGL-954). Commerce's product
@@ -208,12 +209,30 @@ import { PUBLISHED_SITE_DATA_TTL_SECONDS, tenantDataTag, withRenderCache } from
208
209
  }
209
210
  /**
210
211
  * A scheduled entry still waiting on its time: not due yet, and not refused.
211
- * Nothing but a render publishes a content entry, so a cache that stored a
212
- * read holding one would withhold the render that notices it come due — see
213
- * {@link LiveEntriesRead.pendingSchedule}.
212
+ * A render is the backstop that publishes a content entry the beat has not
213
+ * reached, so a cache that stored a read holding one would withhold the render
214
+ * that notices it come due — see {@link LiveEntriesRead.pendingSchedule}.
214
215
  */ export function isPendingScheduled(value) {
215
216
  return value['status'] === 'scheduled' && !scheduleAlreadyRefused(value) && !isDueScheduled(value);
216
217
  }
218
+ /**
219
+ * When this entry's schedule comes due, in epoch ms — or null when it holds
220
+ * no schedule to act on: not `scheduled`, already refused, or carrying no
221
+ * `publishAt` (AGL-3340).
222
+ *
223
+ * The beat orders its probe by this instant. A refused schedule keeps
224
+ * `status: 'scheduled'` forever (see {@link ENTRY_SCHEDULE_SKIPPED}), so
225
+ * reading one as due would make every beat re-probe for work that will never
226
+ * be done. Seconds, like {@link isDueScheduled}, so the two agree about the
227
+ * instant an entry comes due.
228
+ */ export function entryScheduleDueAtMs(value) {
229
+ var _value_publishAt;
230
+ if (value['status'] !== 'scheduled' || scheduleAlreadyRefused(value)) {
231
+ return null;
232
+ }
233
+ const seconds = (_value_publishAt = value['publishAt']) == null ? void 0 : _value_publishAt.seconds;
234
+ return typeof seconds === 'number' ? seconds * 1000 : null;
235
+ }
217
236
  export async function scheduledPublishingPermission(hostId) {
218
237
  try {
219
238
  var _this;
@@ -232,7 +251,10 @@ export async function scheduledPublishingPermission(hostId) {
232
251
  /**
233
252
  * Scheduled entries (AGL-123) go live lazily like AGL-61: a due
234
253
  * `publishAt` counts as published for this render, and the doc is flipped
235
- * to `published` fail-open so the state becomes durable.
254
+ * to `published` fail-open so the state becomes durable. The tenant's
255
+ * `apply-entry-schedules` beat makes the same flip at the entry's time and
256
+ * drops the pages that list it (AGL-3340); this render-time half stays as the
257
+ * backstop for a beat that is down or has not reached the entry yet.
236
258
  *
237
259
  * PLAN GATE (AGL-471). `scheduledPublishing` is a Business entitlement, and
238
260
  * until now nothing on the entry path checked it: the console let any plan
@@ -263,28 +285,54 @@ export async function scheduledPublishingPermission(hostId) {
263
285
  * subsequent render.
264
286
  *
265
287
  * Both writes fail open: an error leaves today's state, and the next render
266
- * retries.
267
- */ function flipDueEntry(docRef, value, permission) {
268
- if (value['status'] !== 'scheduled') return;
269
- if (scheduleAlreadyRefused(value)) return;
288
+ * retries. A render does not wait for them (`void`); the beat does, because
289
+ * it drops pages only for a flip that landed.
290
+ *
291
+ * `precondition` is the beat's: it read the entry some milliseconds earlier,
292
+ * and an author rescheduling it in between must win. A render passes none —
293
+ * it writes exactly what it just rendered.
294
+ */ async function flipDueEntry(docRef, value, permission, precondition) {
295
+ if (value['status'] !== 'scheduled') return 'unchanged';
296
+ if (scheduleAlreadyRefused(value)) return 'unchanged';
270
297
  // `unresolved` writes NOTHING. It withholds this render and leaves the
271
298
  // schedule exactly as it found it, so a later render can still publish it.
272
- if (permission === 'unresolved') return;
273
- if (permission === 'refused') {
274
- if (!isDueScheduled(value)) return;
275
- docRef.update({
276
- scheduleStatus: ENTRY_SCHEDULE_SKIPPED
277
- }).catch((error)=>console.error(error));
278
- return;
279
- }
280
- // The console's sort key follows the flip (AGL-3323). It already held
281
- // `publishAt` while the entry waited, and `publishedAt` takes that same
282
- // instant, so the entry keeps its place in the console's Published order.
283
- docRef.update({
299
+ if (permission === 'unresolved') return 'unchanged';
300
+ if (!isDueScheduled(value)) return 'unchanged';
301
+ const update = permission === 'refused' ? {
302
+ scheduleStatus: ENTRY_SCHEDULE_SKIPPED
303
+ } : // `publishAt` while the entry waited, and `publishedAt` takes that
304
+ // same instant, so the entry keeps its place in the console's
305
+ // Published order.
306
+ {
284
307
  status: 'published',
285
308
  publishedAt: value['publishAt'],
286
309
  [ENTRY_PUBLISH_SORT_FIELD]: value['publishAt']
287
- }).catch((error)=>console.error(error));
310
+ };
311
+ try {
312
+ if (precondition) await docRef.update(update, precondition);
313
+ else await docRef.update(update);
314
+ return permission === 'refused' ? 'refused' : 'published';
315
+ } catch (error) {
316
+ console.error(error);
317
+ return 'failed';
318
+ }
319
+ }
320
+ /**
321
+ * Apply one due entry schedule outside a render (AGL-3340) — the executor the
322
+ * `apply-entry-schedules` beat runs, and the same one a render runs, so the
323
+ * two cannot come to disagree about the plan gate or the fields a flip
324
+ * writes.
325
+ *
326
+ * Asks the plan only for an entry that is actually due, as every render path
327
+ * does. Guarded on the snapshot's own `updateTime`: a schedule edited after
328
+ * the beat read it is left for the next beat, which reads it fresh.
329
+ */ export async function applyDueEntrySchedule(options) {
330
+ const value = options.entry.data();
331
+ if (!value || !isDueScheduled(value)) return 'unchanged';
332
+ const permission = await scheduledPublishingPermission(options.hostId);
333
+ return flipDueEntry(options.entry.ref, value, permission, options.entry.updateTime ? {
334
+ lastUpdateTime: options.entry.updateTime
335
+ } : undefined);
288
336
  }
289
337
  /**
290
338
  * The fields a LISTING read of an entry needs — the field mask on the query
@@ -340,9 +388,9 @@ export async function scheduledPublishingPermission(hostId) {
340
388
  *
341
389
  * Read by its own query rather than taken from the dated page below, because
342
390
  * a schedule is invisible to that page: scheduling writes `publishAt` and
343
- * never `publishedAt`, and `flipDueEntry` during a render is the only thing
344
- * that publishes one. A schedule the read misses is a post that never goes
345
- * out. The set is small by nature — a hundred pending schedules on one
391
+ * never `publishedAt`, and when the beat has not reached one, `flipDueEntry`
392
+ * during a render is the only thing that publishes it. A schedule the read
393
+ * misses is then a post that does not go out. The set is small by nature — a hundred pending schedules on one
346
394
  * collection is already an unusual editorial calendar.
347
395
  */ const SCHEDULED_SOURCE_MAX = 100;
348
396
  /** Everything a public listing may show, before any ordering. */ function liveEntriesBase(entriesRef) {
@@ -444,7 +492,7 @@ export async function scheduledPublishingPermission(hostId) {
444
492
  return docs.filter((entryDoc)=>isLive(entryDoc.data(), permission)).map((entryDoc)=>{
445
493
  var _value_title, _value_slug, _value_publishedAt, _value_publishedAt1;
446
494
  const value = entryDoc.data();
447
- flipDueEntry(entryDoc.ref, value, permission);
495
+ void flipDueEntry(entryDoc.ref, value, permission);
448
496
  return _extends({
449
497
  $id: entryDoc.id,
450
498
  title: (_value_title = value['title']) != null ? _value_title : entryDoc.id,
@@ -478,7 +526,7 @@ export async function scheduledPublishingPermission(hostId) {
478
526
  // re-reading the org on each one.
479
527
  if (permission !== 'allowed') {
480
528
  for (const entryDoc of due){
481
- flipDueEntry(entryDoc.ref, entryDoc.data(), permission);
529
+ void flipDueEntry(entryDoc.ref, entryDoc.data(), permission);
482
530
  }
483
531
  }
484
532
  // Measured on the RAW docs, before the liveness filter (AGL-1516): a
@@ -553,7 +601,7 @@ export async function scheduledPublishingPermission(hostId) {
553
601
  const permission = due.length ? await scheduledPublishingPermission(options.hostId) : 'allowed';
554
602
  if (permission !== 'allowed') {
555
603
  for (const entryDoc of due){
556
- flipDueEntry(entryDoc.ref, entryDoc.data(), permission);
604
+ void flipDueEntry(entryDoc.ref, entryDoc.data(), permission);
557
605
  }
558
606
  }
559
607
  const entries = toLiveEntries(docs, permission);
@@ -624,13 +672,13 @@ export async function scheduledPublishingPermission(hostId) {
624
672
  read: ()=>readPublishedCollectionSource(options),
625
673
  // A collection with a schedule still pending is SERVED but not STORED.
626
674
  //
627
- // No beat publishes a content entry — `flipDueEntry` during a render is
628
- // the whole mechanism — so storing this source would suppress the very
629
- // renders that would have noticed the entry coming due, and the post
630
- // would wait out the TTL rather than land at its time. Declining to
631
- // store leaves those collections exactly as uncached as they were
632
- // before this cache existed, which is the only cost this cache is not
633
- // allowed to reduce.
675
+ // The beat publishes a due entry and drops this tag (AGL-3340); when
676
+ // the beat is down, `flipDueEntry` during a render is what publishes
677
+ // it, so storing this source would suppress the very renders that would
678
+ // have noticed the entry coming due, and the post would wait out the
679
+ // TTL rather than land at its time. Declining to store leaves those
680
+ // collections exactly as uncached as they were before this cache
681
+ // existed, which is the only cost this cache is not allowed to reduce.
634
682
  //
635
683
  // Also refuses a collection that resolved to nothing, for the reason
636
684
  // `withRenderCache` states about negatives generally: a slug that
@@ -912,19 +960,19 @@ async function readPublishedCollectionSource(options) {
912
960
  // something is due, and record the refusal on its own pass because a
913
961
  // refused entry never becomes the `entryDoc` below (AGL-471).
914
962
  //
915
- // A PREVIEW RENDER SKIPS BOTH WRITES (AGL-3205). `flipDueEntry` is the
916
- // entire publishing mechanism for a content entry, and it is a mechanism
917
- // that belongs to the PUBLIC render: a preview is one person looking at
918
- // one post through a link, and it must not be able to publish it, nor to
919
- // burn its schedule with the terminal refusal marker. Skipping the writes
920
- // costs nothing — the next public render asks the same questions and
921
- // writes the same answers — and it is what keeps "nothing but a render
922
- // publishes a content entry" true of renders anybody can reach.
963
+ // A PREVIEW RENDER SKIPS BOTH WRITES (AGL-3205). `flipDueEntry` is how a
964
+ // content entry is published — by the beat, or by a render as its
965
+ // backstop — and the render half belongs to the PUBLIC render: a preview
966
+ // is one person looking at one post through a link, and it must not be
967
+ // able to publish it, nor to burn its schedule with the terminal refusal
968
+ // marker. Skipping the writes costs nothing — the beat or the next public
969
+ // render asks the same questions and writes the same answers — and it is
970
+ // what keeps a preview link from publishing anything.
923
971
  const dueHere = entryQuery.docs.filter((docSnapshot)=>isDueScheduled(docSnapshot.data()));
924
972
  const permission = dueHere.length ? await scheduledPublishingPermission(hostId) : 'allowed';
925
973
  if (permission !== 'allowed' && !preview) {
926
974
  for (const docSnapshot of dueHere){
927
- flipDueEntry(docSnapshot.ref, docSnapshot.data(), permission);
975
+ void flipDueEntry(docSnapshot.ref, docSnapshot.data(), permission);
928
976
  }
929
977
  }
930
978
  /**
@@ -944,7 +992,7 @@ async function readPublishedCollectionSource(options) {
944
992
  if (entryDoc) {
945
993
  var _value_title, _value_body, _value_publishedAt, _value_publishedAt1;
946
994
  const value = entryDoc.data();
947
- if (!preview) flipDueEntry(entryDoc.ref, value, permission);
995
+ if (!preview) void flipDueEntry(entryDoc.ref, value, permission);
948
996
  if (preview && !isLive(value, permission)) {
949
997
  var _value_status;
950
998
  var _value_publishAt;