@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.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { configureGoogleHost, type GoogleLogger } from './host.js';
|
|
2
2
|
export { isIsoDateTimeWithOffset } from './datetime.js';
|
|
3
|
+
export { deleteRefusalMessage, planDelete, type DeleteDecision, type DeleteRefusal } from './deletePlan.js';
|
|
4
|
+
export { conferenceVideoUri, selfResponseStatus } from './eventDerived.js';
|
|
5
|
+
export { isCalendarDateOnly, resolvePartialSpanInput, resolveSpanInput, toEventTimeInput, eventTimeHint, EVENT_TIME_HINT, ALL_DAY_END_HINT, LONE_ALL_DAY_HINT, MIXED_SPAN_HINT, type PartialSpanResult, type PartialSpanTimes, type SpanResult, type SpanTimes, } from './eventSpanInput.js';
|
|
3
6
|
export { googleConfigDir, googleSecretsDir, googleTokenPath, legacyGoogleTokenPath } from './paths.js';
|
|
4
7
|
export { clientSecretPresence, findClientSecretPath, loadClientSecret, type ClientSecretPresence, type InstalledClientSecret } from './clientSecret.js';
|
|
5
8
|
export { deleteGoogleTokens, loadGoogleTokens, mergeGoogleTokens, saveGoogleTokens, type IssuedVia, type StoredGoogleTokens } from './tokenStore.js';
|
|
@@ -7,13 +10,13 @@ export { brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, type BrokerS
|
|
|
7
10
|
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
11
|
export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
|
|
9
12
|
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, syncPageParams, } from './calendar.js';
|
|
13
|
+
export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, EVENT_ABSENT_STATUSES, 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
14
|
export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, toolCalendarSyncKey, } from './calendarSyncStore.js';
|
|
12
15
|
export { calendarSyncDueWindowMs, isCalendarSyncDue } from './calendarSyncDue.js';
|
|
13
16
|
export { calendarBackfillPath, markCalendarBackfilled, needsCalendarBackfill } from './calendarBackfillState.js';
|
|
14
17
|
export { stateLockPath, withCalendarStateLock, liveLockClock, type LockClock } from './calendarStateLock.js';
|
|
15
18
|
export { calendarPushStatePath, clearCalendarShadow, loadCalendarShadow, mergeShadow, saveCalendarShadow, toShadowEvent, type ShadowEvent, } from './calendarPushState.js';
|
|
16
|
-
export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, unsentLocalEdits, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
|
|
19
|
+
export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, sweepDeletes, unsentLocalEdits, type DeleteSweep, type DeleteSweepDeps, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
|
|
17
20
|
export { toGoogleEventTime, zoneSuffixOf } from './pushDateTime.js';
|
|
18
21
|
export { baselineRecord, bySourceField, conflictingFields, fieldText, isClientSettableEventId, locallyChangedFields, locallyDeletedIds, mayAdoptExisting, planRecord, pushableMap, PUSHABLE_SOURCE_FIELDS, type PushableSourceField, type RecordPlan, } from './pushPlan.js';
|
|
19
22
|
export { toCollectionDateTime } from './collectionDateTime.js';
|
package/dist/google/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { n as writeFileAtomic } from "../root-BcS4-Kge.js";
|
|
|
5
5
|
import { fetchWithTimeout } from "../utils/fetch.js";
|
|
6
6
|
import { d as parseIsoDate, f as parseIsoDateTime } from "../itemId-4P9S_ehz.js";
|
|
7
7
|
import { t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
|
|
8
|
-
import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-
|
|
8
|
+
import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-B_64PBZU.js";
|
|
9
9
|
import { t as mulmoConfigDir } from "../paths-CkJxNZ7T.js";
|
|
10
10
|
import { promises } from "node:fs";
|
|
11
11
|
import path, { join } from "node:path";
|
|
@@ -41,6 +41,172 @@ var isIsoDateTimeWithOffset = (value) => {
|
|
|
41
41
|
return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;
|
|
42
42
|
};
|
|
43
43
|
//#endregion
|
|
44
|
+
//#region src/google/deletePlan.ts
|
|
45
|
+
var ATTENDEES_REASON = "it carries attendees — deleting an invited event withdraws it from their calendars too";
|
|
46
|
+
var ALREADY_GONE_REASON = "it is no longer in Google";
|
|
47
|
+
/** The message the push reports for a refusal, with the event named. */
|
|
48
|
+
var deleteRefusalMessage = (eventId, refusal) => `${eventId}: left in Google because ${refusal.kind === "has-attendees" ? ATTENDEES_REASON : ALREADY_GONE_REASON}`;
|
|
49
|
+
/** Decide one deletion against what Google currently holds.
|
|
50
|
+
*
|
|
51
|
+
* `null` means the event is already absent — a 404/410 on the read. Nothing to
|
|
52
|
+
* delete, and reported apart from a refusal so "we chose not to" never reads
|
|
53
|
+
* as "we could not".
|
|
54
|
+
*
|
|
55
|
+
* ANY attendee refuses, including the entry Google adds for the organiser. A
|
|
56
|
+
* rule that tried to exclude "only me" would have to work out which entry is
|
|
57
|
+
* the user from a payload that may not say, and being wrong there withdraws a
|
|
58
|
+
* real invitation. Solo events — the ones this feature exists for — carry no
|
|
59
|
+
* attendees at all, so the blunt rule costs them nothing. */
|
|
60
|
+
function planDelete(fetched) {
|
|
61
|
+
if (fetched === null) return {
|
|
62
|
+
ok: false,
|
|
63
|
+
kind: "already-gone"
|
|
64
|
+
};
|
|
65
|
+
return fetched.attendeeCount > 0 ? {
|
|
66
|
+
ok: false,
|
|
67
|
+
kind: "has-attendees"
|
|
68
|
+
} : {
|
|
69
|
+
ok: true,
|
|
70
|
+
etag: fetched.etag
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/google/util.ts
|
|
75
|
+
var ONE_SECOND_MS = 1e3;
|
|
76
|
+
var ONE_MINUTE_MS = 6e4;
|
|
77
|
+
function isRecord(value) {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/google/eventDerived.ts
|
|
82
|
+
var stringAt = (record, key) => typeof record[key] === "string" ? record[key] : "";
|
|
83
|
+
var recordsIn = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
|
|
84
|
+
/** The signed-in user's own `responseStatus` on this event, `""` when Google
|
|
85
|
+
* reported none.
|
|
86
|
+
*
|
|
87
|
+
* `""` is the common case, not an edge one: an event with no attendees has no
|
|
88
|
+
* entry to mark `self`, which is most of a personal calendar. So it means
|
|
89
|
+
* "Google said nothing", never "not going" — filter with `!= "declined"`, not
|
|
90
|
+
* with `== "accepted"`, or every solo event disappears too.
|
|
91
|
+
*
|
|
92
|
+
* Google's values: `needsAction`, `declined`, `tentative`, `accepted`. */
|
|
93
|
+
function selfResponseStatus(attendees) {
|
|
94
|
+
const self = recordsIn(attendees).find((attendee) => attendee.self === true);
|
|
95
|
+
return self === void 0 ? "" : stringAt(self, "responseStatus");
|
|
96
|
+
}
|
|
97
|
+
var VIDEO_ENTRY_POINT = "video";
|
|
98
|
+
/** The URL that joins this event's meeting, `""` when it has none.
|
|
99
|
+
*
|
|
100
|
+
* Only the `video` entry point. The others are a phone number and a dial-in
|
|
101
|
+
* PIN page, and a column named for joining that sometimes holds `tel:` would
|
|
102
|
+
* be worse than one that is empty — the caller can see empty and fall back.
|
|
103
|
+
*
|
|
104
|
+
* `hangoutLink` (#3229) already carries this for Google Meet; this is what
|
|
105
|
+
* reaches the events whose conference is Zoom or Teams. */
|
|
106
|
+
function conferenceVideoUri(conferenceData) {
|
|
107
|
+
if (!isRecord(conferenceData)) return "";
|
|
108
|
+
const video = recordsIn(conferenceData.entryPoints).find((entry) => entry.entryPointType === VIDEO_ENTRY_POINT);
|
|
109
|
+
return video === void 0 ? "" : stringAt(video, "uri");
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region src/google/eventSpanInput.ts
|
|
113
|
+
var DATE_ONLY_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
114
|
+
/** Whether `value` is a bare calendar day — the all-day spelling of an event
|
|
115
|
+
* time, and the one shape `isIsoDateTimeWithOffset` is built to refuse.
|
|
116
|
+
*
|
|
117
|
+
* Realness comes from the parser the record lint, the calendar grid and the
|
|
118
|
+
* push already share, so every surface agrees that `2026-02-30` is not a day.
|
|
119
|
+
* The regex runs first because that parser trims, and a value with whitespace
|
|
120
|
+
* around it would validate here and then reach Google verbatim. */
|
|
121
|
+
var isCalendarDateOnly = (value) => DATE_ONLY_RE$1.test(value) && parseIsoDate(value) !== null;
|
|
122
|
+
/** One end of a span, or null when it is neither shape Calendar accepts. */
|
|
123
|
+
var toEventTimeInput = (value) => {
|
|
124
|
+
if (isCalendarDateOnly(value)) return { date: value };
|
|
125
|
+
return isIsoDateTimeWithOffset(value) ? { dateTime: value } : null;
|
|
126
|
+
};
|
|
127
|
+
/** The rule every surface states for a value that is neither shape. Shared so
|
|
128
|
+
* the tool, the remote host and their tests cannot drift apart. */
|
|
129
|
+
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)";
|
|
130
|
+
/** The same rule with the offending key named, for a surface whose error text
|
|
131
|
+
* carries no path of its own. */
|
|
132
|
+
var eventTimeHint = (key) => `${key} ${EVENT_TIME_HINT}`;
|
|
133
|
+
/** All-day `end` is exclusive, so a one-day event ends on the NEXT day. Spelled
|
|
134
|
+
* out because the natural reading of "all day on the 17th" is `end` = the
|
|
135
|
+
* 17th, which Calendar refuses. */
|
|
136
|
+
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";
|
|
137
|
+
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";
|
|
138
|
+
/** A lone all-day end cannot be patched: the stored event's kind is unknown
|
|
139
|
+
* here, and giving one end of a timed event a `date` makes an event Calendar
|
|
140
|
+
* rejects — with a message that names neither end. Both together are
|
|
141
|
+
* unambiguous, so that is what the caller is asked for. */
|
|
142
|
+
var LONE_ALL_DAY_HINT = `pass BOTH start and end to move an all-day event — ${ALL_DAY_END_HINT}`;
|
|
143
|
+
var isAllDay = (time) => "date" in time;
|
|
144
|
+
/** Both ends of a create, validated together.
|
|
145
|
+
*
|
|
146
|
+
* Only the all-day ordering is checked: a timed span's ordering depends on the
|
|
147
|
+
* two offsets, which Calendar resolves and reports on clearly, while an all-day
|
|
148
|
+
* `end <= start` comes back as an opaque 400. Lexicographic comparison is exact
|
|
149
|
+
* for `YYYY-MM-DD` — fixed-width, most significant part first. */
|
|
150
|
+
function resolveSpanInput(start, end) {
|
|
151
|
+
const startTime = toEventTimeInput(start);
|
|
152
|
+
if (startTime === null) return {
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: eventTimeHint("start")
|
|
155
|
+
};
|
|
156
|
+
const endTime = toEventTimeInput(end);
|
|
157
|
+
if (endTime === null) return {
|
|
158
|
+
ok: false,
|
|
159
|
+
reason: eventTimeHint("end")
|
|
160
|
+
};
|
|
161
|
+
if (isAllDay(startTime) !== isAllDay(endTime)) return {
|
|
162
|
+
ok: false,
|
|
163
|
+
reason: MIXED_SPAN_HINT
|
|
164
|
+
};
|
|
165
|
+
if (isAllDay(startTime) && isAllDay(endTime) && endTime.date <= startTime.date) return {
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: ALL_DAY_END_HINT
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
span: {
|
|
172
|
+
start: startTime,
|
|
173
|
+
end: endTime
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** One end of an EDIT, where the other end is absent. */
|
|
178
|
+
function resolveLoneEnd(key, value) {
|
|
179
|
+
const time = toEventTimeInput(value);
|
|
180
|
+
if (time === null) return {
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: eventTimeHint(key)
|
|
183
|
+
};
|
|
184
|
+
if (isAllDay(time)) return {
|
|
185
|
+
ok: false,
|
|
186
|
+
reason: LONE_ALL_DAY_HINT
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
times: { [key]: time }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** The span of an EDIT, where either end may be absent. */
|
|
194
|
+
function resolvePartialSpanInput(start, end) {
|
|
195
|
+
if (start !== void 0 && end !== void 0) {
|
|
196
|
+
const resolved = resolveSpanInput(start, end);
|
|
197
|
+
return resolved.ok ? {
|
|
198
|
+
ok: true,
|
|
199
|
+
times: resolved.span
|
|
200
|
+
} : resolved;
|
|
201
|
+
}
|
|
202
|
+
if (start !== void 0) return resolveLoneEnd("start", start);
|
|
203
|
+
if (end !== void 0) return resolveLoneEnd("end", end);
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
times: {}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
44
210
|
//#region src/google/paths.ts
|
|
45
211
|
/** Same directory as every other cross-app per-user file — see
|
|
46
212
|
* `../global-config/paths.ts`, which owns the spelling. */
|
|
@@ -58,13 +224,6 @@ function googleSecretsDir(home) {
|
|
|
58
224
|
return join(home ?? homedir(), ".secrets");
|
|
59
225
|
}
|
|
60
226
|
//#endregion
|
|
61
|
-
//#region src/google/util.ts
|
|
62
|
-
var ONE_SECOND_MS = 1e3;
|
|
63
|
-
var ONE_MINUTE_MS = 6e4;
|
|
64
|
-
function isRecord(value) {
|
|
65
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
-
}
|
|
67
|
-
//#endregion
|
|
68
227
|
//#region src/google/clientSecret.ts
|
|
69
228
|
var isNonEmptyString = (value) => typeof value === "string" && value !== "";
|
|
70
229
|
var isInstalledClientSecret = (value) => {
|
|
@@ -595,6 +754,10 @@ var EVENT_SYNC_PAGE_SIZE = 2500;
|
|
|
595
754
|
var MAX_EVENT_SYNC_PAGES = 200;
|
|
596
755
|
var CALENDAR_META_PAGE_SIZE = 1;
|
|
597
756
|
var HTTP_GONE = 410;
|
|
757
|
+
/** A GET or DELETE on an event that is not there. Exported because the push's
|
|
758
|
+
* delete sweep has to read "already gone" as success, not as a failure to
|
|
759
|
+
* retry — a retained baseline would announce the same phantom deletion on
|
|
760
|
+
* every run. */
|
|
598
761
|
var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
|
|
599
762
|
/** An `If-Match` write whose etag no longer matches — someone else changed the
|
|
600
763
|
* event since it was read. */
|
|
@@ -612,6 +775,10 @@ var CANCELLED_EVENT_STATUS = "cancelled";
|
|
|
612
775
|
* apart while they share a sync token silently loses events (#2184). */
|
|
613
776
|
var canonicalCalendarId = (calendarId) => calendarId || DEFAULT_CALENDAR_ID;
|
|
614
777
|
var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}/events`;
|
|
778
|
+
/** People Google lists on the event, `0` when it carries none. Counts EVERY
|
|
779
|
+
* entry, including the one Google adds for the organiser: see `deletePlan.ts`
|
|
780
|
+
* for why the guard is not cleverer than that. */
|
|
781
|
+
var attendeeCount = (record) => Array.isArray(record.attendees) ? record.attendees.length : 0;
|
|
615
782
|
var eventTime$1 = (value) => {
|
|
616
783
|
if (!isRecord(value)) return "";
|
|
617
784
|
if (typeof value.dateTime === "string") return value.dateTime;
|
|
@@ -635,7 +802,9 @@ var toEventSummary = (value) => {
|
|
|
635
802
|
updated: stringField(record, "updated"),
|
|
636
803
|
transparency: stringField(record, "transparency"),
|
|
637
804
|
eventType: stringField(record, "eventType"),
|
|
638
|
-
hangoutLink: stringField(record, "hangoutLink")
|
|
805
|
+
hangoutLink: stringField(record, "hangoutLink"),
|
|
806
|
+
selfResponseStatus: selfResponseStatus(record.attendees),
|
|
807
|
+
conferenceVideoUri: conferenceVideoUri(record.conferenceData)
|
|
639
808
|
};
|
|
640
809
|
};
|
|
641
810
|
var toCalendarSummary = (value) => {
|
|
@@ -726,9 +895,11 @@ async function updateCalendarEvent(accessToken, input) {
|
|
|
726
895
|
async function getCalendarEvent(accessToken, input) {
|
|
727
896
|
try {
|
|
728
897
|
const fetched = await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId));
|
|
898
|
+
const record = asRecord(fetched);
|
|
729
899
|
return {
|
|
730
900
|
event: toEventSummary(fetched),
|
|
731
|
-
etag: stringField(
|
|
901
|
+
etag: stringField(record, "etag"),
|
|
902
|
+
attendeeCount: attendeeCount(record)
|
|
732
903
|
};
|
|
733
904
|
} catch (error) {
|
|
734
905
|
if (isGoogleApiError(error) && EVENT_ABSENT_STATUSES.includes(error.status)) return null;
|
|
@@ -739,7 +910,10 @@ async function getCalendarEvent(accessToken, input) {
|
|
|
739
910
|
* return; a second delete of the same id answers 410, which surfaces as a
|
|
740
911
|
* GoogleApiError rather than being swallowed. */
|
|
741
912
|
async function deleteCalendarEvent(accessToken, input) {
|
|
742
|
-
await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
|
|
913
|
+
await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
|
|
914
|
+
method: "DELETE",
|
|
915
|
+
...input.ifMatch ? { extraHeaders: { "If-Match": input.ifMatch } } : {}
|
|
916
|
+
});
|
|
743
917
|
}
|
|
744
918
|
async function listCalendarEvents(accessToken, input = {}) {
|
|
745
919
|
const params = new URLSearchParams({
|
|
@@ -1521,7 +1695,9 @@ var UNPUSHABLE_EVENT_FIELDS = {
|
|
|
1521
1695
|
updated: "",
|
|
1522
1696
|
transparency: "",
|
|
1523
1697
|
eventType: "",
|
|
1524
|
-
hangoutLink: ""
|
|
1698
|
+
hangoutLink: "",
|
|
1699
|
+
selfResponseStatus: "",
|
|
1700
|
+
conferenceVideoUri: ""
|
|
1525
1701
|
};
|
|
1526
1702
|
/** A baseline shaped as the event it came from, so the comparison can run
|
|
1527
1703
|
* through the very projection that wrote the record. */
|
|
@@ -1642,7 +1818,9 @@ var liveDeps = {
|
|
|
1642
1818
|
findCollection: findCalendarCollection,
|
|
1643
1819
|
isLinked: async () => Boolean((await loadGoogleTokens())?.refresh_token),
|
|
1644
1820
|
accessToken: getGoogleAccessToken,
|
|
1645
|
-
calendarMeta: liveCalendarMeta
|
|
1821
|
+
calendarMeta: liveCalendarMeta,
|
|
1822
|
+
fetchEvent: getCalendarEvent,
|
|
1823
|
+
deleteEvent: deleteCalendarEvent
|
|
1646
1824
|
};
|
|
1647
1825
|
var localValue = (ctx, record, source) => {
|
|
1648
1826
|
const field = ctx.bySource[source];
|
|
@@ -1852,7 +2030,7 @@ var UNPUSHED_KINDS = ["conflict", "error"];
|
|
|
1852
2030
|
/** Whether this outcome leaves a local edit that only exists locally, so the
|
|
1853
2031
|
* pull must not overwrite the record and the baseline must not advance. */
|
|
1854
2032
|
var isUnpushed = (kind) => UNPUSHED_KINDS.includes(kind);
|
|
1855
|
-
function tally(slug, attempts,
|
|
2033
|
+
function tally(slug, attempts, deletes) {
|
|
1856
2034
|
const outcomes = attempts.map((attempt) => attempt.outcome);
|
|
1857
2035
|
const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
|
|
1858
2036
|
return {
|
|
@@ -1860,9 +2038,10 @@ function tally(slug, attempts, localDeletes) {
|
|
|
1860
2038
|
created: count("created"),
|
|
1861
2039
|
updated: count("updated"),
|
|
1862
2040
|
conflicts: count("conflict"),
|
|
1863
|
-
localDeletes,
|
|
1864
|
-
|
|
1865
|
-
|
|
2041
|
+
localDeletes: deletes.seen,
|
|
2042
|
+
deletedInGoogle: deletes.deleted.length,
|
|
2043
|
+
skipped: [...outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []), ...deletes.skipped],
|
|
2044
|
+
errors: [...outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []), ...deletes.errors],
|
|
1866
2045
|
unpushedIds: attempts.filter((attempt) => isUnpushed(attempt.outcome.kind)).map((attempt) => attempt.eventId)
|
|
1867
2046
|
};
|
|
1868
2047
|
}
|
|
@@ -1891,6 +2070,69 @@ async function unsentLocalEdits(collection, workspaceRoot) {
|
|
|
1891
2070
|
const shadow = await loadCalendarShadow(schema.googleCalendar?.calendarId, workspaceRoot);
|
|
1892
2071
|
return locallyEditedIds(await storeFor(collection, { workspaceRoot }).list(), shadow, pushableMap(schema.googleCalendar?.map ?? {}), schema.primaryKey, schema.fields);
|
|
1893
2072
|
}
|
|
2073
|
+
var noDeletes = (seen) => ({
|
|
2074
|
+
seen,
|
|
2075
|
+
deleted: [],
|
|
2076
|
+
skipped: [],
|
|
2077
|
+
errors: []
|
|
2078
|
+
});
|
|
2079
|
+
/** A delete whose event changed in Google between the guard reading it and the
|
|
2080
|
+
* delete landing. Reported rather than retried: what changed may be the very
|
|
2081
|
+
* thing the guard refuses, and re-reading in a loop would race the same way. */
|
|
2082
|
+
var staleDeleteMessage = (eventId) => `${eventId}: left in Google because it changed there while this push was checking it — press Push again to re-check`;
|
|
2083
|
+
/** Delete ONE event and forget its baseline, or say why it was left alone.
|
|
2084
|
+
*
|
|
2085
|
+
* The baseline entry goes only after Google confirms, and it must go: the
|
|
2086
|
+
* baseline is what `locallyDeletedIds` reads, so a kept entry would re-report
|
|
2087
|
+
* the same deletion on every run forever. A refusal keeps its entry on
|
|
2088
|
+
* purpose — the event IS still standing in Google, and the report is the only
|
|
2089
|
+
* thing saying so.
|
|
2090
|
+
*
|
|
2091
|
+
* The delete carries the etag the guard read. Without it, an attendee added
|
|
2092
|
+
* between the two requests would be withdrawn by a decision taken before they
|
|
2093
|
+
* existed — the same race `updateCalendarEvent` already guards, and it costs
|
|
2094
|
+
* more here because a delete cannot be undone from this side. */
|
|
2095
|
+
async function propagateOneDelete(eventId, deps) {
|
|
2096
|
+
const decision = planDelete(await deps.fetchEvent(eventId));
|
|
2097
|
+
if (!decision.ok) {
|
|
2098
|
+
if (decision.kind === "already-gone") {
|
|
2099
|
+
await deps.forget(eventId);
|
|
2100
|
+
return {};
|
|
2101
|
+
}
|
|
2102
|
+
return { skipped: [deleteRefusalMessage(eventId, decision)] };
|
|
2103
|
+
}
|
|
2104
|
+
try {
|
|
2105
|
+
await deps.deleteEvent(eventId, decision.etag);
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
if (!isGoogleApiError(error)) throw error;
|
|
2108
|
+
if (EVENT_ABSENT_STATUSES.includes(error.status)) {
|
|
2109
|
+
await deps.forget(eventId);
|
|
2110
|
+
return {};
|
|
2111
|
+
}
|
|
2112
|
+
if (error.status !== 412) throw error;
|
|
2113
|
+
return { skipped: [staleDeleteMessage(eventId)] };
|
|
2114
|
+
}
|
|
2115
|
+
await deps.forget(eventId);
|
|
2116
|
+
log.info("google", "calendar event deleted by push", { id: eventId });
|
|
2117
|
+
return { deleted: [eventId] };
|
|
2118
|
+
}
|
|
2119
|
+
/** Carry every local deletion, when the collection opted in.
|
|
2120
|
+
*
|
|
2121
|
+
* Sequential rather than concurrent: each one writes the shared baseline file,
|
|
2122
|
+
* and an interrupted run has to leave a state the next run resumes from rather
|
|
2123
|
+
* than a partially applied batch. */
|
|
2124
|
+
async function sweepDeletes(eventIds, propagate, deps) {
|
|
2125
|
+
if (!propagate || eventIds.length === 0) return noDeletes(eventIds.length);
|
|
2126
|
+
const sweep = noDeletes(eventIds.length);
|
|
2127
|
+
for (const eventId of eventIds) try {
|
|
2128
|
+
const one = await propagateOneDelete(eventId, deps);
|
|
2129
|
+
sweep.deleted.push(...one.deleted ?? []);
|
|
2130
|
+
sweep.skipped.push(...one.skipped ?? []);
|
|
2131
|
+
} catch (error) {
|
|
2132
|
+
sweep.errors.push(`${eventId}: could not be deleted in Google — ${errorMessage(error)}`);
|
|
2133
|
+
}
|
|
2134
|
+
return sweep;
|
|
2135
|
+
}
|
|
1894
2136
|
/** Push one collection WITHOUT taking the calendar lock.
|
|
1895
2137
|
*
|
|
1896
2138
|
* The caller must already hold it. The scheduled push→pull cycle
|
|
@@ -1929,7 +2171,18 @@ async function pushCollectionNow(collection, workspaceRoot, deps = liveDeps) {
|
|
|
1929
2171
|
});
|
|
1930
2172
|
await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
|
|
1931
2173
|
}
|
|
1932
|
-
const result = tally(slug, attempts, locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))).
|
|
2174
|
+
const result = tally(slug, attempts, await sweepDeletes(locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))), schema.googleCalendar?.propagateDeletes === true, {
|
|
2175
|
+
fetchEvent: (eventId) => (deps.fetchEvent ?? getCalendarEvent)(accessToken, {
|
|
2176
|
+
calendarId,
|
|
2177
|
+
eventId
|
|
2178
|
+
}),
|
|
2179
|
+
deleteEvent: (eventId, ifMatch) => (deps.deleteEvent ?? deleteCalendarEvent)(accessToken, {
|
|
2180
|
+
calendarId,
|
|
2181
|
+
eventId,
|
|
2182
|
+
ifMatch
|
|
2183
|
+
}),
|
|
2184
|
+
forget: (eventId) => saveCalendarShadow(calendarId, { [eventId]: null }, workspaceRoot)
|
|
2185
|
+
}));
|
|
1933
2186
|
if (result.errors.length > 0) log.warn("google", "calendar push finished with errors", {
|
|
1934
2187
|
slug,
|
|
1935
2188
|
errors: result.errors.length
|
|
@@ -2887,6 +3140,6 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
2887
3140
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
2888
3141
|
}
|
|
2889
3142
|
//#endregion
|
|
2890
|
-
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 };
|
|
3143
|
+
export { ALL_DAY_END_HINT, CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, EVENT_ABSENT_STATUSES, EVENT_TIME_HINT, 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, LONE_ALL_DAY_HINT, MAX_LIST_RESULTS, MIXED_SPAN_HINT, 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, conferenceVideoUri, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteRefusalMessage, deleteTask, eventTimeHint, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarDateOnly, 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, planDelete, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resolvePartialSpanInput, resolveSpanInput, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, selfResponseStatus, shadowUpdates, stateLockPath, sweepDeletes, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toEventTimeInput, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
|
|
2891
3144
|
|
|
2892
3145
|
//# sourceMappingURL=index.js.map
|