@plinth-music/cli 0.23.0 → 0.23.1
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/CHANGELOG.md +39 -0
- package/dist/build-stamp.json +2 -2
- package/dist/cli.js +427 -55
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@plinth-music/cli`. Grouped by what a user notices, not by PR.
|
|
4
4
|
|
|
5
|
+
## 0.23.1 - 2026-09-29
|
|
6
|
+
|
|
7
|
+
Two code PRs since `0.23.0` (`v0.23.0..0dd34dc`: #212, #213; #209, #210 and #211 docs).
|
|
8
|
+
A patch: fixes only, no new command or sync surface.
|
|
9
|
+
|
|
10
|
+
⚠️ **A file you delete from the mirror stays deleted** (#213). A poll landing in the
|
|
11
|
+
second or so between your `rm` and the daemon's delete hold could write the file back,
|
|
12
|
+
and the delete was then dropped; four of seven documents came back this way on 26 Sept.
|
|
13
|
+
The daemon now leaves a path it saw you remove alone for five minutes, and waits one
|
|
14
|
+
poll before restoring a file that is missing for a reason it has not seen yet. A file
|
|
15
|
+
missing for any other reason is still restored, about one poll later than before.
|
|
16
|
+
|
|
17
|
+
⚠️ **One slow calendar no longer freezes every calendar view** (#212). A calendar whose
|
|
18
|
+
read fails or times out now shows as not read, with no entries, until the next refresh.
|
|
19
|
+
Before, the whole refresh failed and every `views/calendar/` file kept its old contents.
|
|
20
|
+
A read that did not complete is described as incomplete, not as Google refusing access.
|
|
21
|
+
|
|
22
|
+
**The daemon stops re-downloading what has not changed** (#213). Since 0.22.0 every
|
|
23
|
+
poll fetched every memory fact in full, left `_freshness.md` reporting memory as
|
|
24
|
+
`unknown` / `incomplete_apply`, and re-fetched the extracted text of every PDF. Both now
|
|
25
|
+
happen once. Feeds ask from the server's high-water mark instead of re-reading their
|
|
26
|
+
last page; a full read from the cursor still runs at boot, on `plinth sync` and once an
|
|
27
|
+
hour, so nothing is missed. On the release commit, over 47 minutes after the first pass:
|
|
28
|
+
no full memory-fact downloads, no text re-fetches, no inconsistent trails.
|
|
29
|
+
|
|
30
|
+
**Each calendar view says what it covers** (#212). A view holds only its window and now
|
|
31
|
+
says so, naming the Plinth tool `list_calendar_events` for any other date. A truncated
|
|
32
|
+
read says why it was cut short.
|
|
33
|
+
|
|
34
|
+
**What is not proven.**
|
|
35
|
+
|
|
36
|
+
- The delete fix was driven live on the release commit (a delete inside a poll, and a
|
|
37
|
+
held batch left 13 minutes past the window: neither came back). A delete within about
|
|
38
|
+
1.6 s of the lid closing has one exposed poll on wake.
|
|
39
|
+
- The calendar changes were not exercised live: the test workspace has no mapped
|
|
40
|
+
calendar. They are covered by tests and are checked on a real workspace after release.
|
|
41
|
+
- A sync request that hangs can still stall the poll loop for minutes (seen once, 4 min
|
|
42
|
+
7 s, in the release check). Pre-existing; not fixed here.
|
|
43
|
+
|
|
5
44
|
## 0.23.0 - 2026-09-23
|
|
6
45
|
|
|
7
46
|
One code PR since `0.22.0` (`v0.22.0..f3334fb`, #207; #204, #205 and #206 docs). A
|
package/dist/build-stamp.json
CHANGED
package/dist/cli.js
CHANGED
|
@@ -7584,7 +7584,7 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
7584
7584
|
// package.json
|
|
7585
7585
|
var package_default = {
|
|
7586
7586
|
name: "@plinth-music/cli",
|
|
7587
|
-
version: "0.23.
|
|
7587
|
+
version: "0.23.1",
|
|
7588
7588
|
description: "Plinth capstone sync daemon and CLI",
|
|
7589
7589
|
type: "module",
|
|
7590
7590
|
private: false,
|
|
@@ -22168,7 +22168,8 @@ var TombstoneSchema = exports_external.object({
|
|
|
22168
22168
|
function makePageResponseSchema(responseKey, liveSchema) {
|
|
22169
22169
|
return exports_external.object({
|
|
22170
22170
|
[responseKey]: exports_external.array(exports_external.union([liveSchema, TombstoneSchema])),
|
|
22171
|
-
next_cursor: exports_external.string().nullable()
|
|
22171
|
+
next_cursor: exports_external.string().nullable(),
|
|
22172
|
+
high_water: exports_external.string().nullable().optional()
|
|
22172
22173
|
});
|
|
22173
22174
|
}
|
|
22174
22175
|
function makeParsePageResponse(schema, responseKey) {
|
|
@@ -22176,7 +22177,8 @@ function makeParsePageResponse(schema, responseKey) {
|
|
|
22176
22177
|
const parsed = schema.parse(json2);
|
|
22177
22178
|
return {
|
|
22178
22179
|
rows: parsed[responseKey],
|
|
22179
|
-
next_cursor: parsed.next_cursor
|
|
22180
|
+
next_cursor: parsed.next_cursor,
|
|
22181
|
+
high_water: parsed.high_water ?? null
|
|
22180
22182
|
};
|
|
22181
22183
|
};
|
|
22182
22184
|
}
|
|
@@ -25345,7 +25347,8 @@ var WorkspaceEntrySchema = exports_external.object({
|
|
|
25345
25347
|
last_synced_at: exports_external.string().datetime({ offset: true }).nullable(),
|
|
25346
25348
|
workspace_name: exports_external.string().min(1).optional(),
|
|
25347
25349
|
member: MemberPrincipalSchema.optional(),
|
|
25348
|
-
sync_cursors: SyncCursorsSchema.optional()
|
|
25350
|
+
sync_cursors: SyncCursorsSchema.optional(),
|
|
25351
|
+
sync_high_water: SyncCursorsSchema.optional()
|
|
25349
25352
|
});
|
|
25350
25353
|
var ConfigFileSchema = exports_external.object({
|
|
25351
25354
|
version: exports_external.literal(1),
|
|
@@ -25407,7 +25410,8 @@ function carryForwardSyncState(prior, entry) {
|
|
|
25407
25410
|
return {
|
|
25408
25411
|
...entry,
|
|
25409
25412
|
last_synced_at: prior.last_synced_at,
|
|
25410
|
-
...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {}
|
|
25413
|
+
...prior.sync_cursors !== undefined ? { sync_cursors: prior.sync_cursors } : {},
|
|
25414
|
+
...prior.sync_high_water !== undefined ? { sync_high_water: prior.sync_high_water } : {}
|
|
25411
25415
|
};
|
|
25412
25416
|
}
|
|
25413
25417
|
function upsertWorkspace(slug, entry, options2 = {}) {
|
|
@@ -25448,6 +25452,23 @@ function updateSyncCursor(slug, entityType, cursor) {
|
|
|
25448
25452
|
writeAtomic(next);
|
|
25449
25453
|
return next;
|
|
25450
25454
|
}
|
|
25455
|
+
function updateSyncHighWater(slug, entityType, mark) {
|
|
25456
|
+
const existing = loadConfig();
|
|
25457
|
+
if (!existing) {
|
|
25458
|
+
throw new Error(`No config. Run \`plinth login --workspace ${slug}\` first.`);
|
|
25459
|
+
}
|
|
25460
|
+
const entry = requireWorkspace(existing, slug);
|
|
25461
|
+
if ((entry.sync_high_water?.[entityType] ?? null) === mark)
|
|
25462
|
+
return;
|
|
25463
|
+
const nextEntry = {
|
|
25464
|
+
...entry,
|
|
25465
|
+
sync_high_water: { ...entry.sync_high_water, [entityType]: mark }
|
|
25466
|
+
};
|
|
25467
|
+
writeAtomic({
|
|
25468
|
+
...existing,
|
|
25469
|
+
workspaces: { ...existing.workspaces, [slug]: nextEntry }
|
|
25470
|
+
});
|
|
25471
|
+
}
|
|
25451
25472
|
function setMemberPrincipal(slug, member) {
|
|
25452
25473
|
const existing = loadConfig();
|
|
25453
25474
|
if (!existing)
|
|
@@ -26735,7 +26756,7 @@ function allFileEntries(state) {
|
|
|
26735
26756
|
return Object.values(state.files ?? {});
|
|
26736
26757
|
}
|
|
26737
26758
|
function fileEntriesEqual(a, b) {
|
|
26738
|
-
return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode;
|
|
26759
|
+
return a.id === b.id && a.path === b.path && a.content_hash === b.content_hash && a.size_bytes === b.size_bytes && a.mime_type === b.mime_type && a.updated_at === b.updated_at && a.inode === b.inode && a.text_hash === b.text_hash;
|
|
26739
26760
|
}
|
|
26740
26761
|
function clearFrontmatterIdBackfillMarker(state) {
|
|
26741
26762
|
if (state.frontmatter_id_backfill_version === undefined) {
|
|
@@ -30195,7 +30216,8 @@ var FileTombstoneSchema = exports_external.object({
|
|
|
30195
30216
|
var FileRowSchema = exports_external.union([FileLiveSchema, FileTombstoneSchema]);
|
|
30196
30217
|
var FilesPageSchema = exports_external.object({
|
|
30197
30218
|
files: exports_external.array(FileRowSchema),
|
|
30198
|
-
next_cursor: exports_external.string().nullable()
|
|
30219
|
+
next_cursor: exports_external.string().nullable(),
|
|
30220
|
+
high_water: exports_external.string().nullable().optional()
|
|
30199
30221
|
});
|
|
30200
30222
|
var FileWithDownloadSchema = FileLiveSchema.extend({
|
|
30201
30223
|
download_url: exports_external.string().url().nullable()
|
|
@@ -30248,7 +30270,11 @@ async function syncFilesPage(args) {
|
|
|
30248
30270
|
if (!parsed.success) {
|
|
30249
30271
|
throw new ApiError(res.status, `sync files response did not match the expected shape: ${parsed.error.message}`);
|
|
30250
30272
|
}
|
|
30251
|
-
return {
|
|
30273
|
+
return {
|
|
30274
|
+
rows: parsed.data.files,
|
|
30275
|
+
next_cursor: parsed.data.next_cursor,
|
|
30276
|
+
high_water: parsed.data.high_water ?? null
|
|
30277
|
+
};
|
|
30252
30278
|
}
|
|
30253
30279
|
async function getFile(args) {
|
|
30254
30280
|
const { baseUrl, pat, id, fetchImpl = fetch } = args;
|
|
@@ -30519,6 +30545,12 @@ function fileDeleteSuppressedLine(f) {
|
|
|
30519
30545
|
function entityDeleteSuppressedLine(f) {
|
|
30520
30546
|
return `[ENTITY_DELETE_SUPPRESSED entityType=${f.entityType} slug=${f.slug} reason=${f.reason}]`;
|
|
30521
30547
|
}
|
|
30548
|
+
function entityRestoreDeferredLine(f) {
|
|
30549
|
+
return `[ENTITY_RESTORE_DEFERRED entityType=${f.entityType} slug=${f.slug} wait_s=${f.waitSeconds}]`;
|
|
30550
|
+
}
|
|
30551
|
+
function fileRestoreDeferredLine(f) {
|
|
30552
|
+
return `[FILE_RESTORE_DEFERRED path=${f.path} wait_s=${f.waitSeconds}]`;
|
|
30553
|
+
}
|
|
30522
30554
|
function fileOverCapLine(f) {
|
|
30523
30555
|
return `[FILE_OVER_CAP path=${f.path} size=${f.size} max=${f.max} max_from=${f.source}]`;
|
|
30524
30556
|
}
|
|
@@ -38054,7 +38086,7 @@ function unknownBecause(reason) {
|
|
|
38054
38086
|
case "no_watermark":
|
|
38055
38087
|
return "The last download carried no freshness marker, so there is nothing to compare against.";
|
|
38056
38088
|
case "incomplete_apply":
|
|
38057
|
-
return "The last download did not fully land, so this copy is not
|
|
38089
|
+
return "The last download either did not fully land or carried a correction trail this copy could not use, so this copy is not one the freshness marker can vouch for.";
|
|
38058
38090
|
case "check_failed":
|
|
38059
38091
|
return "The freshness check did not complete.";
|
|
38060
38092
|
default:
|
|
@@ -38211,6 +38243,9 @@ function resolveTrail(row, slugById) {
|
|
|
38211
38243
|
consistent: true
|
|
38212
38244
|
};
|
|
38213
38245
|
}
|
|
38246
|
+
if (outcome === "current" && successor === row.id) {
|
|
38247
|
+
return { trail: "current", consistent: true };
|
|
38248
|
+
}
|
|
38214
38249
|
if (successor !== null)
|
|
38215
38250
|
return { trail: UNKNOWN, consistent: false };
|
|
38216
38251
|
return { trail: outcome, consistent: true };
|
|
@@ -40675,7 +40710,12 @@ function startWatcher(opts, handler) {
|
|
|
40675
40710
|
});
|
|
40676
40711
|
watcher.on("add", (p) => classifier.onAdd(p));
|
|
40677
40712
|
watcher.on("change", (p) => classifier.onChange(p));
|
|
40678
|
-
watcher.on("unlink", (p) =>
|
|
40713
|
+
watcher.on("unlink", (p) => {
|
|
40714
|
+
try {
|
|
40715
|
+
opts.onUnlink?.(p);
|
|
40716
|
+
} catch {}
|
|
40717
|
+
classifier.onUnlink(p);
|
|
40718
|
+
});
|
|
40679
40719
|
return {
|
|
40680
40720
|
async stop() {
|
|
40681
40721
|
classifier.shutdown();
|
|
@@ -42799,6 +42839,109 @@ class PullLoop {
|
|
|
42799
42839
|
}
|
|
42800
42840
|
}
|
|
42801
42841
|
|
|
42842
|
+
// src/lib/high-water.ts
|
|
42843
|
+
var HIGH_WATER_BASE_READ_MS = 60 * 60 * 1000;
|
|
42844
|
+
function baseReadIntervalMs(env2 = process.env) {
|
|
42845
|
+
const raw = Number(env2.PLINTH_HIGH_WATER_BASE_READ_MS);
|
|
42846
|
+
return Number.isFinite(raw) && raw > 0 ? raw : HIGH_WATER_BASE_READ_MS;
|
|
42847
|
+
}
|
|
42848
|
+
function provenMark(args) {
|
|
42849
|
+
if (!args.clean)
|
|
42850
|
+
return null;
|
|
42851
|
+
return args.finalHighWater ?? args.advancedTo;
|
|
42852
|
+
}
|
|
42853
|
+
function createBaseReadSchedule(bootMs, intervalMs = HIGH_WATER_BASE_READ_MS) {
|
|
42854
|
+
let lastBaseReadMs = bootMs;
|
|
42855
|
+
let restoresWaiting = false;
|
|
42856
|
+
let lastUsedHighWater = false;
|
|
42857
|
+
return {
|
|
42858
|
+
useHighWater(nowMs) {
|
|
42859
|
+
if (restoresWaiting)
|
|
42860
|
+
return lastUsedHighWater;
|
|
42861
|
+
return nowMs - lastBaseReadMs < intervalMs;
|
|
42862
|
+
},
|
|
42863
|
+
completed(nowMs, usedHighWater, ok, restoresDeferred = 0) {
|
|
42864
|
+
if (!ok)
|
|
42865
|
+
return;
|
|
42866
|
+
if (!usedHighWater)
|
|
42867
|
+
lastBaseReadMs = nowMs;
|
|
42868
|
+
restoresWaiting = restoresDeferred > 0;
|
|
42869
|
+
lastUsedHighWater = usedHighWater;
|
|
42870
|
+
}
|
|
42871
|
+
};
|
|
42872
|
+
}
|
|
42873
|
+
|
|
42874
|
+
// src/lib/restore-deferral.ts
|
|
42875
|
+
var RESTORE_DEFERRAL_MS = 5 * 60000;
|
|
42876
|
+
var UNEXPLAINED_GRACE_MS = 5000;
|
|
42877
|
+
function keyOf(absolutePath) {
|
|
42878
|
+
return absolutePath.normalize("NFC");
|
|
42879
|
+
}
|
|
42880
|
+
function sightingKey(key, version2) {
|
|
42881
|
+
return `${key}\x00${version2}`;
|
|
42882
|
+
}
|
|
42883
|
+
function forgetSightings(recent, key) {
|
|
42884
|
+
const prefix = `${key}\x00`;
|
|
42885
|
+
for (const k of recent.keys())
|
|
42886
|
+
if (k.startsWith(prefix))
|
|
42887
|
+
recent.delete(k);
|
|
42888
|
+
}
|
|
42889
|
+
function noteUnlink(recent, absolutePath, nowMs) {
|
|
42890
|
+
forgetExpired(recent, nowMs);
|
|
42891
|
+
const key = keyOf(absolutePath);
|
|
42892
|
+
forgetSightings(recent, key);
|
|
42893
|
+
recent.set(key, { kind: "unlink", at: nowMs, logged: false });
|
|
42894
|
+
}
|
|
42895
|
+
function deferRestore(recent, absolutePath, version2, nowMs) {
|
|
42896
|
+
if (!recent)
|
|
42897
|
+
return { defer: false, first: false };
|
|
42898
|
+
forgetExpired(recent, nowMs);
|
|
42899
|
+
const key = keyOf(absolutePath);
|
|
42900
|
+
const unlinked = recent.get(key);
|
|
42901
|
+
if (unlinked?.kind === "unlink") {
|
|
42902
|
+
if (unlinked.logged)
|
|
42903
|
+
return { defer: true, first: false };
|
|
42904
|
+
recent.set(key, { ...unlinked, logged: true });
|
|
42905
|
+
return { defer: true, first: true };
|
|
42906
|
+
}
|
|
42907
|
+
const sighting = sightingKey(key, version2);
|
|
42908
|
+
const seen = recent.get(sighting);
|
|
42909
|
+
if (seen === undefined) {
|
|
42910
|
+
recent.set(sighting, { kind: "missing", at: nowMs });
|
|
42911
|
+
return { defer: true, first: false };
|
|
42912
|
+
}
|
|
42913
|
+
if (nowMs - seen.at < UNEXPLAINED_GRACE_MS)
|
|
42914
|
+
return { defer: true, first: false };
|
|
42915
|
+
recent.delete(sighting);
|
|
42916
|
+
return { defer: false, first: false };
|
|
42917
|
+
}
|
|
42918
|
+
function presentAgain(recent, absolutePath) {
|
|
42919
|
+
if (recent)
|
|
42920
|
+
forgetSightings(recent, keyOf(absolutePath));
|
|
42921
|
+
}
|
|
42922
|
+
function forgetExpired(recent, nowMs) {
|
|
42923
|
+
for (const [key, seen] of recent) {
|
|
42924
|
+
if (seen.kind === "unlink" && nowMs - seen.at >= RESTORE_DEFERRAL_MS)
|
|
42925
|
+
recent.delete(key);
|
|
42926
|
+
}
|
|
42927
|
+
}
|
|
42928
|
+
|
|
42929
|
+
// src/lib/ambient-poll.ts
|
|
42930
|
+
function createAmbientPollWiring(opts) {
|
|
42931
|
+
const recentUnlinks = new Map;
|
|
42932
|
+
const baseReads = createBaseReadSchedule(opts.now(), opts.baseReadIntervalMs);
|
|
42933
|
+
return {
|
|
42934
|
+
onUnlink: (absolutePath) => noteUnlink(recentUnlinks, absolutePath, opts.now()),
|
|
42935
|
+
async poll(run) {
|
|
42936
|
+
const startMs = opts.now();
|
|
42937
|
+
const useHighWater = baseReads.useHighWater(startMs);
|
|
42938
|
+
const result = await run({ useHighWater, recentUnlinks });
|
|
42939
|
+
baseReads.completed(startMs, useHighWater, result.exitCode === 0, result.restoresDeferred ?? 0);
|
|
42940
|
+
return result;
|
|
42941
|
+
}
|
|
42942
|
+
};
|
|
42943
|
+
}
|
|
42944
|
+
|
|
42802
42945
|
// src/lib/calendar-loop.ts
|
|
42803
42946
|
var DEFAULT_CALENDAR_INTERVAL_MS = 900000;
|
|
42804
42947
|
var CALENDAR_BACKOFF_CAP_MS = 3600000;
|
|
@@ -42959,6 +43102,12 @@ var CALENDAR_STATUSES = [
|
|
|
42959
43102
|
"token-expired",
|
|
42960
43103
|
"google-error"
|
|
42961
43104
|
];
|
|
43105
|
+
var KNOWN_TRUNCATED_REASONS = ["page_limit", "time_limit"];
|
|
43106
|
+
var TruncatedReasonSchema = exports_external.string().nullish().transform((v) => {
|
|
43107
|
+
if (v === null || v === undefined)
|
|
43108
|
+
return null;
|
|
43109
|
+
return KNOWN_TRUNCATED_REASONS.includes(v) ? v : "other";
|
|
43110
|
+
});
|
|
42962
43111
|
var CalendarRowSchema = exports_external.object({
|
|
42963
43112
|
artist_id: exports_external.string().nullable(),
|
|
42964
43113
|
artist_slug: exports_external.string().nullable(),
|
|
@@ -42967,8 +43116,13 @@ var CalendarRowSchema = exports_external.object({
|
|
|
42967
43116
|
calendar_name: exports_external.string(),
|
|
42968
43117
|
status: exports_external.enum(CALENDAR_STATUSES),
|
|
42969
43118
|
events: exports_external.array(CalendarEventSchema).nullable(),
|
|
42970
|
-
error: exports_external.object({
|
|
42971
|
-
|
|
43119
|
+
error: exports_external.object({
|
|
43120
|
+
upstream_status: exports_external.number().nullable(),
|
|
43121
|
+
message: exports_external.string()
|
|
43122
|
+
}).nullish().transform((v) => v ?? null),
|
|
43123
|
+
truncated: exports_external.boolean().optional().default(false),
|
|
43124
|
+
truncated_reason: TruncatedReasonSchema,
|
|
43125
|
+
resume_from: exports_external.string().nullish().transform((v) => v ?? null)
|
|
42972
43126
|
});
|
|
42973
43127
|
var CalendarResponseSchema = exports_external.object({
|
|
42974
43128
|
generated_at: exports_external.string().min(1),
|
|
@@ -43156,6 +43310,62 @@ function readAsOf(text, today) {
|
|
|
43156
43310
|
return { parts, yearInferred: first.yearInferred };
|
|
43157
43311
|
}
|
|
43158
43312
|
|
|
43313
|
+
// src/lib/calendar/limits.ts
|
|
43314
|
+
var CALENDAR_RANGE_TOOL = "list_calendar_events";
|
|
43315
|
+
function spanPhrase(timeMin, timeMax) {
|
|
43316
|
+
const ms = Date.parse(timeMax) - Date.parse(timeMin);
|
|
43317
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
43318
|
+
return null;
|
|
43319
|
+
const weeks = ms / (7 * DAY_MS);
|
|
43320
|
+
if (Number.isInteger(weeks))
|
|
43321
|
+
return `${weeks} ${weeks === 1 ? "week" : "weeks"}`;
|
|
43322
|
+
const days = ms / DAY_MS;
|
|
43323
|
+
if (Number.isInteger(days))
|
|
43324
|
+
return `${days} ${days === 1 ? "day" : "days"}`;
|
|
43325
|
+
return null;
|
|
43326
|
+
}
|
|
43327
|
+
function coverageLine(timeMin, timeMax) {
|
|
43328
|
+
const span = spanPhrase(timeMin, timeMax);
|
|
43329
|
+
return `**This file holds only the window above${span ? ` (${span})` : ""}.** ` + `When a question needs a date outside it, past or future, call the Plinth ` + `tool \`${CALENDAR_RANGE_TOOL}\` for just that range.`;
|
|
43330
|
+
}
|
|
43331
|
+
var TRUNCATION_CAUSE = {
|
|
43332
|
+
page_limit: "Google had more entries in this window than one refresh reads.",
|
|
43333
|
+
time_limit: "The read from Google ran out of time.",
|
|
43334
|
+
other: "The read stopped before the end of the window."
|
|
43335
|
+
};
|
|
43336
|
+
var TRUNCATION_SHORT = {
|
|
43337
|
+
page_limit: "stopped at the page limit",
|
|
43338
|
+
time_limit: "ran out of time",
|
|
43339
|
+
other: "stopped before the end of the window"
|
|
43340
|
+
};
|
|
43341
|
+
var OFFSET_TIMESTAMP_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/i;
|
|
43342
|
+
function resumeRange(resumeFrom, window2) {
|
|
43343
|
+
if (resumeFrom === null || !OFFSET_TIMESTAMP_RE.test(resumeFrom))
|
|
43344
|
+
return null;
|
|
43345
|
+
if (!OFFSET_TIMESTAMP_RE.test(window2.time_min) || !OFFSET_TIMESTAMP_RE.test(window2.time_max)) {
|
|
43346
|
+
return null;
|
|
43347
|
+
}
|
|
43348
|
+
const at = Date.parse(resumeFrom);
|
|
43349
|
+
const min = Date.parse(window2.time_min);
|
|
43350
|
+
const max = Date.parse(window2.time_max);
|
|
43351
|
+
if (!(at > min && at < max))
|
|
43352
|
+
return null;
|
|
43353
|
+
return { from: new Date(at).toISOString(), to: new Date(max).toISOString() };
|
|
43354
|
+
}
|
|
43355
|
+
function incompleteLine(args) {
|
|
43356
|
+
const { stamp: stamp2, eventCount, reason, resumeFrom, window: window2 } = args;
|
|
43357
|
+
const kept = eventCount === 0 ? "No entries were collected before the read stopped, so this is NOT a clear diary." : `${eventCount} ${eventCount === 1 ? "entry was" : "entries were"} collected before the read stopped.`;
|
|
43358
|
+
const range = resumeRange(resumeFrom, window2);
|
|
43359
|
+
const pointer = range ? ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` from ` + `\`${range.from}\` to \`${range.to}\` (a few entries repeat).` : ` For the rest, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` for a ` + `narrower range inside the window.`;
|
|
43360
|
+
return `**Read as of ${stamp2}, but this list is INCOMPLETE.** ` + `${TRUNCATION_CAUSE[reason ?? "other"]} ${kept} ` + `The rest of the window is not included: unknown, not absent.${pointer}`;
|
|
43361
|
+
}
|
|
43362
|
+
function sentence(text) {
|
|
43363
|
+
return /[.!?\u2026]$/.test(text) ? text : `${text}.`;
|
|
43364
|
+
}
|
|
43365
|
+
function truncationShort(reason) {
|
|
43366
|
+
return TRUNCATION_SHORT[reason ?? "other"];
|
|
43367
|
+
}
|
|
43368
|
+
|
|
43159
43369
|
// src/lib/calendar/render.ts
|
|
43160
43370
|
var CALENDAR_BODY_MARKER = "<!-- PLINTH:CALENDAR-BODY -->";
|
|
43161
43371
|
var OWNERSHIP_NOTE2 = [
|
|
@@ -43259,37 +43469,61 @@ function windowLine(timeMin, timeMax) {
|
|
|
43259
43469
|
return `${timeMin} to ${timeMax}`;
|
|
43260
43470
|
return `${formatDay(from, -1)} to ${formatDay(to, -1)}`;
|
|
43261
43471
|
}
|
|
43472
|
+
function isRefusal(error52) {
|
|
43473
|
+
return error52.upstream_status !== null && error52.upstream_status >= 400;
|
|
43474
|
+
}
|
|
43475
|
+
function googleErrorLine(error52) {
|
|
43476
|
+
if (error52 !== null && !isRefusal(error52)) {
|
|
43477
|
+
return `**NOT READ - the read from Google did not complete.** ` + `${sentence(oneLine2(error52.message, 300))} The next refresh retries; until then ` + `nothing below is current, so treat this calendar as unknown rather than empty. ` + `For a date on it you need now, call the Plinth tool \`${CALENDAR_RANGE_TOOL}\` ` + `for a narrower range.`;
|
|
43478
|
+
}
|
|
43479
|
+
return `**NOT READ - Google refused the request` + `${error52 ? ` (HTTP ${error52.upstream_status})` : ""}.** ` + `${error52 ? sentence(oneLine2(error52.message, 300)) : "No detail was returned."} ` + `A calendar that was unshared or deleted in Google reads exactly like this. ` + `Nothing below is current; treat this calendar as unknown rather than empty.`;
|
|
43480
|
+
}
|
|
43262
43481
|
function statusLine(args) {
|
|
43263
43482
|
const { status, eventCount, error: error52, truncated, stamp: stamp2, userLabel } = args;
|
|
43264
43483
|
switch (status) {
|
|
43265
43484
|
case "fetched": {
|
|
43485
|
+
if (truncated) {
|
|
43486
|
+
return incompleteLine({
|
|
43487
|
+
stamp: stamp2,
|
|
43488
|
+
eventCount,
|
|
43489
|
+
reason: args.truncatedReason,
|
|
43490
|
+
resumeFrom: args.resumeFrom,
|
|
43491
|
+
window: args.window
|
|
43492
|
+
});
|
|
43493
|
+
}
|
|
43266
43494
|
if (eventCount === 0) {
|
|
43267
43495
|
return `**Read fine as of ${stamp2}. Nothing in this window.** The diary really ` + `is clear across the dates above. This is a successful read of an empty ` + `calendar, not a failed read - if it had failed, this line would say so.`;
|
|
43268
43496
|
}
|
|
43269
|
-
|
|
43270
|
-
return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.${truncatedNote}`;
|
|
43497
|
+
return `**Read fine as of ${stamp2}.** ${eventCount} ` + `${eventCount === 1 ? "entry" : "entries"} in the window.`;
|
|
43271
43498
|
}
|
|
43272
43499
|
case "no-token":
|
|
43273
43500
|
return `**NOT READ - Google is not connected for ${userLabel}.** Nothing is listed ` + `below because Plinth could not look, NOT because the diary is clear. ` + `Connect Google in Plinth to fill this in, and until then treat this ` + `calendar as unknown rather than empty.`;
|
|
43274
43501
|
case "token-expired":
|
|
43275
43502
|
return `**NOT READ - the Google connection for ${userLabel} has expired.** Plinth ` + `still holds a connection but could not refresh it, so nothing below is ` + `current. Reconnect Google in Plinth. Treat this calendar as unknown ` + `rather than empty.`;
|
|
43276
43503
|
case "google-error":
|
|
43277
|
-
return
|
|
43504
|
+
return googleErrorLine(error52);
|
|
43278
43505
|
}
|
|
43279
43506
|
}
|
|
43280
43507
|
function shortStatus(row) {
|
|
43281
43508
|
switch (row.status) {
|
|
43282
43509
|
case "fetched": {
|
|
43283
43510
|
const n = row.events?.length ?? 0;
|
|
43511
|
+
const count = `${n} ${n === 1 ? "entry" : "entries"}`;
|
|
43512
|
+
if (row.truncated) {
|
|
43513
|
+
return `read, ${count}, INCOMPLETE (${truncationShort(row.truncated_reason)}); ` + `the rest of the window is unknown, not empty`;
|
|
43514
|
+
}
|
|
43284
43515
|
if (n === 0)
|
|
43285
43516
|
return "read fine, nothing in this window";
|
|
43286
|
-
return `read fine, ${
|
|
43517
|
+
return `read fine, ${count}`;
|
|
43287
43518
|
}
|
|
43288
43519
|
case "no-token":
|
|
43289
43520
|
return "NOT READ - Google is not connected; unknown, not empty";
|
|
43290
43521
|
case "token-expired":
|
|
43291
43522
|
return "NOT READ - the Google connection has expired; unknown, not empty";
|
|
43292
43523
|
case "google-error":
|
|
43524
|
+
if (row.error !== null && !isRefusal(row.error)) {
|
|
43525
|
+
return `NOT READ - the read from Google did not complete: ` + `${oneLine2(row.error.message, 120)}; the next refresh retries; unknown, not empty`;
|
|
43526
|
+
}
|
|
43293
43527
|
return `NOT READ - Google refused` + `${row.error ? ` (HTTP ${row.error.upstream_status}): ${oneLine2(row.error.message, 120)}` : ""}` + `; unknown, not empty`;
|
|
43294
43528
|
}
|
|
43295
43529
|
}
|
|
@@ -43347,21 +43581,25 @@ function renderArtistCalendar(args) {
|
|
|
43347
43581
|
eventCount: events.length,
|
|
43348
43582
|
error: row.error,
|
|
43349
43583
|
truncated: row.truncated,
|
|
43584
|
+
truncatedReason: row.truncated_reason,
|
|
43585
|
+
resumeFrom: row.resume_from,
|
|
43586
|
+
window: window2,
|
|
43350
43587
|
stamp: stamp2,
|
|
43351
43588
|
userLabel
|
|
43352
43589
|
}),
|
|
43353
43590
|
"",
|
|
43354
43591
|
`**Window:** ${windowLine(window2.time_min, window2.time_max)}.`,
|
|
43355
43592
|
"",
|
|
43593
|
+
...row.status === "fetched" ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
|
|
43356
43594
|
"**Dates are read here and changed in Plinth's calendar view, by a person.** If something below needs moving, say what the change is and hand it over.",
|
|
43357
43595
|
"",
|
|
43358
|
-
renderBands(events, generatedAt, referenceYear, "")
|
|
43596
|
+
renderBands(events, generatedAt, referenceYear, "", row.truncated)
|
|
43359
43597
|
].join(`
|
|
43360
43598
|
`);
|
|
43361
43599
|
return `${header}${CALENDAR_BODY_MARKER}
|
|
43362
43600
|
${body}`;
|
|
43363
43601
|
}
|
|
43364
|
-
function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
43602
|
+
function renderBands(events, generatedAt, referenceYear, artistPrefix, truncated) {
|
|
43365
43603
|
if (events.length === 0)
|
|
43366
43604
|
return "";
|
|
43367
43605
|
const { full, rest } = splitBands(events, generatedAt);
|
|
@@ -43375,7 +43613,7 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
|
43375
43613
|
}
|
|
43376
43614
|
out.push("", "## Rest of the window", "");
|
|
43377
43615
|
if (rest.length === 0) {
|
|
43378
|
-
out.push("_Nothing further in the window._");
|
|
43616
|
+
out.push(truncated ? "_Nothing further was collected: the read stopped before the end of the window._" : "_Nothing further in the window._");
|
|
43379
43617
|
} else {
|
|
43380
43618
|
for (const e of rest)
|
|
43381
43619
|
out.push(compactLine(e, referenceYear, artistPrefix));
|
|
@@ -43384,6 +43622,9 @@ function renderBands(events, generatedAt, referenceYear, artistPrefix) {
|
|
|
43384
43622
|
return out.join(`
|
|
43385
43623
|
`);
|
|
43386
43624
|
}
|
|
43625
|
+
function lacksConnection(row) {
|
|
43626
|
+
return row.status === "no-token" || row.status === "token-expired";
|
|
43627
|
+
}
|
|
43387
43628
|
function renderUpcoming(args) {
|
|
43388
43629
|
const {
|
|
43389
43630
|
rows,
|
|
@@ -43399,6 +43640,7 @@ function renderUpcoming(args) {
|
|
|
43399
43640
|
const fetched = rows.filter((r) => r.status === "fetched");
|
|
43400
43641
|
const failed = rows.filter((r) => r.status !== "fetched");
|
|
43401
43642
|
const truncatedAnywhere = rows.some((r) => r.truncated);
|
|
43643
|
+
const cutShort = fetched.filter((r) => r.truncated).length;
|
|
43402
43644
|
const merged = fetched.flatMap((row) => (row.events ?? []).map((event) => ({ event, row }))).sort((a, b) => sortInstant(a.event) - sortInstant(b.event));
|
|
43403
43645
|
const { full, rest } = splitBands(merged.map((m) => m.event), generatedAt);
|
|
43404
43646
|
const fullSet = new Set(full);
|
|
@@ -43429,7 +43671,12 @@ function renderUpcoming(args) {
|
|
|
43429
43671
|
"",
|
|
43430
43672
|
`**As of ${stamp2}.** Window ${windowLine(window2.time_min, window2.time_max)}.`,
|
|
43431
43673
|
"",
|
|
43674
|
+
...rows.length > 0 && !rows.every(lacksConnection) ? [coverageLine(window2.time_min, window2.time_max), ""] : [],
|
|
43432
43675
|
failed.length === 0 ? `**All ${fetched.length} mapped ${fetched.length === 1 ? "calendar was" : "calendars were"} read.**` : `**${fetched.length} of ${rows.length} mapped ${rows.length === 1 ? "calendar" : "calendars"} read; ${failed.length} could NOT be read.**` + ` Anything on an unread calendar is missing from the lists below - unknown, not absent.`,
|
|
43676
|
+
...cutShort === 0 ? [] : [
|
|
43677
|
+
"",
|
|
43678
|
+
`**${cutShort} of the calendars read ${cutShort === 1 ? "was" : "were"} cut short** before the end ` + `of the window, so entries past the point where ${cutShort === 1 ? "it" : "each"} stopped are ` + `missing from the lists below - unknown, not absent. The status list says which.`
|
|
43679
|
+
],
|
|
43433
43680
|
"",
|
|
43434
43681
|
`${artistCount} ${artistCount === 1 ? "artist" : "artists"} in the roster; ` + `${mappedArtistCount} ${mappedArtistCount === 1 ? "has" : "have"} a calendar mapped in Plinth.`,
|
|
43435
43682
|
"",
|
|
@@ -43446,7 +43693,7 @@ function renderUpcoming(args) {
|
|
|
43446
43693
|
body.push(`## Next ${FULL_BAND_DAYS} days`, "", "_Links are in the per-artist file named beside each calendar above._", "");
|
|
43447
43694
|
const fullMerged = merged.filter((m) => fullSet.has(m.event));
|
|
43448
43695
|
if (fullMerged.length === 0) {
|
|
43449
|
-
body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
|
|
43696
|
+
body.push(fetched.length === 0 ? `_No calendar could be read, so nothing can be listed for the next ${FULL_BAND_DAYS} days._` : cutShort > 0 ? `_Nothing collected for the next ${FULL_BAND_DAYS} days on any calendar that was read; the ones cut short may be missing entries._` : `_Nothing in the next ${FULL_BAND_DAYS} days on any calendar that was read._`);
|
|
43450
43697
|
} else {
|
|
43451
43698
|
for (const { event, row } of fullMerged) {
|
|
43452
43699
|
body.push(fullLine(event, referenceYear, `**${rowDisplayName(row)}**`, false));
|
|
@@ -43455,7 +43702,7 @@ function renderUpcoming(args) {
|
|
|
43455
43702
|
body.push("", "## Rest of the window", "");
|
|
43456
43703
|
const restMerged = merged.filter((m) => restSet.has(m.event));
|
|
43457
43704
|
if (restMerged.length === 0) {
|
|
43458
|
-
body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : "_Nothing further in the window on any calendar that was read._");
|
|
43705
|
+
body.push(fetched.length === 0 ? "_No calendar could be read, so nothing can be listed._" : cutShort > 0 ? "_Nothing further was collected on any calendar that was read; the ones cut short may be missing entries past where they stopped._" : "_Nothing further in the window on any calendar that was read._");
|
|
43459
43706
|
} else {
|
|
43460
43707
|
for (const { event, row } of restMerged) {
|
|
43461
43708
|
body.push(compactLine(event, referenceYear, `**${rowDisplayName(row)}**`));
|
|
@@ -46061,7 +46308,8 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46061
46308
|
const now = args.now ?? (() => new Date);
|
|
46062
46309
|
const rows = [];
|
|
46063
46310
|
let cursor = null;
|
|
46064
|
-
|
|
46311
|
+
const savedCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
|
|
46312
|
+
let resultingCursor = savedCursor;
|
|
46065
46313
|
while (true) {
|
|
46066
46314
|
const page = await syncFilesPage({
|
|
46067
46315
|
baseUrl: args.baseUrl,
|
|
@@ -46084,6 +46332,7 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46084
46332
|
let pointerRows = 0;
|
|
46085
46333
|
let symlinkRefused = 0;
|
|
46086
46334
|
let stashFailed = 0;
|
|
46335
|
+
let restoresDeferred = 0;
|
|
46087
46336
|
let completePass = true;
|
|
46088
46337
|
let lastCommittedState = args.state;
|
|
46089
46338
|
const completed = [];
|
|
@@ -46172,7 +46421,8 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46172
46421
|
isFileFrozen: args.isFileFrozen,
|
|
46173
46422
|
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46174
46423
|
forceTextRewalk: args.forceTextRewalk,
|
|
46175
|
-
loggedStashNotices: args.loggedStashNotices
|
|
46424
|
+
loggedStashNotices: args.loggedStashNotices,
|
|
46425
|
+
recentUnlinks: args.recentUnlinks
|
|
46176
46426
|
});
|
|
46177
46427
|
state = outcome.state;
|
|
46178
46428
|
if (outcome.action === "written")
|
|
@@ -46189,11 +46439,15 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46189
46439
|
symlinkRefused++;
|
|
46190
46440
|
completePass = false;
|
|
46191
46441
|
}
|
|
46192
|
-
if (outcome.stashFailed || outcome.textFailed) {
|
|
46442
|
+
if (outcome.stashFailed || outcome.textFailed || outcome.integrityFailed) {
|
|
46193
46443
|
if (outcome.stashFailed)
|
|
46194
46444
|
stashFailed++;
|
|
46195
46445
|
completePass = false;
|
|
46196
46446
|
}
|
|
46447
|
+
if (outcome.restoreDeferred) {
|
|
46448
|
+
restoresDeferred++;
|
|
46449
|
+
completePass = false;
|
|
46450
|
+
}
|
|
46197
46451
|
}
|
|
46198
46452
|
if (args.commit && state !== lastCommittedState)
|
|
46199
46453
|
state = args.commit(lastCommittedState, state);
|
|
@@ -46208,7 +46462,8 @@ async function replayPermanentFiles(args, markers) {
|
|
|
46208
46462
|
pointerRows,
|
|
46209
46463
|
symlinkRefused,
|
|
46210
46464
|
stashFailed,
|
|
46211
|
-
lastCursor: completePass ? resultingCursor :
|
|
46465
|
+
lastCursor: completePass ? resultingCursor : savedCursor,
|
|
46466
|
+
restoresDeferred,
|
|
46212
46467
|
permanentRefreshes: completed,
|
|
46213
46468
|
permanentReplayComplete: completePass
|
|
46214
46469
|
};
|
|
@@ -46232,12 +46487,27 @@ async function pullFiles(args) {
|
|
|
46232
46487
|
let pointerRows = 0;
|
|
46233
46488
|
let symlinkRefused = 0;
|
|
46234
46489
|
let stashFailed = 0;
|
|
46490
|
+
let restoresDeferred = 0;
|
|
46235
46491
|
let permanentEncountered = false;
|
|
46492
|
+
const baseCursor = args.baseCursor === undefined ? args.cursor : args.baseCursor;
|
|
46236
46493
|
let cursor = args.cursor;
|
|
46237
46494
|
let lastCursor = args.cursor;
|
|
46495
|
+
let finalHighWater = null;
|
|
46496
|
+
let markRefused = false;
|
|
46238
46497
|
let cursorBlocked = permanentJournalBlocked;
|
|
46239
46498
|
while (true) {
|
|
46240
|
-
|
|
46499
|
+
let page;
|
|
46500
|
+
try {
|
|
46501
|
+
page = await syncFilesPage({ baseUrl, pat, cursor, fetchImpl });
|
|
46502
|
+
} catch (err) {
|
|
46503
|
+
if (!markRefused && cursor === args.cursor && args.cursor !== baseCursor && err instanceof ApiError && err.status === 400) {
|
|
46504
|
+
markRefused = true;
|
|
46505
|
+
cursor = baseCursor;
|
|
46506
|
+
lastCursor = baseCursor;
|
|
46507
|
+
continue;
|
|
46508
|
+
}
|
|
46509
|
+
throw err;
|
|
46510
|
+
}
|
|
46241
46511
|
const pageBase = state;
|
|
46242
46512
|
let pageTextFailed = false;
|
|
46243
46513
|
for (const row of page.rows) {
|
|
@@ -46271,7 +46541,8 @@ async function pullFiles(args) {
|
|
|
46271
46541
|
isFileFrozen: args.isFileFrozen,
|
|
46272
46542
|
loggedTombstoneSkips: args.loggedTombstoneSkips,
|
|
46273
46543
|
forceTextRewalk: args.forceTextRewalk,
|
|
46274
|
-
loggedStashNotices: args.loggedStashNotices
|
|
46544
|
+
loggedStashNotices: args.loggedStashNotices,
|
|
46545
|
+
recentUnlinks: args.recentUnlinks
|
|
46275
46546
|
});
|
|
46276
46547
|
state = outcome.state;
|
|
46277
46548
|
if (!outcome.symlinkRefused) {
|
|
@@ -46297,13 +46568,21 @@ async function pullFiles(args) {
|
|
|
46297
46568
|
stashFailed++;
|
|
46298
46569
|
pageTextFailed = true;
|
|
46299
46570
|
}
|
|
46571
|
+
if (outcome.integrityFailed)
|
|
46572
|
+
pageTextFailed = true;
|
|
46573
|
+
if (outcome.restoreDeferred) {
|
|
46574
|
+
restoresDeferred++;
|
|
46575
|
+
pageTextFailed = true;
|
|
46576
|
+
}
|
|
46300
46577
|
}
|
|
46301
46578
|
if (args.commit && state !== pageBase)
|
|
46302
46579
|
state = args.commit(pageBase, state);
|
|
46303
46580
|
if (pageTextFailed)
|
|
46304
46581
|
cursorBlocked = true;
|
|
46305
|
-
if (page.next_cursor === null)
|
|
46582
|
+
if (page.next_cursor === null) {
|
|
46583
|
+
finalHighWater = page.high_water ?? null;
|
|
46306
46584
|
break;
|
|
46585
|
+
}
|
|
46307
46586
|
if (!cursorBlocked)
|
|
46308
46587
|
lastCursor = page.next_cursor;
|
|
46309
46588
|
cursor = page.next_cursor;
|
|
@@ -46340,7 +46619,14 @@ async function pullFiles(args) {
|
|
|
46340
46619
|
pointerRows,
|
|
46341
46620
|
symlinkRefused,
|
|
46342
46621
|
stashFailed,
|
|
46343
|
-
lastCursor
|
|
46622
|
+
lastCursor,
|
|
46623
|
+
highWater: provenMark({
|
|
46624
|
+
clean: !cursorBlocked && textUnknown === 0,
|
|
46625
|
+
finalHighWater,
|
|
46626
|
+
advancedTo: lastCursor !== (markRefused ? baseCursor : args.cursor) ? lastCursor : null
|
|
46627
|
+
}),
|
|
46628
|
+
markRefused,
|
|
46629
|
+
restoresDeferred
|
|
46344
46630
|
};
|
|
46345
46631
|
}
|
|
46346
46632
|
async function applyExtractedText(args) {
|
|
@@ -46501,6 +46787,17 @@ async function applyFileRow(args) {
|
|
|
46501
46787
|
output
|
|
46502
46788
|
});
|
|
46503
46789
|
}
|
|
46790
|
+
if (existing && existing.content_hash === row.content_hash && !existsSync8(abs)) {
|
|
46791
|
+
const verdict = deferRestore(args.recentUnlinks, abs, row.updated_at, now().getTime());
|
|
46792
|
+
if (verdict.defer) {
|
|
46793
|
+
if (verdict.first) {
|
|
46794
|
+
output?.log(fileRestoreDeferredLine({ path: relPath, waitSeconds: RESTORE_DEFERRAL_MS / 1000 }));
|
|
46795
|
+
}
|
|
46796
|
+
return { state, action: "noop", textUnknown, restoreDeferred: true };
|
|
46797
|
+
}
|
|
46798
|
+
} else {
|
|
46799
|
+
presentAgain(args.recentUnlinks, abs);
|
|
46800
|
+
}
|
|
46504
46801
|
if (existing && existing.content_hash === row.content_hash && existsSync8(abs)) {
|
|
46505
46802
|
if (!args.forceTextRewalk && textIsCurrent(existing, row, workspaceSlug, relPath)) {
|
|
46506
46803
|
return { state, action: "noop", textUnknown };
|
|
@@ -46553,7 +46850,7 @@ async function applyFileRow(args) {
|
|
|
46553
46850
|
const bytes = await downloadBytes(got.row.download_url, fetchImpl);
|
|
46554
46851
|
if (sha256Bytes(bytes) !== row.content_hash) {
|
|
46555
46852
|
output?.warn(`Integrity check failed for files/${relPath} (hash mismatch) — skipped this pull.`);
|
|
46556
|
-
return { state, action: "skipped" };
|
|
46853
|
+
return { state, action: "skipped", integrityFailed: true };
|
|
46557
46854
|
}
|
|
46558
46855
|
const stashFailure = stashUnpushedLocalEdit({
|
|
46559
46856
|
workspaceSlug,
|
|
@@ -46712,6 +47009,8 @@ function textIsCurrent(existing, row, workspaceSlug, relPath) {
|
|
|
46712
47009
|
}
|
|
46713
47010
|
if (existing.text_hash === undefined)
|
|
46714
47011
|
return false;
|
|
47012
|
+
if (existing.updated_at !== row.updated_at)
|
|
47013
|
+
return false;
|
|
46715
47014
|
const onDisk = readExtractedText(workspaceSlug, relPath);
|
|
46716
47015
|
if (onDisk === null)
|
|
46717
47016
|
return false;
|
|
@@ -47023,6 +47322,7 @@ async function replayPermanentDocuments(args) {
|
|
|
47023
47322
|
let state = args.state;
|
|
47024
47323
|
const completed = [];
|
|
47025
47324
|
let completePass = true;
|
|
47325
|
+
let restoresDeferred = 0;
|
|
47026
47326
|
for (const marker of args.markers) {
|
|
47027
47327
|
const candidates = rows.filter((row) => row.deleted_at !== null && row.slug === marker.logical_key && row.permanent_delete === true && typeof row.purge_generation === "string" && typeof row.retirement_sequence === "number");
|
|
47028
47328
|
candidates.sort((a, b) => b.retirement_sequence - a.retirement_sequence);
|
|
@@ -47092,7 +47392,7 @@ async function replayPermanentDocuments(args) {
|
|
|
47092
47392
|
for (const row of rows) {
|
|
47093
47393
|
if (row.deleted_at !== null && row.permanent_delete === true)
|
|
47094
47394
|
continue;
|
|
47095
|
-
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips);
|
|
47395
|
+
const outcome = applyRow(args.workspaceSlug, args.strategy, row, state, args.output, args.suppressor, args.now, args.isFrozen, args.loggedTombstoneSkips, args.recentUnlinks);
|
|
47096
47396
|
state = outcome.state;
|
|
47097
47397
|
if (outcome.action === "written")
|
|
47098
47398
|
counters.written++;
|
|
@@ -47103,9 +47403,12 @@ async function replayPermanentDocuments(args) {
|
|
|
47103
47403
|
else if (outcome.action === "slug_refused") {
|
|
47104
47404
|
counters.refused = (counters.refused ?? 0) + 1;
|
|
47105
47405
|
completePass = false;
|
|
47406
|
+
} else if (outcome.action === "restore_deferred") {
|
|
47407
|
+
restoresDeferred++;
|
|
47408
|
+
completePass = false;
|
|
47106
47409
|
}
|
|
47107
47410
|
}
|
|
47108
|
-
return { state, counters, cursor: resultingCursor, completed, completePass };
|
|
47411
|
+
return { state, counters, cursor: resultingCursor, completed, completePass, restoresDeferred };
|
|
47109
47412
|
}
|
|
47110
47413
|
async function runSync(workspaceSlug, deps = {}) {
|
|
47111
47414
|
const out = new Output({
|
|
@@ -47226,6 +47529,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47226
47529
|
committed = state;
|
|
47227
47530
|
};
|
|
47228
47531
|
const counters = {};
|
|
47532
|
+
let restoresDeferred = 0;
|
|
47229
47533
|
try {
|
|
47230
47534
|
for (const entityType of allEntityTypes()) {
|
|
47231
47535
|
const strategy = requireStrategy(entityType);
|
|
@@ -47247,6 +47551,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47247
47551
|
now,
|
|
47248
47552
|
isFrozen: deps.isFrozen,
|
|
47249
47553
|
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47554
|
+
recentUnlinks: deps.recentUnlinks,
|
|
47250
47555
|
commit: (before, after) => {
|
|
47251
47556
|
const next = commitState(before, after);
|
|
47252
47557
|
committed = next;
|
|
@@ -47256,8 +47561,10 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47256
47561
|
state = replay.state;
|
|
47257
47562
|
commitPending();
|
|
47258
47563
|
counters[entityType] = replay.counters;
|
|
47564
|
+
restoresDeferred += replay.restoresDeferred;
|
|
47259
47565
|
if (replay.completePass) {
|
|
47260
47566
|
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47567
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47261
47568
|
for (const item of replay.completed) {
|
|
47262
47569
|
clearFeedRefresh({
|
|
47263
47570
|
workspaceSlug: slug,
|
|
@@ -47270,8 +47577,11 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47270
47577
|
continue;
|
|
47271
47578
|
}
|
|
47272
47579
|
}
|
|
47273
|
-
|
|
47274
|
-
let
|
|
47580
|
+
const storedMark = entry2.sync_high_water?.[entityType] ?? null;
|
|
47581
|
+
let walkStart = deps.useHighWater === true && storedMark !== null ? storedMark : startCursor;
|
|
47582
|
+
let cursor = walkStart;
|
|
47583
|
+
let lastAdvancedCursor = walkStart;
|
|
47584
|
+
let finalHighWater = null;
|
|
47275
47585
|
let cursorBlocked = false;
|
|
47276
47586
|
let permanentEncountered = false;
|
|
47277
47587
|
const permanentJournalBlocked = entityType === "documents" && dueDocumentRefreshMarkers(slug, now()) === null;
|
|
@@ -47287,13 +47597,26 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47287
47597
|
refused: 0
|
|
47288
47598
|
};
|
|
47289
47599
|
while (true) {
|
|
47290
|
-
|
|
47291
|
-
|
|
47292
|
-
|
|
47293
|
-
|
|
47294
|
-
|
|
47295
|
-
|
|
47296
|
-
|
|
47600
|
+
let page;
|
|
47601
|
+
try {
|
|
47602
|
+
page = await syncEntity({
|
|
47603
|
+
strategy,
|
|
47604
|
+
baseUrl: entry2.api_base_url,
|
|
47605
|
+
pat,
|
|
47606
|
+
cursor,
|
|
47607
|
+
fetchImpl
|
|
47608
|
+
});
|
|
47609
|
+
} catch (err) {
|
|
47610
|
+
if (cursor === walkStart && walkStart !== startCursor && err instanceof ApiError && err.status === 400) {
|
|
47611
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47612
|
+
out.warn(`${entityType}: the stored high-water mark was refused; reading from the cursor instead.`);
|
|
47613
|
+
walkStart = startCursor;
|
|
47614
|
+
cursor = startCursor;
|
|
47615
|
+
lastAdvancedCursor = startCursor;
|
|
47616
|
+
continue;
|
|
47617
|
+
}
|
|
47618
|
+
throw err;
|
|
47619
|
+
}
|
|
47297
47620
|
for (const row of page.rows) {
|
|
47298
47621
|
ctr.fetched++;
|
|
47299
47622
|
if (entityType === "documents" && row.deleted_at !== null && row.permanent_delete === true && row.purge_generation !== undefined) {
|
|
@@ -47314,7 +47637,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47314
47637
|
}
|
|
47315
47638
|
let outcome;
|
|
47316
47639
|
try {
|
|
47317
|
-
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips);
|
|
47640
|
+
outcome = applyRow(slug, strategy, row, state, out, deps.suppressor, now, deps.isFrozen, deps.loggedTombstoneSkips, deps.recentUnlinks);
|
|
47318
47641
|
} catch (err) {
|
|
47319
47642
|
if (!(err instanceof MirrorSymlinkError))
|
|
47320
47643
|
throw err;
|
|
@@ -47337,14 +47660,19 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47337
47660
|
ctr.deleted++;
|
|
47338
47661
|
else if (outcome.action === "moved")
|
|
47339
47662
|
ctr.moved = (ctr.moved ?? 0) + 1;
|
|
47340
|
-
else if (outcome.action === "
|
|
47663
|
+
else if (outcome.action === "restore_deferred") {
|
|
47664
|
+
cursorBlocked = true;
|
|
47665
|
+
restoresDeferred++;
|
|
47666
|
+
} else if (outcome.action === "slug_refused") {
|
|
47341
47667
|
ctr.refused = (ctr.refused ?? 0) + 1;
|
|
47342
47668
|
cursorBlocked = true;
|
|
47343
47669
|
}
|
|
47344
47670
|
}
|
|
47345
47671
|
commitPending();
|
|
47346
|
-
if (page.next_cursor === null)
|
|
47672
|
+
if (page.next_cursor === null) {
|
|
47673
|
+
finalHighWater = page.high_water ?? null;
|
|
47347
47674
|
break;
|
|
47675
|
+
}
|
|
47348
47676
|
if (!cursorBlocked)
|
|
47349
47677
|
lastAdvancedCursor = page.next_cursor;
|
|
47350
47678
|
cursor = page.next_cursor;
|
|
@@ -47368,6 +47696,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47368
47696
|
now,
|
|
47369
47697
|
isFrozen: deps.isFrozen,
|
|
47370
47698
|
loggedTombstoneSkips: deps.loggedTombstoneSkips,
|
|
47699
|
+
recentUnlinks: deps.recentUnlinks,
|
|
47371
47700
|
commit: (before, after) => {
|
|
47372
47701
|
const next = commitState(before, after);
|
|
47373
47702
|
committed = next;
|
|
@@ -47377,8 +47706,10 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47377
47706
|
state = replay.state;
|
|
47378
47707
|
commitPending();
|
|
47379
47708
|
counters[entityType] = replay.counters;
|
|
47709
|
+
restoresDeferred += replay.restoresDeferred;
|
|
47380
47710
|
if (replay.completePass) {
|
|
47381
47711
|
updateSyncCursor(slug, entityType, replay.cursor);
|
|
47712
|
+
updateSyncHighWater(slug, entityType, null);
|
|
47382
47713
|
for (const item of replay.completed) {
|
|
47383
47714
|
clearFeedRefresh({
|
|
47384
47715
|
workspaceSlug: slug,
|
|
@@ -47390,12 +47721,21 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47390
47721
|
}
|
|
47391
47722
|
continue;
|
|
47392
47723
|
}
|
|
47393
|
-
if (lastAdvancedCursor !==
|
|
47724
|
+
if (walkStart === startCursor && lastAdvancedCursor !== walkStart) {
|
|
47394
47725
|
updateSyncCursor(slug, entityType, lastAdvancedCursor);
|
|
47395
47726
|
}
|
|
47727
|
+
const mark = provenMark({
|
|
47728
|
+
clean: !cursorBlocked,
|
|
47729
|
+
finalHighWater,
|
|
47730
|
+
advancedTo: lastAdvancedCursor !== walkStart ? lastAdvancedCursor : null
|
|
47731
|
+
});
|
|
47732
|
+
if (mark !== null)
|
|
47733
|
+
updateSyncHighWater(slug, entityType, mark);
|
|
47396
47734
|
}
|
|
47397
47735
|
const needsDerivedTextRewalk = state.derived_text_version !== DERIVED_TEXT_VERSION;
|
|
47398
|
-
const
|
|
47736
|
+
const filesBaseCursor = needsDerivedTextRewalk ? null : entry2.sync_cursors?.["files"] ?? null;
|
|
47737
|
+
const filesMark = entry2.sync_high_water?.["files"] ?? null;
|
|
47738
|
+
const filesStartCursor = !needsDerivedTextRewalk && deps.useHighWater === true && filesMark !== null ? filesMark : filesBaseCursor;
|
|
47399
47739
|
if (needsDerivedTextRewalk) {
|
|
47400
47740
|
out.info("Re-reading the file index once to refresh extracted text (template changed).");
|
|
47401
47741
|
}
|
|
@@ -47405,6 +47745,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47405
47745
|
pat,
|
|
47406
47746
|
state,
|
|
47407
47747
|
cursor: filesStartCursor,
|
|
47748
|
+
baseCursor: filesBaseCursor,
|
|
47749
|
+
recentUnlinks: deps.recentUnlinks,
|
|
47408
47750
|
fetchImpl,
|
|
47409
47751
|
now,
|
|
47410
47752
|
output: out,
|
|
@@ -47418,7 +47760,8 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47418
47760
|
if (filesResult.state !== state) {
|
|
47419
47761
|
state = filesResult.state;
|
|
47420
47762
|
}
|
|
47421
|
-
|
|
47763
|
+
restoresDeferred += filesResult.restoresDeferred ?? 0;
|
|
47764
|
+
if (needsDerivedTextRewalk && filesResult.textUnknown === 0 && (filesResult.restoresDeferred ?? 0) === 0) {
|
|
47422
47765
|
state = { ...state, derived_text_version: DERIVED_TEXT_VERSION };
|
|
47423
47766
|
}
|
|
47424
47767
|
counters["files"] = {
|
|
@@ -47427,10 +47770,19 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47427
47770
|
deleted: filesResult.deleted + filesResult.textDeleted
|
|
47428
47771
|
};
|
|
47429
47772
|
commitPending();
|
|
47430
|
-
|
|
47773
|
+
const filesWalkStart = filesResult.markRefused ? filesBaseCursor : filesStartCursor;
|
|
47774
|
+
const filesWalkedFromCursor = filesWalkStart === filesBaseCursor || filesResult.permanentReplayComplete === true;
|
|
47775
|
+
if (filesWalkedFromCursor && filesResult.lastCursor !== filesBaseCursor) {
|
|
47431
47776
|
updateSyncCursor(slug, "files", filesResult.lastCursor);
|
|
47432
47777
|
}
|
|
47778
|
+
if (filesResult.markRefused) {
|
|
47779
|
+
updateSyncHighWater(slug, "files", null);
|
|
47780
|
+
out.warn("files: the stored high-water mark was refused; reading from the cursor instead.");
|
|
47781
|
+
}
|
|
47782
|
+
if (filesResult.highWater)
|
|
47783
|
+
updateSyncHighWater(slug, "files", filesResult.highWater);
|
|
47433
47784
|
if (filesResult.permanentReplayComplete) {
|
|
47785
|
+
updateSyncHighWater(slug, "files", null);
|
|
47434
47786
|
for (const item of filesResult.permanentRefreshes ?? []) {
|
|
47435
47787
|
clearFeedRefresh({
|
|
47436
47788
|
workspaceSlug: slug,
|
|
@@ -47618,7 +47970,7 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47618
47970
|
const totalRefused = Object.values(counters).reduce((sum, c) => sum + (c.refused ?? 0), 0);
|
|
47619
47971
|
if (totalWritten === 0 && totalDeleted === 0 && totalMoved === 0 && totalRefused === 0) {
|
|
47620
47972
|
out.success("Already up to date.");
|
|
47621
|
-
return { exitCode: 0, applied: 0 };
|
|
47973
|
+
return { exitCode: 0, applied: 0, restoresDeferred };
|
|
47622
47974
|
}
|
|
47623
47975
|
const elapsed = ((now().getTime() - startedAt.getTime()) / 1000).toFixed(1);
|
|
47624
47976
|
for (const [type, c] of Object.entries(counters)) {
|
|
@@ -47636,9 +47988,9 @@ async function runSync(workspaceSlug, deps = {}) {
|
|
|
47636
47988
|
out.info(` ${type}: ${parts.join(", ")}`);
|
|
47637
47989
|
}
|
|
47638
47990
|
out.success(`Done in ${elapsed}s.`);
|
|
47639
|
-
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted };
|
|
47991
|
+
return { exitCode: 0, applied: totalWritten + totalMoved + totalDeleted, restoresDeferred };
|
|
47640
47992
|
}
|
|
47641
|
-
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips) {
|
|
47993
|
+
function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = () => new Date, isFrozen, loggedTombstoneSkips, recentUnlinks) {
|
|
47642
47994
|
if (isFrozen?.(strategy.type, row.id)) {
|
|
47643
47995
|
return { state, action: "skipped" };
|
|
47644
47996
|
}
|
|
@@ -47750,7 +48102,22 @@ function applyRow(workspaceSlug, strategy, row, state, out, suppressor, now = ()
|
|
|
47750
48102
|
const writeSlug = relocation.writeSlug;
|
|
47751
48103
|
if (existingEntry && existingEntry.updated_at === live.updated_at && relocation.kind === "not_needed") {
|
|
47752
48104
|
const path5 = entityFilePath(workspaceSlug, strategy.type, live.slug);
|
|
47753
|
-
|
|
48105
|
+
const present2 = existsSync9(path5);
|
|
48106
|
+
if (!present2) {
|
|
48107
|
+
const verdict = deferRestore(recentUnlinks, path5, live.updated_at, now().getTime());
|
|
48108
|
+
if (verdict.defer) {
|
|
48109
|
+
if (verdict.first) {
|
|
48110
|
+
out.log(entityRestoreDeferredLine({
|
|
48111
|
+
entityType: strategy.type,
|
|
48112
|
+
slug: live.slug,
|
|
48113
|
+
waitSeconds: RESTORE_DEFERRAL_MS / 1000
|
|
48114
|
+
}));
|
|
48115
|
+
}
|
|
48116
|
+
return { state, action: "restore_deferred" };
|
|
48117
|
+
}
|
|
48118
|
+
}
|
|
48119
|
+
if (present2) {
|
|
48120
|
+
presentAgain(recentUnlinks, path5);
|
|
47754
48121
|
if (strategy.type === "artists") {
|
|
47755
48122
|
const artistLive = live;
|
|
47756
48123
|
state = applyArtistMemoryOnPull({
|
|
@@ -49187,13 +49554,18 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49187
49554
|
return { exitCode: 0 };
|
|
49188
49555
|
}
|
|
49189
49556
|
writeDaemonPid(slug, process.pid, (deps.now ?? (() => new Date))());
|
|
49190
|
-
const
|
|
49557
|
+
const ambient = createAmbientPollWiring({
|
|
49558
|
+
now: () => (deps.now?.() ?? new Date).getTime(),
|
|
49559
|
+
baseReadIntervalMs: baseReadIntervalMs()
|
|
49560
|
+
});
|
|
49561
|
+
const watcher = startWatcher({ workspaceSlug: slug, getStateEntry, classifyPath, onUnlink: ambient.onUnlink }, handler);
|
|
49191
49562
|
const quietSink = discardSink();
|
|
49192
49563
|
let draining = false;
|
|
49193
49564
|
let lastHeartbeatMs = 0;
|
|
49194
49565
|
const pullLoop = new PullLoop({
|
|
49195
49566
|
runOnce: async () => {
|
|
49196
|
-
const result = await runSync(slug, {
|
|
49567
|
+
const result = await ambient.poll((sc2) => runSync(slug, {
|
|
49568
|
+
...sc2,
|
|
49197
49569
|
fetchImpl,
|
|
49198
49570
|
keychain,
|
|
49199
49571
|
now: deps.now,
|
|
@@ -49205,7 +49577,7 @@ async function runStart(workspaceSlug, deps = {}) {
|
|
|
49205
49577
|
isFileFrozen,
|
|
49206
49578
|
loggedTombstoneSkips,
|
|
49207
49579
|
loggedStashNotices
|
|
49208
|
-
});
|
|
49580
|
+
}));
|
|
49209
49581
|
if (result.exitCode === 0) {
|
|
49210
49582
|
const applied = result.applied ?? 0;
|
|
49211
49583
|
if (applied > 0) {
|