@mulmoclaude/core 5.0.1 → 5.3.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 +78 -0
- package/assets/helps/google-calendar-collection.md +130 -15
- package/assets/helps/google.md +14 -4
- package/dist/collection/core/schemaZ.d.ts +15 -2
- package/dist/collection/core/viewChatPolicy.d.ts +6 -3
- package/dist/collection/index.cjs +6 -3
- package/dist/collection/index.cjs.map +1 -1
- package/dist/collection/index.js +6 -3
- package/dist/collection/index.js.map +1 -1
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- 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-CBjI1lx9.js → discovery-B_64PBZU.js} +14 -3
- package/dist/{discovery-CBjI1lx9.js.map → discovery-B_64PBZU.js.map} +1 -1
- package/dist/{discovery-DCMW05Bp.cjs → discovery-CDXYswtw.cjs} +14 -3
- package/dist/{discovery-DCMW05Bp.cjs.map → discovery-CDXYswtw.cjs.map} +1 -1
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/calendar.d.ts +24 -0
- package/dist/google/collectionPush.d.ts +45 -2
- package/dist/google/deletePlan.d.ts +32 -0
- package/dist/google/eventDerived.d.ts +19 -0
- package/dist/google/eventSpanInput.d.ts +58 -0
- package/dist/google/index.cjs +286 -18
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +5 -2
- package/dist/google/index.js +272 -19
- package/dist/google/index.js.map +1 -1
- package/dist/remote-view/index.cjs +7 -7
- package/dist/remote-view/index.cjs.map +1 -1
- package/dist/remote-view/index.d.ts +3 -3
- package/dist/remote-view/index.js +7 -7
- package/dist/remote-view/index.js.map +1 -1
- package/dist/{server-BMZ_BcPH.js → server-7POrhHKv.js} +2 -2
- package/dist/{server-BMZ_BcPH.js.map → server-7POrhHKv.js.map} +1 -1
- package/dist/{server-rA9FSkl2.cjs → server-DfYwSX8b.cjs} +2 -2
- package/dist/{server-rA9FSkl2.cjs.map → server-DfYwSX8b.cjs.map} +1 -1
- package/package.json +1 -1
package/dist/google/index.cjs
CHANGED
|
@@ -7,7 +7,7 @@ const require_root = require("../root-BvwBUcol.cjs");
|
|
|
7
7
|
const require_utils_fetch = require("../utils/fetch.cjs");
|
|
8
8
|
const require_itemId = require("../itemId-CGT2J7YK.cjs");
|
|
9
9
|
const require_project = require("../project-C1ep9pvo.cjs");
|
|
10
|
-
const require_discovery = require("../discovery-
|
|
10
|
+
const require_discovery = require("../discovery-CDXYswtw.cjs");
|
|
11
11
|
const require_paths = require("../paths-C8-jxR0s.cjs");
|
|
12
12
|
let node_fs = require("node:fs");
|
|
13
13
|
let node_path = require("node:path");
|
|
@@ -45,6 +45,172 @@ var isIsoDateTimeWithOffset = (value) => {
|
|
|
45
45
|
return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;
|
|
46
46
|
};
|
|
47
47
|
//#endregion
|
|
48
|
+
//#region src/google/deletePlan.ts
|
|
49
|
+
var ATTENDEES_REASON = "it carries attendees — deleting an invited event withdraws it from their calendars too";
|
|
50
|
+
var ALREADY_GONE_REASON = "it is no longer in Google";
|
|
51
|
+
/** The message the push reports for a refusal, with the event named. */
|
|
52
|
+
var deleteRefusalMessage = (eventId, refusal) => `${eventId}: left in Google because ${refusal.kind === "has-attendees" ? ATTENDEES_REASON : ALREADY_GONE_REASON}`;
|
|
53
|
+
/** Decide one deletion against what Google currently holds.
|
|
54
|
+
*
|
|
55
|
+
* `null` means the event is already absent — a 404/410 on the read. Nothing to
|
|
56
|
+
* delete, and reported apart from a refusal so "we chose not to" never reads
|
|
57
|
+
* as "we could not".
|
|
58
|
+
*
|
|
59
|
+
* ANY attendee refuses, including the entry Google adds for the organiser. A
|
|
60
|
+
* rule that tried to exclude "only me" would have to work out which entry is
|
|
61
|
+
* the user from a payload that may not say, and being wrong there withdraws a
|
|
62
|
+
* real invitation. Solo events — the ones this feature exists for — carry no
|
|
63
|
+
* attendees at all, so the blunt rule costs them nothing. */
|
|
64
|
+
function planDelete(fetched) {
|
|
65
|
+
if (fetched === null) return {
|
|
66
|
+
ok: false,
|
|
67
|
+
kind: "already-gone"
|
|
68
|
+
};
|
|
69
|
+
return fetched.attendeeCount > 0 ? {
|
|
70
|
+
ok: false,
|
|
71
|
+
kind: "has-attendees"
|
|
72
|
+
} : {
|
|
73
|
+
ok: true,
|
|
74
|
+
etag: fetched.etag
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/google/util.ts
|
|
79
|
+
var ONE_SECOND_MS = 1e3;
|
|
80
|
+
var ONE_MINUTE_MS = 6e4;
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/google/eventDerived.ts
|
|
86
|
+
var stringAt = (record, key) => typeof record[key] === "string" ? record[key] : "";
|
|
87
|
+
var recordsIn = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
|
|
88
|
+
/** The signed-in user's own `responseStatus` on this event, `""` when Google
|
|
89
|
+
* reported none.
|
|
90
|
+
*
|
|
91
|
+
* `""` is the common case, not an edge one: an event with no attendees has no
|
|
92
|
+
* entry to mark `self`, which is most of a personal calendar. So it means
|
|
93
|
+
* "Google said nothing", never "not going" — filter with `!= "declined"`, not
|
|
94
|
+
* with `== "accepted"`, or every solo event disappears too.
|
|
95
|
+
*
|
|
96
|
+
* Google's values: `needsAction`, `declined`, `tentative`, `accepted`. */
|
|
97
|
+
function selfResponseStatus(attendees) {
|
|
98
|
+
const self = recordsIn(attendees).find((attendee) => attendee.self === true);
|
|
99
|
+
return self === void 0 ? "" : stringAt(self, "responseStatus");
|
|
100
|
+
}
|
|
101
|
+
var VIDEO_ENTRY_POINT = "video";
|
|
102
|
+
/** The URL that joins this event's meeting, `""` when it has none.
|
|
103
|
+
*
|
|
104
|
+
* Only the `video` entry point. The others are a phone number and a dial-in
|
|
105
|
+
* PIN page, and a column named for joining that sometimes holds `tel:` would
|
|
106
|
+
* be worse than one that is empty — the caller can see empty and fall back.
|
|
107
|
+
*
|
|
108
|
+
* `hangoutLink` (#3229) already carries this for Google Meet; this is what
|
|
109
|
+
* reaches the events whose conference is Zoom or Teams. */
|
|
110
|
+
function conferenceVideoUri(conferenceData) {
|
|
111
|
+
if (!isRecord(conferenceData)) return "";
|
|
112
|
+
const video = recordsIn(conferenceData.entryPoints).find((entry) => entry.entryPointType === VIDEO_ENTRY_POINT);
|
|
113
|
+
return video === void 0 ? "" : stringAt(video, "uri");
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/google/eventSpanInput.ts
|
|
117
|
+
var DATE_ONLY_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
118
|
+
/** Whether `value` is a bare calendar day — the all-day spelling of an event
|
|
119
|
+
* time, and the one shape `isIsoDateTimeWithOffset` is built to refuse.
|
|
120
|
+
*
|
|
121
|
+
* Realness comes from the parser the record lint, the calendar grid and the
|
|
122
|
+
* push already share, so every surface agrees that `2026-02-30` is not a day.
|
|
123
|
+
* The regex runs first because that parser trims, and a value with whitespace
|
|
124
|
+
* around it would validate here and then reach Google verbatim. */
|
|
125
|
+
var isCalendarDateOnly = (value) => DATE_ONLY_RE$1.test(value) && require_itemId.parseIsoDate(value) !== null;
|
|
126
|
+
/** One end of a span, or null when it is neither shape Calendar accepts. */
|
|
127
|
+
var toEventTimeInput = (value) => {
|
|
128
|
+
if (isCalendarDateOnly(value)) return { date: value };
|
|
129
|
+
return isIsoDateTimeWithOffset(value) ? { dateTime: value } : null;
|
|
130
|
+
};
|
|
131
|
+
/** The rule every surface states for a value that is neither shape. Shared so
|
|
132
|
+
* the tool, the remote host and their tests cannot drift apart. */
|
|
133
|
+
var EVENT_TIME_HINT = "must be an ISO 8601 date-time with a timezone offset (e.g. 2026-07-17T09:00:00+09:00), or a date for an all-day event (e.g. 2026-07-17)";
|
|
134
|
+
/** The same rule with the offending key named, for a surface whose error text
|
|
135
|
+
* carries no path of its own. */
|
|
136
|
+
var eventTimeHint = (key) => `${key} ${EVENT_TIME_HINT}`;
|
|
137
|
+
/** All-day `end` is exclusive, so a one-day event ends on the NEXT day. Spelled
|
|
138
|
+
* out because the natural reading of "all day on the 17th" is `end` = the
|
|
139
|
+
* 17th, which Calendar refuses. */
|
|
140
|
+
var ALL_DAY_END_HINT = "an all-day `end` is EXCLUSIVE — it is the day AFTER the last day, so a single day on 2026-07-17 needs end 2026-07-18";
|
|
141
|
+
var MIXED_SPAN_HINT = "start and end must both be date-times with an offset, or both be dates for an all-day event — not one of each";
|
|
142
|
+
/** A lone all-day end cannot be patched: the stored event's kind is unknown
|
|
143
|
+
* here, and giving one end of a timed event a `date` makes an event Calendar
|
|
144
|
+
* rejects — with a message that names neither end. Both together are
|
|
145
|
+
* unambiguous, so that is what the caller is asked for. */
|
|
146
|
+
var LONE_ALL_DAY_HINT = `pass BOTH start and end to move an all-day event — ${ALL_DAY_END_HINT}`;
|
|
147
|
+
var isAllDay = (time) => "date" in time;
|
|
148
|
+
/** Both ends of a create, validated together.
|
|
149
|
+
*
|
|
150
|
+
* Only the all-day ordering is checked: a timed span's ordering depends on the
|
|
151
|
+
* two offsets, which Calendar resolves and reports on clearly, while an all-day
|
|
152
|
+
* `end <= start` comes back as an opaque 400. Lexicographic comparison is exact
|
|
153
|
+
* for `YYYY-MM-DD` — fixed-width, most significant part first. */
|
|
154
|
+
function resolveSpanInput(start, end) {
|
|
155
|
+
const startTime = toEventTimeInput(start);
|
|
156
|
+
if (startTime === null) return {
|
|
157
|
+
ok: false,
|
|
158
|
+
reason: eventTimeHint("start")
|
|
159
|
+
};
|
|
160
|
+
const endTime = toEventTimeInput(end);
|
|
161
|
+
if (endTime === null) return {
|
|
162
|
+
ok: false,
|
|
163
|
+
reason: eventTimeHint("end")
|
|
164
|
+
};
|
|
165
|
+
if (isAllDay(startTime) !== isAllDay(endTime)) return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: MIXED_SPAN_HINT
|
|
168
|
+
};
|
|
169
|
+
if (isAllDay(startTime) && isAllDay(endTime) && endTime.date <= startTime.date) return {
|
|
170
|
+
ok: false,
|
|
171
|
+
reason: ALL_DAY_END_HINT
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
span: {
|
|
176
|
+
start: startTime,
|
|
177
|
+
end: endTime
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** One end of an EDIT, where the other end is absent. */
|
|
182
|
+
function resolveLoneEnd(key, value) {
|
|
183
|
+
const time = toEventTimeInput(value);
|
|
184
|
+
if (time === null) return {
|
|
185
|
+
ok: false,
|
|
186
|
+
reason: eventTimeHint(key)
|
|
187
|
+
};
|
|
188
|
+
if (isAllDay(time)) return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reason: LONE_ALL_DAY_HINT
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
ok: true,
|
|
194
|
+
times: { [key]: time }
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** The span of an EDIT, where either end may be absent. */
|
|
198
|
+
function resolvePartialSpanInput(start, end) {
|
|
199
|
+
if (start !== void 0 && end !== void 0) {
|
|
200
|
+
const resolved = resolveSpanInput(start, end);
|
|
201
|
+
return resolved.ok ? {
|
|
202
|
+
ok: true,
|
|
203
|
+
times: resolved.span
|
|
204
|
+
} : resolved;
|
|
205
|
+
}
|
|
206
|
+
if (start !== void 0) return resolveLoneEnd("start", start);
|
|
207
|
+
if (end !== void 0) return resolveLoneEnd("end", end);
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
times: {}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
48
214
|
//#region src/google/paths.ts
|
|
49
215
|
/** Same directory as every other cross-app per-user file — see
|
|
50
216
|
* `../global-config/paths.ts`, which owns the spelling. */
|
|
@@ -62,13 +228,6 @@ function googleSecretsDir(home) {
|
|
|
62
228
|
return (0, node_path.join)(home ?? (0, node_os.homedir)(), ".secrets");
|
|
63
229
|
}
|
|
64
230
|
//#endregion
|
|
65
|
-
//#region src/google/util.ts
|
|
66
|
-
var ONE_SECOND_MS = 1e3;
|
|
67
|
-
var ONE_MINUTE_MS = 6e4;
|
|
68
|
-
function isRecord(value) {
|
|
69
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
70
|
-
}
|
|
71
|
-
//#endregion
|
|
72
231
|
//#region src/google/clientSecret.ts
|
|
73
232
|
var isNonEmptyString = (value) => typeof value === "string" && value !== "";
|
|
74
233
|
var isInstalledClientSecret = (value) => {
|
|
@@ -599,6 +758,10 @@ var EVENT_SYNC_PAGE_SIZE = 2500;
|
|
|
599
758
|
var MAX_EVENT_SYNC_PAGES = 200;
|
|
600
759
|
var CALENDAR_META_PAGE_SIZE = 1;
|
|
601
760
|
var HTTP_GONE = 410;
|
|
761
|
+
/** A GET or DELETE on an event that is not there. Exported because the push's
|
|
762
|
+
* delete sweep has to read "already gone" as success, not as a failure to
|
|
763
|
+
* retry — a retained baseline would announce the same phantom deletion on
|
|
764
|
+
* every run. */
|
|
602
765
|
var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
|
|
603
766
|
/** An `If-Match` write whose etag no longer matches — someone else changed the
|
|
604
767
|
* event since it was read. */
|
|
@@ -616,6 +779,10 @@ var CANCELLED_EVENT_STATUS = "cancelled";
|
|
|
616
779
|
* apart while they share a sync token silently loses events (#2184). */
|
|
617
780
|
var canonicalCalendarId = (calendarId) => calendarId || DEFAULT_CALENDAR_ID;
|
|
618
781
|
var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}/events`;
|
|
782
|
+
/** People Google lists on the event, `0` when it carries none. Counts EVERY
|
|
783
|
+
* entry, including the one Google adds for the organiser: see `deletePlan.ts`
|
|
784
|
+
* for why the guard is not cleverer than that. */
|
|
785
|
+
var attendeeCount = (record) => Array.isArray(record.attendees) ? record.attendees.length : 0;
|
|
619
786
|
var eventTime$1 = (value) => {
|
|
620
787
|
if (!isRecord(value)) return "";
|
|
621
788
|
if (typeof value.dateTime === "string") return value.dateTime;
|
|
@@ -639,7 +806,9 @@ var toEventSummary = (value) => {
|
|
|
639
806
|
updated: stringField(record, "updated"),
|
|
640
807
|
transparency: stringField(record, "transparency"),
|
|
641
808
|
eventType: stringField(record, "eventType"),
|
|
642
|
-
hangoutLink: stringField(record, "hangoutLink")
|
|
809
|
+
hangoutLink: stringField(record, "hangoutLink"),
|
|
810
|
+
selfResponseStatus: selfResponseStatus(record.attendees),
|
|
811
|
+
conferenceVideoUri: conferenceVideoUri(record.conferenceData)
|
|
643
812
|
};
|
|
644
813
|
};
|
|
645
814
|
var toCalendarSummary = (value) => {
|
|
@@ -730,9 +899,11 @@ async function updateCalendarEvent(accessToken, input) {
|
|
|
730
899
|
async function getCalendarEvent(accessToken, input) {
|
|
731
900
|
try {
|
|
732
901
|
const fetched = await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId));
|
|
902
|
+
const record = asRecord(fetched);
|
|
733
903
|
return {
|
|
734
904
|
event: toEventSummary(fetched),
|
|
735
|
-
etag: stringField(
|
|
905
|
+
etag: stringField(record, "etag"),
|
|
906
|
+
attendeeCount: attendeeCount(record)
|
|
736
907
|
};
|
|
737
908
|
} catch (error) {
|
|
738
909
|
if (isGoogleApiError(error) && EVENT_ABSENT_STATUSES.includes(error.status)) return null;
|
|
@@ -743,7 +914,10 @@ async function getCalendarEvent(accessToken, input) {
|
|
|
743
914
|
* return; a second delete of the same id answers 410, which surfaces as a
|
|
744
915
|
* GoogleApiError rather than being swallowed. */
|
|
745
916
|
async function deleteCalendarEvent(accessToken, input) {
|
|
746
|
-
await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
|
|
917
|
+
await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
|
|
918
|
+
method: "DELETE",
|
|
919
|
+
...input.ifMatch ? { extraHeaders: { "If-Match": input.ifMatch } } : {}
|
|
920
|
+
});
|
|
747
921
|
}
|
|
748
922
|
async function listCalendarEvents(accessToken, input = {}) {
|
|
749
923
|
const params = new URLSearchParams({
|
|
@@ -1525,7 +1699,9 @@ var UNPUSHABLE_EVENT_FIELDS = {
|
|
|
1525
1699
|
updated: "",
|
|
1526
1700
|
transparency: "",
|
|
1527
1701
|
eventType: "",
|
|
1528
|
-
hangoutLink: ""
|
|
1702
|
+
hangoutLink: "",
|
|
1703
|
+
selfResponseStatus: "",
|
|
1704
|
+
conferenceVideoUri: ""
|
|
1529
1705
|
};
|
|
1530
1706
|
/** A baseline shaped as the event it came from, so the comparison can run
|
|
1531
1707
|
* through the very projection that wrote the record. */
|
|
@@ -1646,7 +1822,9 @@ var liveDeps = {
|
|
|
1646
1822
|
findCollection: findCalendarCollection,
|
|
1647
1823
|
isLinked: async () => Boolean((await loadGoogleTokens())?.refresh_token),
|
|
1648
1824
|
accessToken: getGoogleAccessToken,
|
|
1649
|
-
calendarMeta: liveCalendarMeta
|
|
1825
|
+
calendarMeta: liveCalendarMeta,
|
|
1826
|
+
fetchEvent: getCalendarEvent,
|
|
1827
|
+
deleteEvent: deleteCalendarEvent
|
|
1650
1828
|
};
|
|
1651
1829
|
var localValue = (ctx, record, source) => {
|
|
1652
1830
|
const field = ctx.bySource[source];
|
|
@@ -1856,7 +2034,7 @@ var UNPUSHED_KINDS = ["conflict", "error"];
|
|
|
1856
2034
|
/** Whether this outcome leaves a local edit that only exists locally, so the
|
|
1857
2035
|
* pull must not overwrite the record and the baseline must not advance. */
|
|
1858
2036
|
var isUnpushed = (kind) => UNPUSHED_KINDS.includes(kind);
|
|
1859
|
-
function tally(slug, attempts,
|
|
2037
|
+
function tally(slug, attempts, deletes) {
|
|
1860
2038
|
const outcomes = attempts.map((attempt) => attempt.outcome);
|
|
1861
2039
|
const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
|
|
1862
2040
|
return {
|
|
@@ -1864,9 +2042,10 @@ function tally(slug, attempts, localDeletes) {
|
|
|
1864
2042
|
created: count("created"),
|
|
1865
2043
|
updated: count("updated"),
|
|
1866
2044
|
conflicts: count("conflict"),
|
|
1867
|
-
localDeletes,
|
|
1868
|
-
|
|
1869
|
-
|
|
2045
|
+
localDeletes: deletes.seen,
|
|
2046
|
+
deletedInGoogle: deletes.deleted.length,
|
|
2047
|
+
skipped: [...outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []), ...deletes.skipped],
|
|
2048
|
+
errors: [...outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []), ...deletes.errors],
|
|
1870
2049
|
unpushedIds: attempts.filter((attempt) => isUnpushed(attempt.outcome.kind)).map((attempt) => attempt.eventId)
|
|
1871
2050
|
};
|
|
1872
2051
|
}
|
|
@@ -1895,6 +2074,69 @@ async function unsentLocalEdits(collection, workspaceRoot) {
|
|
|
1895
2074
|
const shadow = await loadCalendarShadow(schema.googleCalendar?.calendarId, workspaceRoot);
|
|
1896
2075
|
return locallyEditedIds(await require_discovery.storeFor(collection, { workspaceRoot }).list(), shadow, pushableMap(schema.googleCalendar?.map ?? {}), schema.primaryKey, schema.fields);
|
|
1897
2076
|
}
|
|
2077
|
+
var noDeletes = (seen) => ({
|
|
2078
|
+
seen,
|
|
2079
|
+
deleted: [],
|
|
2080
|
+
skipped: [],
|
|
2081
|
+
errors: []
|
|
2082
|
+
});
|
|
2083
|
+
/** A delete whose event changed in Google between the guard reading it and the
|
|
2084
|
+
* delete landing. Reported rather than retried: what changed may be the very
|
|
2085
|
+
* thing the guard refuses, and re-reading in a loop would race the same way. */
|
|
2086
|
+
var staleDeleteMessage = (eventId) => `${eventId}: left in Google because it changed there while this push was checking it — press Push again to re-check`;
|
|
2087
|
+
/** Delete ONE event and forget its baseline, or say why it was left alone.
|
|
2088
|
+
*
|
|
2089
|
+
* The baseline entry goes only after Google confirms, and it must go: the
|
|
2090
|
+
* baseline is what `locallyDeletedIds` reads, so a kept entry would re-report
|
|
2091
|
+
* the same deletion on every run forever. A refusal keeps its entry on
|
|
2092
|
+
* purpose — the event IS still standing in Google, and the report is the only
|
|
2093
|
+
* thing saying so.
|
|
2094
|
+
*
|
|
2095
|
+
* The delete carries the etag the guard read. Without it, an attendee added
|
|
2096
|
+
* between the two requests would be withdrawn by a decision taken before they
|
|
2097
|
+
* existed — the same race `updateCalendarEvent` already guards, and it costs
|
|
2098
|
+
* more here because a delete cannot be undone from this side. */
|
|
2099
|
+
async function propagateOneDelete(eventId, deps) {
|
|
2100
|
+
const decision = planDelete(await deps.fetchEvent(eventId));
|
|
2101
|
+
if (!decision.ok) {
|
|
2102
|
+
if (decision.kind === "already-gone") {
|
|
2103
|
+
await deps.forget(eventId);
|
|
2104
|
+
return {};
|
|
2105
|
+
}
|
|
2106
|
+
return { skipped: [deleteRefusalMessage(eventId, decision)] };
|
|
2107
|
+
}
|
|
2108
|
+
try {
|
|
2109
|
+
await deps.deleteEvent(eventId, decision.etag);
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
if (!isGoogleApiError(error)) throw error;
|
|
2112
|
+
if (EVENT_ABSENT_STATUSES.includes(error.status)) {
|
|
2113
|
+
await deps.forget(eventId);
|
|
2114
|
+
return {};
|
|
2115
|
+
}
|
|
2116
|
+
if (error.status !== 412) throw error;
|
|
2117
|
+
return { skipped: [staleDeleteMessage(eventId)] };
|
|
2118
|
+
}
|
|
2119
|
+
await deps.forget(eventId);
|
|
2120
|
+
log.info("google", "calendar event deleted by push", { id: eventId });
|
|
2121
|
+
return { deleted: [eventId] };
|
|
2122
|
+
}
|
|
2123
|
+
/** Carry every local deletion, when the collection opted in.
|
|
2124
|
+
*
|
|
2125
|
+
* Sequential rather than concurrent: each one writes the shared baseline file,
|
|
2126
|
+
* and an interrupted run has to leave a state the next run resumes from rather
|
|
2127
|
+
* than a partially applied batch. */
|
|
2128
|
+
async function sweepDeletes(eventIds, propagate, deps) {
|
|
2129
|
+
if (!propagate || eventIds.length === 0) return noDeletes(eventIds.length);
|
|
2130
|
+
const sweep = noDeletes(eventIds.length);
|
|
2131
|
+
for (const eventId of eventIds) try {
|
|
2132
|
+
const one = await propagateOneDelete(eventId, deps);
|
|
2133
|
+
sweep.deleted.push(...one.deleted ?? []);
|
|
2134
|
+
sweep.skipped.push(...one.skipped ?? []);
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
sweep.errors.push(`${eventId}: could not be deleted in Google — ${require_dist.errorMessage(error)}`);
|
|
2137
|
+
}
|
|
2138
|
+
return sweep;
|
|
2139
|
+
}
|
|
1898
2140
|
/** Push one collection WITHOUT taking the calendar lock.
|
|
1899
2141
|
*
|
|
1900
2142
|
* The caller must already hold it. The scheduled push→pull cycle
|
|
@@ -1933,7 +2175,18 @@ async function pushCollectionNow(collection, workspaceRoot, deps = liveDeps) {
|
|
|
1933
2175
|
});
|
|
1934
2176
|
await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
|
|
1935
2177
|
}
|
|
1936
|
-
const result = tally(slug, attempts, locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))).
|
|
2178
|
+
const result = tally(slug, attempts, await sweepDeletes(locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))), schema.googleCalendar?.propagateDeletes === true, {
|
|
2179
|
+
fetchEvent: (eventId) => (deps.fetchEvent ?? getCalendarEvent)(accessToken, {
|
|
2180
|
+
calendarId,
|
|
2181
|
+
eventId
|
|
2182
|
+
}),
|
|
2183
|
+
deleteEvent: (eventId, ifMatch) => (deps.deleteEvent ?? deleteCalendarEvent)(accessToken, {
|
|
2184
|
+
calendarId,
|
|
2185
|
+
eventId,
|
|
2186
|
+
ifMatch
|
|
2187
|
+
}),
|
|
2188
|
+
forget: (eventId) => saveCalendarShadow(calendarId, { [eventId]: null }, workspaceRoot)
|
|
2189
|
+
}));
|
|
1937
2190
|
if (result.errors.length > 0) log.warn("google", "calendar push finished with errors", {
|
|
1938
2191
|
slug,
|
|
1939
2192
|
errors: result.errors.length
|
|
@@ -2891,8 +3144,11 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
2891
3144
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
2892
3145
|
}
|
|
2893
3146
|
//#endregion
|
|
3147
|
+
exports.ALL_DAY_END_HINT = ALL_DAY_END_HINT;
|
|
2894
3148
|
exports.CANCELLED_EVENT_STATUS = CANCELLED_EVENT_STATUS;
|
|
2895
3149
|
exports.DEFAULT_LIST_MAX_RESULTS = DEFAULT_LIST_MAX_RESULTS;
|
|
3150
|
+
exports.EVENT_ABSENT_STATUSES = EVENT_ABSENT_STATUSES;
|
|
3151
|
+
exports.EVENT_TIME_HINT = EVENT_TIME_HINT;
|
|
2896
3152
|
exports.GOOGLE_AUTH_CANCELLED = GOOGLE_AUTH_CANCELLED;
|
|
2897
3153
|
exports.GOOGLE_CALENDARLIST_SCOPE = GOOGLE_CALENDARLIST_SCOPE;
|
|
2898
3154
|
exports.GOOGLE_CALENDAR_SCOPE = GOOGLE_CALENDAR_SCOPE;
|
|
@@ -2904,7 +3160,9 @@ exports.GoogleApiError = GoogleApiError;
|
|
|
2904
3160
|
exports.HTTP_CONFLICT = HTTP_CONFLICT;
|
|
2905
3161
|
exports.HTTP_FORBIDDEN = HTTP_FORBIDDEN;
|
|
2906
3162
|
exports.HTTP_PRECONDITION_FAILED = HTTP_PRECONDITION_FAILED;
|
|
3163
|
+
exports.LONE_ALL_DAY_HINT = LONE_ALL_DAY_HINT;
|
|
2907
3164
|
exports.MAX_LIST_RESULTS = MAX_LIST_RESULTS;
|
|
3165
|
+
exports.MIXED_SPAN_HINT = MIXED_SPAN_HINT;
|
|
2908
3166
|
exports.PARTIAL_CALENDAR_WINDOW = PARTIAL_CALENDAR_WINDOW;
|
|
2909
3167
|
exports.PROTECTION_UNKNOWN = PROTECTION_UNKNOWN;
|
|
2910
3168
|
exports.PUSHABLE_SOURCE_FIELDS = PUSHABLE_SOURCE_FIELDS;
|
|
@@ -2940,6 +3198,7 @@ exports.collectCalendarPages = collectCalendarPages;
|
|
|
2940
3198
|
exports.collectionsNeedingBackfill = collectionsNeedingBackfill;
|
|
2941
3199
|
exports.commitLinkedTokens = commitLinkedTokens;
|
|
2942
3200
|
exports.completeTask = completeTask;
|
|
3201
|
+
exports.conferenceVideoUri = conferenceVideoUri;
|
|
2943
3202
|
exports.configureGoogleHost = configureGoogleHost;
|
|
2944
3203
|
exports.conflictingFields = conflictingFields;
|
|
2945
3204
|
exports.createCalendarEvent = createCalendarEvent;
|
|
@@ -2949,7 +3208,9 @@ exports.createTask = createTask;
|
|
|
2949
3208
|
exports.deleteCalendarEvent = deleteCalendarEvent;
|
|
2950
3209
|
exports.deleteDriveFile = deleteDriveFile;
|
|
2951
3210
|
exports.deleteGoogleTokens = deleteGoogleTokens;
|
|
3211
|
+
exports.deleteRefusalMessage = deleteRefusalMessage;
|
|
2952
3212
|
exports.deleteTask = deleteTask;
|
|
3213
|
+
exports.eventTimeHint = eventTimeHint;
|
|
2953
3214
|
exports.fieldText = fieldText;
|
|
2954
3215
|
exports.findClientSecretPath = findClientSecretPath;
|
|
2955
3216
|
exports.getCalendarColors = getCalendarColors;
|
|
@@ -2965,6 +3226,7 @@ exports.googleTokenPath = googleTokenPath;
|
|
|
2965
3226
|
exports.groupByCalendar = groupByCalendar;
|
|
2966
3227
|
exports.groupsNeedingBackfill = groupsNeedingBackfill;
|
|
2967
3228
|
exports.heldBack = heldBack;
|
|
3229
|
+
exports.isCalendarDateOnly = isCalendarDateOnly;
|
|
2968
3230
|
exports.isCalendarSyncDue = isCalendarSyncDue;
|
|
2969
3231
|
exports.isClientSettableEventId = isClientSettableEventId;
|
|
2970
3232
|
exports.isDeniedAccessRole = isDeniedAccessRole;
|
|
@@ -2995,6 +3257,7 @@ exports.mergeShadow = mergeShadow;
|
|
|
2995
3257
|
exports.needsCalendarBackfill = needsCalendarBackfill;
|
|
2996
3258
|
exports.orphanedCalendarId = orphanedCalendarId;
|
|
2997
3259
|
exports.pickBoundary = pickBoundary;
|
|
3260
|
+
exports.planDelete = planDelete;
|
|
2998
3261
|
exports.planRecord = planRecord;
|
|
2999
3262
|
exports.pullProtectionFor = pullProtectionFor;
|
|
3000
3263
|
exports.pullableEvents = pullableEvents;
|
|
@@ -3006,12 +3269,16 @@ exports.readDriveFile = readDriveFile;
|
|
|
3006
3269
|
exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
|
|
3007
3270
|
exports.reportedAccessRole = reportedAccessRole;
|
|
3008
3271
|
exports.resolveEventSpan = resolveEventSpan;
|
|
3272
|
+
exports.resolvePartialSpanInput = resolvePartialSpanInput;
|
|
3273
|
+
exports.resolveSpanInput = resolveSpanInput;
|
|
3009
3274
|
exports.resumableToken = resumableToken;
|
|
3010
3275
|
exports.saveCalendarShadow = saveCalendarShadow;
|
|
3011
3276
|
exports.saveCalendarSyncToken = saveCalendarSyncToken;
|
|
3012
3277
|
exports.saveGoogleTokens = saveGoogleTokens;
|
|
3278
|
+
exports.selfResponseStatus = selfResponseStatus;
|
|
3013
3279
|
exports.shadowUpdates = shadowUpdates;
|
|
3014
3280
|
exports.stateLockPath = stateLockPath;
|
|
3281
|
+
exports.sweepDeletes = sweepDeletes;
|
|
3015
3282
|
exports.syncCalendarEvents = syncCalendarEvents;
|
|
3016
3283
|
exports.syncCalendarForCollection = syncCalendarForCollection;
|
|
3017
3284
|
exports.syncCalendarGroup = syncCalendarGroup;
|
|
@@ -3024,6 +3291,7 @@ exports.toCollectionDateTime = toCollectionDateTime;
|
|
|
3024
3291
|
exports.toCollectionRecord = toCollectionRecord;
|
|
3025
3292
|
exports.toDriveFileSummary = toDriveFileSummary;
|
|
3026
3293
|
exports.toEventSummary = toEventSummary;
|
|
3294
|
+
exports.toEventTimeInput = toEventTimeInput;
|
|
3027
3295
|
exports.toGoogleEventTime = toGoogleEventTime;
|
|
3028
3296
|
exports.toShadowEvent = toShadowEvent;
|
|
3029
3297
|
exports.toTaskListSummary = toTaskListSummary;
|