@mulmoclaude/core 3.2.0 → 3.4.0
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/assets/helps/error-recovery.md +71 -0
- package/assets/helps/google-calendar-collection.md +10 -0
- package/dist/collection/registry/server/index.cjs +3 -3
- package/dist/collection/registry/server/index.cjs.map +1 -1
- package/dist/collection/registry/server/index.js +3 -3
- package/dist/collection/registry/server/index.js.map +1 -1
- package/dist/collection/server/discovery.d.ts +10 -2
- package/dist/collection/server/host.d.ts +27 -3
- package/dist/collection/server/index.cjs +2 -2
- package/dist/collection/server/index.js +2 -2
- package/dist/collection-watchers/index.cjs +2 -2
- package/dist/collection-watchers/index.js +2 -2
- package/dist/{discovery-CDOjfctD.cjs → discovery-CrA_EqUJ.cjs} +19 -7
- package/dist/discovery-CrA_EqUJ.cjs.map +1 -0
- package/dist/{discovery-CC7kuwHf.js → discovery-D7LCN8gJ.js} +19 -7
- package/dist/discovery-D7LCN8gJ.js.map +1 -0
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/calendar.d.ts +15 -0
- package/dist/google/calendarBackfillState.d.ts +19 -0
- package/dist/google/calendarSyncStore.d.ts +15 -0
- package/dist/google/collectionSync.d.ts +78 -11
- package/dist/google/index.cjs +207 -35
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +4 -3
- package/dist/google/index.js +197 -35
- package/dist/google/index.js.map +1 -1
- package/dist/{server-WU35UYlF.cjs → server-CorfPQVQ.cjs} +2 -2
- package/dist/{server-WU35UYlF.cjs.map → server-CorfPQVQ.cjs.map} +1 -1
- package/dist/{server-AA2kfDfk.js → server-D6QY853r.js} +2 -2
- package/dist/{server-AA2kfDfk.js.map → server-D6QY853r.js.map} +1 -1
- package/package.json +1 -1
- package/dist/discovery-CC7kuwHf.js.map +0 -1
- package/dist/discovery-CDOjfctD.cjs.map +0 -1
package/dist/google/index.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ export { brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, type BrokerS
|
|
|
7
7
|
export { authorizeGoogle, commitLinkedTokens, getGoogleAccessToken, unlinkGoogle, waitForAuthCode, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, type AuthorizeGoogleOptions, type RevokeFetch, } from './auth.js';
|
|
8
8
|
export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
|
|
9
9
|
export { googleApiError, isGoogleApiError, GoogleApiError, DEFAULT_LIST_MAX_RESULTS, HTTP_FORBIDDEN, MAX_LIST_RESULTS } from './apiClient.js';
|
|
10
|
-
export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, } from './calendar.js';
|
|
11
|
-
export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, } from './calendarSyncStore.js';
|
|
10
|
+
export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, syncPageParams, } from './calendar.js';
|
|
11
|
+
export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, toolCalendarSyncKey, } from './calendarSyncStore.js';
|
|
12
12
|
export { calendarSyncDueWindowMs, isCalendarSyncDue } from './calendarSyncDue.js';
|
|
13
|
+
export { calendarBackfillPath, markCalendarBackfilled, needsCalendarBackfill } from './calendarBackfillState.js';
|
|
13
14
|
export { stateLockPath, withCalendarStateLock, liveLockClock, type LockClock } from './calendarStateLock.js';
|
|
14
15
|
export { calendarPushStatePath, clearCalendarShadow, loadCalendarShadow, mergeShadow, saveCalendarShadow, toShadowEvent, type ShadowEvent, } from './calendarPushState.js';
|
|
15
16
|
export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, unsentLocalEdits, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
|
|
@@ -19,6 +20,6 @@ export { toCollectionDateTime } from './collectionDateTime.js';
|
|
|
19
20
|
export { withCalendarLock, withKeyedLock } from './calendarLock.js';
|
|
20
21
|
export { toCollectionRecord, type GoogleCalendarSourceField } from './collectionProjection.js';
|
|
21
22
|
export { mergeIntoExisting } from '../collection/core/project.js';
|
|
22
|
-
export { googleCalendarSyncTaskDef, classifyDelete, classifyWrite, anySyncedCollectionSurvives, applyPlanFor, groupByCalendar, allUnpushed, unsentEditGuard, heldBack, orphanedCalendarId, pullableEvents, pullProtectionFor, pushAndProtect, releaseOrphanedCalendarToken, unpushedFor, PROTECTION_UNKNOWN, type ClaimGuard, type PullProtectionDeps, type UnpushedBySlug, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, shadowUpdates,
|
|
23
|
+
export { googleCalendarSyncTaskDef, classifyDelete, classifyWrite, anySyncedCollectionSurvives, applyPlanFor, groupByCalendar, allUnpushed, unsentEditGuard, heldBack, orphanedCalendarId, pullableEvents, pullProtectionFor, pushAndProtect, releaseOrphanedCalendarToken, unpushedFor, PROTECTION_UNKNOWN, type ClaimGuard, type PullProtectionDeps, type UnpushedBySlug, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, shadowUpdates, collectionsNeedingBackfill, resumableToken, windowAdvance, type WindowAdvance, groupsNeedingBackfill, withPartialWindowError, PARTIAL_CALENDAR_WINDOW, GOOGLE_CALENDAR_SYNC_TASK_ID, type CalendarCollectionSyncResult, type CalendarDeclaring, type ManualCalendarSyncDeps, type ManualCalendarSyncOutcome, } from './collectionSync.js';
|
|
23
24
|
export { buildTaskPatch, canonicalTaskListId, completeTask, createTask, deleteTask, listTaskLists, listTasks, toTaskListSummary, toTaskSummary, uncompleteTask, updateTask, type CompleteTaskInput, type CreateTaskInput, type ListTasksInput, type TaskListSummary, type TaskSummary, type UpdateTaskInput, } from './tasks.js';
|
|
24
25
|
export { assertSafeMimeType, buildMultipartBody, createDriveFile, deleteDriveFile, isTextMimeType, listDriveFiles, pickBoundary, readDriveFile, toDriveFileSummary, type CreateDriveFileInput, type DriveFileSummary, type ListDriveFilesInput, type ReadDriveFileInput, } from './driveFile.js';
|
package/dist/google/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { n as writeFileAtomic } from "../root-BMroU_mB.js";
|
|
|
4
4
|
import { fetchWithTimeout } from "../utils/fetch.js";
|
|
5
5
|
import { l as parseIsoDate, u as parseIsoDateTime } from "../calendarGrid-Csy2rpjp.js";
|
|
6
6
|
import { t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
|
|
7
|
-
import { Y as getWorkspaceRoot, at as createHostSlot, d as storeFor, it as createForwardingLogger, n as discoverCollections } from "../discovery-
|
|
7
|
+
import { Y as getWorkspaceRoot, at as createHostSlot, d as storeFor, it as createForwardingLogger, n as discoverCollections } from "../discovery-D7LCN8gJ.js";
|
|
8
8
|
import { t as mulmoConfigDir } from "../paths-CkJxNZ7T.js";
|
|
9
9
|
import { promises } from "node:fs";
|
|
10
10
|
import path, { join } from "node:path";
|
|
@@ -762,33 +762,42 @@ async function fetchSyncPage(accessToken, calendarId, params) {
|
|
|
762
762
|
*
|
|
763
763
|
* `nextSyncToken` is only present on the LAST page, so every page must be
|
|
764
764
|
* walked before the token is worth storing. */
|
|
765
|
+
/** The query for one page of the walk. `showDeleted` must stay true or
|
|
766
|
+
* deletions would be invisible; `pageToken` is sent ALONGSIDE `syncToken`,
|
|
767
|
+
* which is what Google requires to page an incremental sync. */
|
|
768
|
+
function syncPageParams(input, pageToken) {
|
|
769
|
+
const params = new URLSearchParams({
|
|
770
|
+
singleEvents: "true",
|
|
771
|
+
showDeleted: "true",
|
|
772
|
+
maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
|
|
773
|
+
});
|
|
774
|
+
if (input.syncToken) params.set("syncToken", input.syncToken);
|
|
775
|
+
if (pageToken) params.set("pageToken", pageToken);
|
|
776
|
+
return params;
|
|
777
|
+
}
|
|
765
778
|
async function syncCalendarEvents(accessToken, input = {}) {
|
|
766
779
|
const events = [];
|
|
767
780
|
let pageToken;
|
|
768
781
|
let nextSyncToken;
|
|
769
782
|
for (let page = 0; page < MAX_EVENT_SYNC_PAGES; page += 1) {
|
|
770
|
-
const
|
|
771
|
-
singleEvents: "true",
|
|
772
|
-
showDeleted: "true",
|
|
773
|
-
maxResults: String(input.maxResults ?? EVENT_SYNC_PAGE_SIZE)
|
|
774
|
-
});
|
|
775
|
-
if (input.syncToken) params.set("syncToken", input.syncToken);
|
|
776
|
-
if (pageToken) params.set("pageToken", pageToken);
|
|
777
|
-
const payload = await fetchSyncPage(accessToken, input.calendarId, params);
|
|
783
|
+
const payload = await fetchSyncPage(accessToken, input.calendarId, syncPageParams(input, pageToken));
|
|
778
784
|
if (payload === GONE) return {
|
|
779
785
|
events: [],
|
|
780
|
-
fullResyncRequired: true
|
|
786
|
+
fullResyncRequired: true,
|
|
787
|
+
pagesExhausted: false
|
|
781
788
|
};
|
|
782
789
|
const record = asRecord(payload);
|
|
783
790
|
events.push(...itemsOf(payload).map(toEventSummary));
|
|
784
791
|
nextSyncToken = stringField(record, "nextSyncToken") || void 0;
|
|
785
792
|
pageToken = stringField(record, "nextPageToken") || void 0;
|
|
786
793
|
if (!pageToken) break;
|
|
794
|
+
nextSyncToken = void 0;
|
|
787
795
|
}
|
|
788
796
|
return {
|
|
789
797
|
events,
|
|
790
798
|
nextSyncToken,
|
|
791
|
-
fullResyncRequired: false
|
|
799
|
+
fullResyncRequired: false,
|
|
800
|
+
pagesExhausted: pageToken !== void 0
|
|
792
801
|
};
|
|
793
802
|
}
|
|
794
803
|
/** Pagination loop for CalendarList.list, extracted so it can be tested without
|
|
@@ -1013,6 +1022,21 @@ function calendarSyncStatePath(workspaceRoot) {
|
|
|
1013
1022
|
return path.join(workspaceRoot ?? getWorkspaceRoot(), "data", "calendar", ".sync-state.json");
|
|
1014
1023
|
}
|
|
1015
1024
|
var calendarKey = canonicalCalendarId;
|
|
1025
|
+
/** The key for a consumer that reads a calendar without KEEPING what it reads.
|
|
1026
|
+
*
|
|
1027
|
+
* A sync token is a claim about which events the consumer already holds, but
|
|
1028
|
+
* the key above names only the calendar — so every consumer of one calendar
|
|
1029
|
+
* shares one cursor. That is right for the collections bound to it (one fetch
|
|
1030
|
+
* fans out to all of them in the same pass) and wrong for the standalone
|
|
1031
|
+
* `google` tool's `calendarSync`, which walks the calendar, reports a summary
|
|
1032
|
+
* to the LLM and discards the events.
|
|
1033
|
+
*
|
|
1034
|
+
* Sharing it lost data both ways (#2850): the tool consumed windows the
|
|
1035
|
+
* collections then never received, and a collection created afterwards resumed
|
|
1036
|
+
* from the tool's cursor — so its "first sync" was a delta of a window it had
|
|
1037
|
+
* never held, and the calendar's history never arrived. Same file, separate
|
|
1038
|
+
* keyspace. Idempotent through `calendarKey`, since the result is non-empty. */
|
|
1039
|
+
var toolCalendarSyncKey = (calendarId) => `tool:${canonicalCalendarId(calendarId)}`;
|
|
1016
1040
|
/** Read one map out of whatever is on disk. Tolerant on purpose: a file written
|
|
1017
1041
|
* before `lastSyncedAt` existed simply lacks the key, and a hand-edited one may
|
|
1018
1042
|
* hold anything. A dropped entry costs a full re-walk or a duplicate run, never
|
|
@@ -1138,6 +1162,46 @@ function isCalendarSyncDue(lastSyncedAt, windowMs, now = Date.now()) {
|
|
|
1138
1162
|
return elapsed >= windowMs;
|
|
1139
1163
|
}
|
|
1140
1164
|
//#endregion
|
|
1165
|
+
//#region src/google/calendarBackfillState.ts
|
|
1166
|
+
var BACKFILL_STATE_MODE = 384;
|
|
1167
|
+
var BACKFILL_FILE = ".calendar-sync.json";
|
|
1168
|
+
/** `<dataDir>/.calendar-sync.json` */
|
|
1169
|
+
var calendarBackfillPath = (dataDir) => path.join(dataDir, BACKFILL_FILE);
|
|
1170
|
+
/** Whether these records still need the whole calendar walked into them.
|
|
1171
|
+
*
|
|
1172
|
+
* Compared against the calendar the marker NAMES, not merely its presence: a
|
|
1173
|
+
* schema repointed at another calendar holds a backfill of the wrong one, and
|
|
1174
|
+
* resuming that calendar's cursor would leave the same silent gap this exists
|
|
1175
|
+
* to close.
|
|
1176
|
+
*
|
|
1177
|
+
* Unreadable, absent or INCOMPLETE all answer "yes" — every field must be
|
|
1178
|
+
* there, not just the one being compared. A redundant full walk costs API
|
|
1179
|
+
* calls and rewrites records that already match; skipping a needed one loses
|
|
1180
|
+
* the history silently, which is the failure being fixed, so the bias only
|
|
1181
|
+
* ever goes one way (CodeRabbit review #2853). */
|
|
1182
|
+
async function needsCalendarBackfill(dataDir, calendarId) {
|
|
1183
|
+
const stored = await readJsonOrNull(calendarBackfillPath(dataDir));
|
|
1184
|
+
if (typeof stored?.walkedAt !== "string" || stored.walkedAt === "") return true;
|
|
1185
|
+
return stored.calendarId !== canonicalCalendarId(calendarId);
|
|
1186
|
+
}
|
|
1187
|
+
/** Record that these records now cover the whole calendar. Never throws: the
|
|
1188
|
+
* marker is an optimisation over re-walking, so a workspace that cannot write
|
|
1189
|
+
* it syncs correctly and simply walks in full again next time. */
|
|
1190
|
+
async function markCalendarBackfilled(dataDir, calendarId, walkedAt) {
|
|
1191
|
+
const state = {
|
|
1192
|
+
calendarId: canonicalCalendarId(calendarId),
|
|
1193
|
+
walkedAt
|
|
1194
|
+
};
|
|
1195
|
+
try {
|
|
1196
|
+
await writeJsonAtomicWithMode(calendarBackfillPath(dataDir), state, BACKFILL_STATE_MODE);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
log.warn("google", "could not record the calendar backfill marker — the next sync will walk the calendar again", {
|
|
1199
|
+
dataDir,
|
|
1200
|
+
error: String(error)
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
//#endregion
|
|
1141
1205
|
//#region src/google/calendarPushState.ts
|
|
1142
1206
|
var PUSH_STATE_MODE = 384;
|
|
1143
1207
|
/** `<workspace>/data/calendar/.push-state.json` */
|
|
@@ -2183,21 +2247,114 @@ async function releaseCalendarSyncClaim(calendarId, workspaceRoot) {
|
|
|
2183
2247
|
* would advance the baseline past a record the pull deliberately left holding a
|
|
2184
2248
|
* local edit, which is the silent overwrite this whole path exists to stop. */
|
|
2185
2249
|
var heldBack = (unpushed, results) => /* @__PURE__ */ new Set([...allUnpushed(unpushed), ...results.flatMap((result) => result.withheld)]);
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2250
|
+
/** The collections in this group that have never received the whole calendar,
|
|
2251
|
+
* by slug. Asked of each collection's OWN records (`calendarBackfillState.ts`),
|
|
2252
|
+
* because the sync token cannot answer it — it is keyed by calendar and shared
|
|
2253
|
+
* by every consumer of that calendar (#2850). */
|
|
2254
|
+
async function collectionsNeedingBackfill(collections) {
|
|
2255
|
+
return (await Promise.all(collections.map(async (collection) => {
|
|
2256
|
+
return await needsCalendarBackfill(collection.dataDir, collection.schema.googleCalendar?.calendarId) ? collection.slug : null;
|
|
2257
|
+
}))).filter((slug) => slug !== null);
|
|
2258
|
+
}
|
|
2259
|
+
/** The token this run may resume from — none while any collection in the group
|
|
2260
|
+
* still needs the whole calendar.
|
|
2261
|
+
*
|
|
2262
|
+
* A stored token says how far THE CALENDAR has been read, by whoever read it:
|
|
2263
|
+
* the standalone `google` tool's `calendarSync`, or a sibling collection that
|
|
2264
|
+
* synced before this one existed. Resuming from it hands a brand-new collection
|
|
2265
|
+
* a delta of a window it never received — a handful of records, reported as a
|
|
2266
|
+
* success with no error, and never the history the docs promise (#2850).
|
|
2267
|
+
*
|
|
2268
|
+
* The full walk fans out to the whole group, which is free: writes are upserts,
|
|
2269
|
+
* so the collections that were already current simply rewrite what they hold. */
|
|
2270
|
+
async function resumableToken(calendarId, collections, workspaceRoot) {
|
|
2271
|
+
const pending = await collectionsNeedingBackfill(collections);
|
|
2272
|
+
if (pending.length === 0) return await loadCalendarSyncToken(calendarId, workspaceRoot) ?? void 0;
|
|
2273
|
+
log.info("google", "walking the whole calendar — these collections have never received it", {
|
|
2274
|
+
calendarId,
|
|
2275
|
+
collections: pending
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
/** Record the backfill for every collection the window landed in. Called only
|
|
2279
|
+
* after a FULL walk that fully landed: marking one after an incremental window
|
|
2280
|
+
* would claim a history the records do not hold. */
|
|
2281
|
+
async function markGroupBackfilled(calendarId, collections) {
|
|
2282
|
+
const walkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2283
|
+
await Promise.all(collections.map((collection) => markCalendarBackfilled(collection.dataDir, collection.schema.googleCalendar?.calendarId, walkedAt)));
|
|
2284
|
+
}
|
|
2285
|
+
/** What a partial window reports. Names the cause the user can actually act on:
|
|
2286
|
+
* a page guard is reached when Google has to expand an enormous number of
|
|
2287
|
+
* recurring instances, and one series with no end date is enough. */
|
|
2288
|
+
var PARTIAL_CALENDAR_WINDOW = "Google returned more pages of events than one sync pass walks, so only part of the calendar was copied. Give any recurring event with no end date a finite end date, then sync again.";
|
|
2289
|
+
/** A truncated walk must not read as a completed one (#2850).
|
|
2290
|
+
*
|
|
2291
|
+
* Added AFTER the token/baseline gate, not through it. A short fetch is not a
|
|
2292
|
+
* failed write: the events that did arrive are Google's own and landed
|
|
2293
|
+
* correctly, so their baseline is true and holding it back would freeze them —
|
|
2294
|
+
* a record with no baseline reads as an unsent local edit, which the next pull
|
|
2295
|
+
* then refuses to touch (#2683). What must not happen is the walk claiming
|
|
2296
|
+
* completeness, and that is covered without the gate: `nextSyncToken` only
|
|
2297
|
+
* appears on Google's last page, so a truncated walk has no token to advance,
|
|
2298
|
+
* and the backfill marker is withheld separately. */
|
|
2299
|
+
var withPartialWindowError = (results, pagesExhausted) => pagesExhausted ? results.map((result) => ({
|
|
2300
|
+
...result,
|
|
2301
|
+
errors: [...result.errors, PARTIAL_CALENDAR_WINDOW]
|
|
2302
|
+
})) : [...results];
|
|
2303
|
+
/** `landed` is the write-side verdict (`windowFullyLanded`); the rest describe
|
|
2304
|
+
* the window Google returned.
|
|
2305
|
+
*
|
|
2306
|
+
* A page-capped walk (`pagesExhausted`) keeps its baseline — the events that
|
|
2307
|
+
* arrived are Google's own and were written correctly — but must NOT claim the
|
|
2308
|
+
* backfill, or the collection would stop asking for the rest of its calendar.
|
|
2309
|
+
* Its token is refused too, though Google makes that moot by sending
|
|
2310
|
+
* `nextSyncToken` only on the last page. */
|
|
2311
|
+
function windowAdvance(window) {
|
|
2312
|
+
if (!window.landed) return {
|
|
2313
|
+
baseline: false,
|
|
2314
|
+
backfill: false,
|
|
2315
|
+
token: false
|
|
2316
|
+
};
|
|
2317
|
+
const complete = !window.pagesExhausted;
|
|
2318
|
+
return {
|
|
2319
|
+
baseline: true,
|
|
2320
|
+
backfill: window.walkedInFull && complete,
|
|
2321
|
+
token: complete && window.nextSyncToken !== void 0
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
/** The window this run should read, and whether it was a full walk. A 410
|
|
2325
|
+
* restart is a full walk too, so it backfills just as well as a forced one. */
|
|
2326
|
+
async function readWindow(calendarId, collections, workspaceRoot) {
|
|
2189
2327
|
const accessToken = await getGoogleAccessToken();
|
|
2328
|
+
const resumeFrom = await resumableToken(calendarId, collections, workspaceRoot);
|
|
2190
2329
|
const first = await syncCalendarEvents(accessToken, {
|
|
2191
2330
|
calendarId,
|
|
2192
|
-
syncToken:
|
|
2331
|
+
syncToken: resumeFrom
|
|
2193
2332
|
});
|
|
2194
2333
|
const result = first.fullResyncRequired ? await restartFullSync(accessToken, calendarId, workspaceRoot) : first;
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2334
|
+
if (result.pagesExhausted) log.warn("google", "the calendar walk ran out of pages — only part of it was copied", {
|
|
2335
|
+
calendarId,
|
|
2336
|
+
events: result.events.length
|
|
2337
|
+
});
|
|
2338
|
+
return {
|
|
2339
|
+
result,
|
|
2340
|
+
walkedInFull: resumeFrom === void 0 || first.fullResyncRequired
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
async function syncCalendarGroupNow(calendarId, collections, workspaceRoot) {
|
|
2344
|
+
const unpushed = await pushAndProtect(collections, workspaceRoot);
|
|
2345
|
+
const baseline = await loadCalendarShadow(calendarId, workspaceRoot);
|
|
2346
|
+
const { result, walkedInFull } = await readWindow(calendarId, collections, workspaceRoot);
|
|
2347
|
+
const applied = await applyWindowToGroup(collections, result.events, workspaceRoot, unpushed, baseline);
|
|
2348
|
+
const advance = windowAdvance({
|
|
2349
|
+
landed: windowFullyLanded(calendarId, applied),
|
|
2350
|
+
walkedInFull,
|
|
2351
|
+
pagesExhausted: result.pagesExhausted,
|
|
2352
|
+
nextSyncToken: result.nextSyncToken
|
|
2353
|
+
});
|
|
2354
|
+
if (advance.baseline) await saveCalendarShadow(calendarId, shadowUpdates(result.events, heldBack(unpushed, applied), baseline), workspaceRoot);
|
|
2355
|
+
if (advance.backfill) await markGroupBackfilled(calendarId, collections);
|
|
2356
|
+
if (advance.token && result.nextSyncToken) await advanceToken(calendarId, result.nextSyncToken, collections, workspaceRoot);
|
|
2357
|
+
return withPartialWindowError(applied, result.pagesExhausted);
|
|
2201
2358
|
}
|
|
2202
2359
|
/** Apply one window to every collection on the calendar, honouring what each
|
|
2203
2360
|
* one's own push protected. A collection whose protection could not be worked
|
|
@@ -2433,24 +2590,29 @@ async function syncDueCalendarCollections(workspaceRoot, intervalMs = DEFAULT_SY
|
|
|
2433
2590
|
const windowMs = calendarSyncDueWindowMs(intervalMs);
|
|
2434
2591
|
return await runCalendarGroups(groups, workspaceRoot, (lastSyncedAt) => isCalendarSyncDue(lastSyncedAt, windowMs));
|
|
2435
2592
|
}
|
|
2436
|
-
/** The groups
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2593
|
+
/** The groups holding a collection that still needs the whole calendar.
|
|
2594
|
+
*
|
|
2595
|
+
* This used to ask whether the CALENDAR had a stored token, which is not the
|
|
2596
|
+
* same question and is why the trigger silently did nothing for the #2850
|
|
2597
|
+
* reporter: their calendar already had a token — from the standalone `google`
|
|
2598
|
+
* tool, and on later attempts from the collection they had just deleted by
|
|
2599
|
+
* hand — so the brand-new collection matched nothing and its first sync never
|
|
2600
|
+
* ran at all. `pending` is injected so the rule is testable without a
|
|
2601
|
+
* workspace on disk.
|
|
2439
2602
|
*
|
|
2440
|
-
*
|
|
2441
|
-
*
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
const checked = await Promise.all([...groups].map(async (entry) => await loadToken(entry[0]) === null ? entry : null));
|
|
2603
|
+
* Still self-silencing: a landed full walk marks its collections, so a group
|
|
2604
|
+
* stops matching once every collection in it holds the history. */
|
|
2605
|
+
async function groupsNeedingBackfill(groups, pending) {
|
|
2606
|
+
const checked = await Promise.all([...groups].map(async (entry) => await pending(entry[1]) ? entry : null));
|
|
2445
2607
|
return new Map(checked.filter((entry) => entry !== null));
|
|
2446
2608
|
}
|
|
2447
|
-
/** Sync only the calendars
|
|
2448
|
-
* just-created collection, which otherwise stays empty until the
|
|
2449
|
-
* scheduler run (#2427). Cheap and safe to call on every config write. */
|
|
2609
|
+
/** Sync only the calendars a collection has never received in full — the first
|
|
2610
|
+
* sync for a just-created collection, which otherwise stays empty until the
|
|
2611
|
+
* hourly scheduler run (#2427). Cheap and safe to call on every config write. */
|
|
2450
2612
|
async function syncNewCalendarCollections(workspaceRoot) {
|
|
2451
|
-
const pending = await
|
|
2613
|
+
const pending = await groupsNeedingBackfill(await declaringGroups(workspaceRoot), async (collections) => (await collectionsNeedingBackfill(collections)).length > 0);
|
|
2452
2614
|
if (!await backgroundSyncAllowed(pending)) return [];
|
|
2453
|
-
log.info("google", "running the first sync for
|
|
2615
|
+
log.info("google", "running the first sync for calendars a collection has never received in full", { calendars: [...pending.keys()] });
|
|
2454
2616
|
return await runCalendarGroups(pending, workspaceRoot);
|
|
2455
2617
|
}
|
|
2456
2618
|
var liveManualSyncDeps = {
|
|
@@ -2699,6 +2861,6 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
2699
2861
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
2700
2862
|
}
|
|
2701
2863
|
//#endregion
|
|
2702
|
-
export { CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, MAX_LIST_RESULTS, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, commitLinkedTokens, completeTask, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteTask, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, heldBack, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, orphanedCalendarId, pickBoundary, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits,
|
|
2864
|
+
export { CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, MAX_LIST_RESULTS, PARTIAL_CALENDAR_WINDOW, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarBackfillPath, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, collectionsNeedingBackfill, commitLinkedTokens, completeTask, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteTask, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, markCalendarBackfilled, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, needsCalendarBackfill, orphanedCalendarId, pickBoundary, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
|
|
2703
2865
|
|
|
2704
2866
|
//# sourceMappingURL=index.js.map
|