canopycms 0.12.1 → 0.13.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/CHANGELOG.md +211 -49
- package/dist/api/branch-review.d.ts +20 -2
- package/dist/api/branch-review.js +112 -23
- package/dist/api/branch-status.d.ts +11 -2
- package/dist/api/branch-status.js +3 -2
- package/dist/api/branch-update-from-base.js +57 -6
- package/dist/api/branch.d.ts +5 -0
- package/dist/api/branch.js +27 -2
- package/dist/api/client.d.ts +2 -2
- package/dist/api/client.js +2 -2
- package/dist/api/content.js +1 -1
- package/dist/api/merge-conflicts.d.ts +10 -1
- package/dist/api/merge-conflicts.js +42 -7
- package/dist/api/types.d.ts +3 -2
- package/dist/base-update.d.ts +6 -0
- package/dist/branch-metadata-file.d.ts +15 -0
- package/dist/branch-metadata-file.js +5 -1
- package/dist/cli/cli.js +238 -108
- package/dist/cli/generate-ai-content.js +20 -6
- package/dist/cli/github-app-manifest.d.ts +4 -1
- package/dist/cli/github-app-manifest.js +5 -1
- package/dist/cli/init.js +7 -1
- package/dist/editor/Editor.js +39 -16
- package/dist/editor/admin/ConflictsStatusAlert.d.ts +8 -0
- package/dist/editor/admin/ConflictsStatusAlert.js +22 -0
- package/dist/editor/admin/SystemHealthPanel.js +4 -3
- package/dist/editor/base-update-wait.d.ts +4 -0
- package/dist/editor/base-update-wait.js +7 -0
- package/dist/editor/components/ConflictsGroup.d.ts +13 -1
- package/dist/editor/components/ConflictsGroup.js +52 -12
- package/dist/editor/components/EditorHeader.d.ts +3 -1
- package/dist/editor/components/EditorHeader.js +11 -7
- package/dist/editor/components/EntryChangesDrawer.d.ts +1 -1
- package/dist/editor/components/EntryChangesDrawer.js +58 -6
- package/dist/editor/conflict-cards.d.ts +4 -1
- package/dist/editor/conflict-cards.js +14 -9
- package/dist/editor/hooks/useBaseUpdate.d.ts +2 -0
- package/dist/editor/hooks/useBaseUpdate.js +12 -2
- package/dist/editor/hooks/useBranchChangesData.d.ts +7 -1
- package/dist/editor/hooks/useBranchChangesData.js +13 -0
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +80 -6
- package/dist/editor/hooks/useDraftManager.d.ts +8 -1
- package/dist/editor/hooks/useDraftManager.js +16 -1
- package/dist/editor/hooks/useEntryManager.d.ts +8 -3
- package/dist/editor/hooks/useEntryManager.js +6 -5
- package/dist/git-manager.d.ts +1 -0
- package/dist/git-manager.js +8 -3
- package/dist/merge/entry-merge.js +4 -3
- package/dist/merge/list-alignment.d.ts +9 -6
- package/dist/merge/list-alignment.js +65 -16
- package/dist/merge/rekey.d.ts +11 -0
- package/dist/merge/rekey.js +75 -0
- package/dist/operating-mode/client-safe-strategy.js +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +6 -0
- package/dist/operating-mode/types.d.ts +10 -2
- package/dist/services.d.ts +12 -4
- package/dist/services.js +56 -0
- package/dist/types.d.ts +35 -3
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.js +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-merge.d.ts +27 -3
- package/dist/worker/branch-merge.js +106 -5
- package/dist/worker/branch-policy.d.ts +18 -2
- package/dist/worker/branch-policy.js +36 -0
- package/dist/worker/cms-worker.d.ts +21 -0
- package/dist/worker/cms-worker.js +29 -4
- package/dist/worker/conflicts-status.d.ts +20 -0
- package/dist/worker/conflicts-status.js +204 -0
- package/dist/worker/field-merge.d.ts +17 -2
- package/dist/worker/field-merge.js +95 -2
- package/dist/worker/git-sync.d.ts +3 -2
- package/dist/worker/git-sync.js +9 -0
- package/dist/worker/github-commits.d.ts +3 -3
- package/dist/worker/github-commits.js +192 -40
- package/dist/worker/github-errors.d.ts +15 -0
- package/dist/worker/github-errors.js +47 -0
- package/dist/worker/github-gateway-http.js +7 -0
- package/dist/worker/github-gateway-wire.d.ts +30 -10
- package/dist/worker/github-gateway-wire.js +14 -2
- package/dist/worker/github-gateway.d.ts +17 -0
- package/dist/worker/github-gateway.js +37 -1
- package/dist/worker/history-rewrite.d.ts +40 -8
- package/dist/worker/history-rewrite.js +51 -23
- package/dist/worker/publish-decision.d.ts +37 -0
- package/dist/worker/publish-decision.js +16 -0
- package/dist/worker/rebase.d.ts +25 -5
- package/dist/worker/rebase.js +225 -81
- package/dist/worker/task-runner.js +16 -39
- package/dist/worker/worker-context.d.ts +6 -0
- package/package.json +1 -1
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
+
import { SYNC_BRANCH_ACTION } from '../base-update.js';
|
|
10
|
+
import { operatingStrategy } from '../operating-mode/index.js';
|
|
11
|
+
import { enqueueTask } from '../task-queue/cms-task-queue.js';
|
|
12
|
+
import { getTaskQueueDir } from '../task-queue/task-queue-config.js';
|
|
9
13
|
import { entryRefFromRepoPath, entryRefFromRepoPathShape, parseEntryParts, } from '../content-listing.js';
|
|
10
14
|
import { BranchSyncingError, ContentConflictError, ContentStore, ContentStoreError, EntryGoneError, } from '../content-store.js';
|
|
11
15
|
import { keyOfDataPath } from '../merge/conflict-path.js';
|
|
@@ -127,12 +131,28 @@ const mayAccess = (record, checkAccess, user, level) => {
|
|
|
127
131
|
return admin;
|
|
128
132
|
return [record.address, record.recordedAddress ?? record.address].every((at) => checkAccess(at, level).allowed);
|
|
129
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* Wake the worker's git sync for `branch`, whose last open choice was just resolved, so the pull
|
|
136
|
+
* request's conflicts status (worker/conflicts-status.ts) turns to success within a sync's rest: a
|
|
137
|
+
* required check would otherwise hold the merge until the next regular cycle. Only where a worker
|
|
138
|
+
* answers the queue; best-effort, as that next cycle sets it anyway.
|
|
139
|
+
*/
|
|
140
|
+
async function wakeConflictsStatus(config, branch) {
|
|
141
|
+
if (operatingStrategy(config.mode).baseUpdateRunsInRequest())
|
|
142
|
+
return;
|
|
143
|
+
try {
|
|
144
|
+
await enqueueTask(getTaskQueueDir(config), { action: SYNC_BRANCH_ACTION, payload: { branch } });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
canopyLogWarn(`CanopyCMS: Could not queue a sync for ${branch} after its last conflict was resolved:`, getErrorMessage(err));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
130
150
|
/**
|
|
131
151
|
* Change the record's entries by `change`, under the record's locks, then `branch.json`
|
|
132
|
-
* `conflictFiles` to match
|
|
133
|
-
* or null.
|
|
152
|
+
* `conflictFiles` to match, and wake the worker when that resolved the last open choice.
|
|
153
|
+
* `change` returning null writes nothing. Resolves the entries written, or null.
|
|
134
154
|
*/
|
|
135
|
-
const changeRecord = async (branchContext, change) => {
|
|
155
|
+
const changeRecord = async (config, branchContext, change) => {
|
|
136
156
|
const state = await new MergeConflictStore(branchContext.branchRoot).update((current) => {
|
|
137
157
|
if (current === null)
|
|
138
158
|
return null;
|
|
@@ -144,6 +164,10 @@ const changeRecord = async (branchContext, change) => {
|
|
|
144
164
|
await getBranchMetadataFileManager(branchContext.branchRoot, branchContext.baseRoot).update((existing) => existing?.branch.name === branchContext.branch.name
|
|
145
165
|
? { branch: conflictFilesOf(state.entries) }
|
|
146
166
|
: null, { keepUpdatedAt: true });
|
|
167
|
+
// Each change takes a choice off, so none open now means this one took the last.
|
|
168
|
+
if (openConflictCount(state.entries) === 0 && !state.held) {
|
|
169
|
+
await wakeConflictsStatus(config, branchContext.branch.name);
|
|
170
|
+
}
|
|
147
171
|
return state.entries;
|
|
148
172
|
};
|
|
149
173
|
/**
|
|
@@ -152,9 +176,9 @@ const changeRecord = async (branchContext, change) => {
|
|
|
152
176
|
* call once the entry is written, inside its locks: the record's own locks come after them in
|
|
153
177
|
* the one lock order (docs/concurrency.md).
|
|
154
178
|
*/
|
|
155
|
-
export async function clearFieldConflicts(branchContext, contentId, resolved) {
|
|
179
|
+
export async function clearFieldConflicts(config, branchContext, contentId, resolved) {
|
|
156
180
|
let cleared = [];
|
|
157
|
-
await changeRecord(branchContext, (entries) => {
|
|
181
|
+
await changeRecord(config, branchContext, (entries) => {
|
|
158
182
|
const next = withoutResolvedFields(entries, contentId, resolved);
|
|
159
183
|
cleared = next.cleared;
|
|
160
184
|
return next.cleared.length > 0 ? next.entries : null;
|
|
@@ -408,6 +432,8 @@ const resolveBodySchema = z.object({
|
|
|
408
432
|
* field by field with the latest version (`unreadable`).
|
|
409
433
|
*/
|
|
410
434
|
choice: z.enum(['keep', 'delete', 'restore', 'use-latest']),
|
|
435
|
+
/** The card promised the entry stays deleted: a `keep` is refused if it is in the tree now. */
|
|
436
|
+
entryGone: z.literal(true).optional(),
|
|
411
437
|
});
|
|
412
438
|
/** Which kinds of record each choice resolves. */
|
|
413
439
|
const CHOICE_KINDS = {
|
|
@@ -489,10 +515,16 @@ const resolveHandler = async (gc, ctx, req, params, body) => {
|
|
|
489
515
|
if (current?.kind !== entry.kind || current.mergeSha !== body.mergeSha) {
|
|
490
516
|
throw new ConflictChangedError();
|
|
491
517
|
}
|
|
518
|
+
// Read afresh: a restore since the drawer opened would make "keep it deleted" untrue.
|
|
519
|
+
if (body.entryGone) {
|
|
520
|
+
store.invalidateIndex();
|
|
521
|
+
if ((await store.idIndex()).findById(body.id) !== null)
|
|
522
|
+
throw new ConflictChangedError();
|
|
523
|
+
}
|
|
492
524
|
};
|
|
493
525
|
// Clears the record as it stands under its lock, refusing when a merge changed it since.
|
|
494
526
|
const clear = async () => {
|
|
495
|
-
const written = await changeRecord(branchContext, (now) => {
|
|
527
|
+
const written = await changeRecord(ctx.services.config, branchContext, (now) => {
|
|
496
528
|
const current = now[body.id];
|
|
497
529
|
if (current?.kind === 'fields' && current.mergeSha === body.mergeSha) {
|
|
498
530
|
const { [body.id]: _kept, ...rest } = now;
|
|
@@ -628,7 +660,7 @@ const resolveEntryConflict = defineEndpoint({
|
|
|
628
660
|
* clashed is lost, and the values stay as the merges left them. Under the content-write lock, as a
|
|
629
661
|
* merge's record is written.
|
|
630
662
|
*/
|
|
631
|
-
const releaseHoldHandler = async (gc) => {
|
|
663
|
+
const releaseHoldHandler = async (gc, ctx) => {
|
|
632
664
|
const { branchRoot } = gc.branchContext;
|
|
633
665
|
try {
|
|
634
666
|
const written = await withContentWriteLock(branchRoot, () => new MergeConflictStore(branchRoot).update((current) => {
|
|
@@ -636,6 +668,9 @@ const releaseHoldHandler = async (gc) => {
|
|
|
636
668
|
return null;
|
|
637
669
|
return { ...current, held: undefined };
|
|
638
670
|
}));
|
|
671
|
+
if (written !== null && openConflictCount(written.entries) === 0) {
|
|
672
|
+
await wakeConflictsStatus(ctx.services.config, gc.branchContext.branch.name);
|
|
673
|
+
}
|
|
639
674
|
return { ok: true, status: 200, data: { released: written !== null } };
|
|
640
675
|
}
|
|
641
676
|
catch (err) {
|
package/dist/api/types.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export interface ApiResponse<TData = unknown> {
|
|
|
70
70
|
* another name can pass; no other create failure carries it. `OPEN_CONFLICTS`: a submit or approval
|
|
71
71
|
* was refused while a merge's conflicts are open on the branch (a 409, merge-conflicts.ts).
|
|
72
72
|
* `CONFLICT_CHANGED`: a resolve-entry named a conflict the record no longer holds as shown (a 409);
|
|
73
|
-
* re-read the branch's conflicts.
|
|
73
|
+
* re-read the branch's conflicts. `BRANCH_MOVED`: an approval or a request for changes named a commit
|
|
74
|
+
* other than the branch's head, or none (a 409, api/branch-review.ts); reload the branch first.
|
|
74
75
|
*/
|
|
75
|
-
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED' | 'OPEN_CONFLICTS' | 'CONFLICT_CHANGED';
|
|
76
|
+
export type ApiErrorCode = 'SCHEMA_UNAVAILABLE' | 'WORKER_FAILED' | 'WRITE_OUTCOME_UNKNOWN' | 'ENTRY_GONE' | 'EDITOR_MODE_MISMATCH' | 'SETTINGS_CHANGED' | 'BRANCH_SYNCING' | 'BRANCH_NAME_REFUSED' | 'OPEN_CONFLICTS' | 'CONFLICT_CHANGED' | 'BRANCH_MOVED';
|
package/dist/base-update.d.ts
CHANGED
|
@@ -22,5 +22,11 @@ export declare const BASE_UPDATE_EARLY_RETRY_MS: number;
|
|
|
22
22
|
* so a later one is corrupt and is never waited on.
|
|
23
23
|
*/
|
|
24
24
|
export declare const BASE_UPDATE_CLOCK_SKEW_MS = 60000;
|
|
25
|
+
/**
|
|
26
|
+
* How a run in the request (operating-mode `baseUpdateRunsInRequest`) went: it answered the
|
|
27
|
+
* request; a lock held it back; or the branch clones from a remote other than the workspace's
|
|
28
|
+
* `remote.git`, the only one such a run fetches.
|
|
29
|
+
*/
|
|
30
|
+
export type BaseUpdateRun = 'answered' | 'busy' | 'other-remote';
|
|
25
31
|
/** Whether the branch's latest request for the latest changes is still unanswered. */
|
|
26
32
|
export declare function isBaseUpdatePending(branch: Pick<BranchMetadata, 'baseUpdateRequestedAt' | 'lastBaseUpdate'>): boolean;
|
|
@@ -83,6 +83,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
83
83
|
lastAt: string;
|
|
84
84
|
}>>;
|
|
85
85
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
86
|
+
historyRewrittenTo: z.ZodOptional<z.ZodString>;
|
|
86
87
|
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
87
88
|
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
88
89
|
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
@@ -121,6 +122,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
121
122
|
name?: string | undefined;
|
|
122
123
|
}>;
|
|
123
124
|
at: z.ZodString;
|
|
125
|
+
commit: z.ZodOptional<z.ZodString>;
|
|
124
126
|
}, "strip", z.ZodTypeAny, {
|
|
125
127
|
at: string;
|
|
126
128
|
state: "approved" | "changes-requested";
|
|
@@ -128,6 +130,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
128
130
|
userId: string;
|
|
129
131
|
name?: string | undefined;
|
|
130
132
|
};
|
|
133
|
+
commit?: string | undefined;
|
|
131
134
|
}, {
|
|
132
135
|
at: string;
|
|
133
136
|
state: "approved" | "changes-requested";
|
|
@@ -135,8 +138,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
135
138
|
userId: string;
|
|
136
139
|
name?: string | undefined;
|
|
137
140
|
};
|
|
141
|
+
commit?: string | undefined;
|
|
138
142
|
}>>>;
|
|
139
143
|
baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
144
|
+
latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
140
145
|
lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
141
146
|
at: z.ZodString;
|
|
142
147
|
outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
|
|
@@ -216,6 +221,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
216
221
|
lastAt: string;
|
|
217
222
|
}>>;
|
|
218
223
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
224
|
+
historyRewrittenTo: z.ZodOptional<z.ZodString>;
|
|
219
225
|
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
220
226
|
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
221
227
|
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
@@ -254,6 +260,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
254
260
|
name?: string | undefined;
|
|
255
261
|
}>;
|
|
256
262
|
at: z.ZodString;
|
|
263
|
+
commit: z.ZodOptional<z.ZodString>;
|
|
257
264
|
}, "strip", z.ZodTypeAny, {
|
|
258
265
|
at: string;
|
|
259
266
|
state: "approved" | "changes-requested";
|
|
@@ -261,6 +268,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
261
268
|
userId: string;
|
|
262
269
|
name?: string | undefined;
|
|
263
270
|
};
|
|
271
|
+
commit?: string | undefined;
|
|
264
272
|
}, {
|
|
265
273
|
at: string;
|
|
266
274
|
state: "approved" | "changes-requested";
|
|
@@ -268,8 +276,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
268
276
|
userId: string;
|
|
269
277
|
name?: string | undefined;
|
|
270
278
|
};
|
|
279
|
+
commit?: string | undefined;
|
|
271
280
|
}>>>;
|
|
272
281
|
baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
282
|
+
latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
273
283
|
lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
274
284
|
at: z.ZodString;
|
|
275
285
|
outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
|
|
@@ -349,6 +359,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
349
359
|
lastAt: string;
|
|
350
360
|
}>>;
|
|
351
361
|
historyRewrittenFrom: z.ZodOptional<z.ZodString>;
|
|
362
|
+
historyRewrittenTo: z.ZodOptional<z.ZodString>;
|
|
352
363
|
lastGitHubTip: z.ZodOptional<z.ZodString>;
|
|
353
364
|
keptOverGitHub: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
354
365
|
rewrittenOnGitHub: z.ZodOptional<z.ZodString>;
|
|
@@ -387,6 +398,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
387
398
|
name?: string | undefined;
|
|
388
399
|
}>;
|
|
389
400
|
at: z.ZodString;
|
|
401
|
+
commit: z.ZodOptional<z.ZodString>;
|
|
390
402
|
}, "strip", z.ZodTypeAny, {
|
|
391
403
|
at: string;
|
|
392
404
|
state: "approved" | "changes-requested";
|
|
@@ -394,6 +406,7 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
394
406
|
userId: string;
|
|
395
407
|
name?: string | undefined;
|
|
396
408
|
};
|
|
409
|
+
commit?: string | undefined;
|
|
397
410
|
}, {
|
|
398
411
|
at: string;
|
|
399
412
|
state: "approved" | "changes-requested";
|
|
@@ -401,8 +414,10 @@ export declare const branchMetadataSchema: z.ZodObject<{
|
|
|
401
414
|
userId: string;
|
|
402
415
|
name?: string | undefined;
|
|
403
416
|
};
|
|
417
|
+
commit?: string | undefined;
|
|
404
418
|
}>>>;
|
|
405
419
|
baseUpdateRequestedAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
420
|
+
latestChangesWaiting: z.ZodCatch<z.ZodOptional<z.ZodString>>;
|
|
406
421
|
lastBaseUpdate: z.ZodCatch<z.ZodOptional<z.ZodObject<{
|
|
407
422
|
at: z.ZodString;
|
|
408
423
|
outcome: z.ZodEnum<["merged", "up-to-date", "conflicts", "failed"]>;
|
|
@@ -82,6 +82,7 @@ const branchMetadataShape = {
|
|
|
82
82
|
.object({ message: z.string(), firstAt: z.string(), lastAt: z.string() })
|
|
83
83
|
.optional(),
|
|
84
84
|
historyRewrittenFrom: z.string().optional(),
|
|
85
|
+
historyRewrittenTo: z.string().optional(),
|
|
85
86
|
lastGitHubTip: z.string().optional(),
|
|
86
87
|
keptOverGitHub: z.array(z.string()).optional(),
|
|
87
88
|
rewrittenOnGitHub: z.string().optional(),
|
|
@@ -99,16 +100,19 @@ const branchMetadataShape = {
|
|
|
99
100
|
])
|
|
100
101
|
.optional()
|
|
101
102
|
.catch(undefined),
|
|
102
|
-
// Dropped when malformed: it
|
|
103
|
+
// Dropped when malformed, never failing a read: it decorates the PR body, and without `commit`
|
|
104
|
+
// the worker judges a move against the clone's head before it (worker/github-commits.ts).
|
|
103
105
|
review: z
|
|
104
106
|
.object({
|
|
105
107
|
state: z.enum(['approved', 'changes-requested']),
|
|
106
108
|
by: z.object({ userId: z.string(), name: z.string().optional() }),
|
|
107
109
|
at: z.string(),
|
|
110
|
+
commit: z.string().optional(),
|
|
108
111
|
})
|
|
109
112
|
.optional()
|
|
110
113
|
.catch(undefined),
|
|
111
114
|
baseUpdateRequestedAt: z.string().optional().catch(undefined),
|
|
115
|
+
latestChangesWaiting: z.string().optional().catch(undefined),
|
|
112
116
|
// Dropped when malformed: it only ends the editor's wait, which then times out instead.
|
|
113
117
|
lastBaseUpdate: z
|
|
114
118
|
.object({
|