@read-frog/api-contract 0.23.0 → 0.25.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.
@@ -4,8 +4,10 @@ import {
4
4
  cardStateSchema,
5
5
  fsrsReviewLogSnapshotSchema,
6
6
  fsrsMemoryStateSchema,
7
+ POSTPONE_DAMAGE_THRESHOLD,
7
8
  reviewRatingSchema,
8
9
  scheduleStatusSchema,
10
+ schedulingParamsShape,
9
11
  schedulingParamsSchema,
10
12
  SRS_REVIEW_DURATION_MS_MAX,
11
13
  srsStepSchema,
@@ -152,7 +154,9 @@ export type SrsListCardRevlogsOutput = z.infer<typeof srsListCardRevlogsOutputSc
152
154
  export const srsReviewOutputSchema = z.object({
153
155
  card: cardSchema,
154
156
  revlog: srsRevlogSchema,
155
- txid: z.number(),
157
+ // Null for an idempotent replay: it writes nothing, so no shape ever carries
158
+ // its transaction. Wait for the returned card and revlog instead.
159
+ txid: z.number().nullable(),
156
160
  // False when an idempotent replay returned an existing revlog instead of creating one.
157
161
  created: z.boolean(),
158
162
  });
@@ -259,3 +263,176 @@ function bytesToUuid(bytes: Uint8Array) {
259
263
  hex.slice(10, 16).join(""),
260
264
  ].join("-");
261
265
  }
266
+
267
+ const count = z.number().int().nonnegative();
268
+
269
+ export const srsBacklogStatsInputSchema = z
270
+ .object({
271
+ notebaseIds: z.array(z.uuid()).optional(),
272
+ timezone: canonicalTimezoneSchema,
273
+ })
274
+ .strict();
275
+ export type SrsBacklogStatsInput = z.infer<typeof srsBacklogStatsInputSchema>;
276
+
277
+ /**
278
+ * An Awake notebase with a positive daily review limit whose overdue reviews
279
+ * fill at least `BACKLOG_PROMPT_DAYS.badge` days.
280
+ */
281
+ export const srsBacklogStatsSchema = z.object({
282
+ overdueReviewCount: count.describe(
283
+ "Review cards whose due date is before the start of today's study day.",
284
+ ),
285
+ dailyLimit: z
286
+ .number()
287
+ .int()
288
+ .positive()
289
+ .describe(
290
+ "The notebase's own daily review limit, which the backlog is measured against. Always positive: a notebase with no daily reviews (0) or an unlimited limit (-1) is never listed.",
291
+ ),
292
+ backlogDays: z
293
+ .number()
294
+ .nonnegative()
295
+ .describe("Days of reviews the backlog fills at that limit: overdueReviewCount / dailyLimit."),
296
+ });
297
+ export type SrsBacklogStats = z.infer<typeof srsBacklogStatsSchema>;
298
+
299
+ /** Only the notebases that get the badge; the rest are absent. */
300
+ export const srsBacklogStatsOutputSchema = z.record(z.uuid(), srsBacklogStatsSchema);
301
+ export type SrsBacklogStatsOutput = z.infer<typeof srsBacklogStatsOutputSchema>;
302
+
303
+ const rescheduleDailyLimitSchema = z.number().int().positive();
304
+
305
+ export const srsPreviewPostponeInputSchema = z
306
+ .object({
307
+ notebaseId: z.uuid(),
308
+ timezone: canonicalTimezoneSchema,
309
+ dailyLimit: rescheduleDailyLimitSchema.optional(),
310
+ flattenFullRange: z
311
+ .boolean()
312
+ .optional()
313
+ .describe("Flatten levels every plannable day (180) instead of only the chart days."),
314
+ })
315
+ .strict();
316
+ export type SrsPreviewPostponeInput = z.infer<typeof srsPreviewPostponeInputSchema>;
317
+
318
+ const postponeThresholdSchema = z
319
+ .number()
320
+ .min(POSTPONE_DAMAGE_THRESHOLD.min)
321
+ .max(POSTPONE_DAMAGE_THRESHOLD.max);
322
+
323
+ const postponeDaySchema = z.object({
324
+ day: z.number().int().positive(),
325
+ postponed: count,
326
+ rSum: z.number(),
327
+ });
328
+
329
+ const rescheduleBaselineDaySchema = z.object({
330
+ day: z.number().int().positive(),
331
+ scheduled: count,
332
+ rSum: z.number().nullable(),
333
+ });
334
+ export type RescheduleBaselineDay = z.infer<typeof rescheduleBaselineDaySchema>;
335
+
336
+ export const spreadDaySchema = z.object({
337
+ day: count,
338
+ scheduled: count,
339
+ backlog: count,
340
+ backlogRSum: z.number(),
341
+ surplusIn: count,
342
+ surplusRSum: z.number(),
343
+ surplusStay: count,
344
+ surplusOut: count,
345
+ });
346
+ export type SpreadDay = z.infer<typeof spreadDaySchema>;
347
+
348
+ const spreadKeptSchema = z.object({
349
+ stuck: count,
350
+ noRoom: count,
351
+ });
352
+
353
+ export const flattenStepSchema = z.object({
354
+ checked: z.boolean(),
355
+ fromToday: count,
356
+ fromFuture: count,
357
+ fromDays: count,
358
+ kept: spreadKeptSchema,
359
+ qSuggest: count,
360
+ meanRAfter: z.number(),
361
+ days: z.array(spreadDaySchema),
362
+ /** Cards Postpone already moved that this Flatten moves again, and their R under Postpone. */
363
+ postponedAgain: z.object({ count, rSum: z.number() }),
364
+ });
365
+ export type FlattenStep = z.infer<typeof flattenStepSchema>;
366
+
367
+ /** One threshold's Postpone alone, before its Flatten option is attached. */
368
+ export const postponeSummarySchema = z.object({
369
+ threshold: postponeThresholdSchema,
370
+ moved: count,
371
+ overflowToday: count,
372
+ meanRiBefore: z.number(),
373
+ meanRAfter: z.number(),
374
+ days: z.array(postponeDaySchema),
375
+ });
376
+ export type PostponeSummary = z.infer<typeof postponeSummarySchema>;
377
+
378
+ export const postponeStepSchema = postponeSummarySchema.extend({
379
+ flatten: flattenStepSchema.nullable(),
380
+ });
381
+ export type PostponeStep = z.infer<typeof postponeStepSchema>;
382
+
383
+ export const postponePlanSchema = z.object({
384
+ planId: z.string(),
385
+ limit: z.number().int().positive(),
386
+ steps: z.array(postponeStepSchema),
387
+ skipped: z.object({
388
+ belowFloor: count,
389
+ maxInterval: count,
390
+ invalid: count,
391
+ }),
392
+ });
393
+ export type PostponePlanSummary = z.infer<typeof postponePlanSchema>;
394
+
395
+ const rescheduleReadySchema = z.object({
396
+ status: z.literal("ready"),
397
+ expiresAt: z.date(),
398
+ q: z.number().int().positive(),
399
+ qConfigured: schedulingParamsShape.reviewsPerDay,
400
+ qMax: z.number().int().positive(),
401
+ overdueReviewCount: count,
402
+ dueTodayReviewCount: count,
403
+ usedToday: count,
404
+ });
405
+
406
+ export const srsPreviewPostponeOutputSchema = z.discriminatedUnion("status", [
407
+ rescheduleReadySchema.extend({
408
+ plan: postponePlanSchema,
409
+ suggested: postponePlanSchema.nullable(),
410
+ baseline: z.array(rescheduleBaselineDaySchema),
411
+ }),
412
+ z.object({ status: z.literal("quotaZero") }),
413
+ z.object({ status: z.literal("invalidWeights") }),
414
+ ]);
415
+ export type SrsPreviewPostponeOutput = z.infer<typeof srsPreviewPostponeOutputSchema>;
416
+
417
+ const rescheduleApplyInputSchema = z.object({
418
+ notebaseId: z.uuid(),
419
+ planId: z.string().min(1),
420
+ unlimited: z.boolean().optional(),
421
+ });
422
+
423
+ export const srsApplyPostponeInputSchema = rescheduleApplyInputSchema
424
+ .extend({
425
+ threshold: postponeThresholdSchema,
426
+ flatten: z.boolean().optional(),
427
+ })
428
+ .strict();
429
+ export type SrsApplyPostponeInput = z.infer<typeof srsApplyPostponeInputSchema>;
430
+
431
+ export const srsRescheduleApplyOutputSchema = z.object({
432
+ moved: count,
433
+ txid: z.number().nullable(),
434
+ reviewsPerDay: schedulingParamsShape.reviewsPerDay,
435
+ });
436
+ export type SrsRescheduleApplyOutput = z.infer<typeof srsRescheduleApplyOutputSchema>;
437
+ export const srsApplyPostponeOutputSchema = srsRescheduleApplyOutputSchema;
438
+ export type SrsApplyPostponeOutput = z.infer<typeof srsApplyPostponeOutputSchema>;
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+
3
+ // What the web app reports about keeping its Electric data live. Every field is
4
+ // an enum, so each label the API counts them under has a fixed set of values.
5
+
6
+ /** The shapes the web app syncs, by their names in `@repo/electric-contract`. */
7
+ export const webSyncShapeSchema = z.enum([
8
+ "notebases",
9
+ "notebaseColumns",
10
+ "notebaseRows",
11
+ "cardTemplates",
12
+ "cards",
13
+ "revlogs",
14
+ "mediaAssets",
15
+ ]);
16
+ export type WebSyncShape = z.infer<typeof webSyncShapeSchema>;
17
+
18
+ /** The write whose change the page waited to see come back over Electric. */
19
+ export const webSyncOperationSchema = z.enum([
20
+ // The review session's own writes.
21
+ "review",
22
+ // A review the server had already recorded: it waits for that review's card
23
+ // and revlog, as its own transaction wrote nothing.
24
+ "reviewReplay",
25
+ "undo",
26
+ "bury",
27
+ "suspend",
28
+ "deleteNote",
29
+ // A synced collection's own mutation handlers.
30
+ "insert",
31
+ "update",
32
+ "delete",
33
+ // Other writes that wait for their change.
34
+ "setSleep",
35
+ "srsSettings",
36
+ "cardStatus",
37
+ "createTemplate",
38
+ "updateTemplate",
39
+ ]);
40
+ export type WebSyncOperation = z.infer<typeof webSyncOperationSchema>;
41
+
42
+ /** How the shape that lagged was receiving live changes. */
43
+ export const webSyncTransportSchema = z.enum(["sse", "longPoll"]);
44
+ export type WebSyncTransport = z.infer<typeof webSyncTransportSchema>;
45
+
46
+ /**
47
+ * A write wait is only reported once its first wait ran out: `recovered` when
48
+ * reconnecting the lagging shapes brought the change, `failed` when the page
49
+ * gave up on it.
50
+ */
51
+ export const webSyncWriteOutcomeSchema = z.enum(["recovered", "failed"]);
52
+ export type WebSyncWriteOutcome = z.infer<typeof webSyncWriteOutcomeSchema>;
53
+
54
+ export const webSyncStreamErrorTypeSchema = z.enum([
55
+ // 401: the session ended.
56
+ "unauthorized",
57
+ // 403: the notebase was deleted, or is someone else's.
58
+ "forbidden",
59
+ // 400 and any other 4xx.
60
+ "badRequest",
61
+ "clientError",
62
+ // A response without Electric's headers: a proxy or CORS in the way.
63
+ "missingHeaders",
64
+ // Anything that is not an HTTP status: an unreadable body, a parse error.
65
+ "unexpected",
66
+ ]);
67
+ export type WebSyncStreamErrorType = z.infer<typeof webSyncStreamErrorTypeSchema>;
68
+
69
+ /** `initial` before the shape first loaded, when the page shows the error at once. */
70
+ export const webSyncStreamPhaseSchema = z.enum(["initial", "live"]);
71
+ export type WebSyncStreamPhase = z.infer<typeof webSyncStreamPhaseSchema>;
72
+
73
+ export const webSyncPageSchema = z.enum(["reviewCombined", "reviewNotebase"]);
74
+ export type WebSyncPage = z.infer<typeof webSyncPageSchema>;
75
+
76
+ export const webSyncUnavailableReasonSchema = z.enum([
77
+ // A write's change never came back, even after reconnecting.
78
+ "syncTimeout",
79
+ // A shape stream stopped.
80
+ "streamFailed",
81
+ // The notebase is gone: its shapes answer 403.
82
+ "notebaseGone",
83
+ ]);
84
+ export type WebSyncUnavailableReason = z.infer<typeof webSyncUnavailableReasonSchema>;
85
+
86
+ export const webSyncEventSchema = z.discriminatedUnion("type", [
87
+ z
88
+ .object({
89
+ type: z.literal("writeWait"),
90
+ operation: webSyncOperationSchema,
91
+ outcome: webSyncWriteOutcomeSchema,
92
+ transport: webSyncTransportSchema,
93
+ })
94
+ .strict(),
95
+ z
96
+ .object({
97
+ type: z.literal("streamError"),
98
+ shape: webSyncShapeSchema,
99
+ errorType: webSyncStreamErrorTypeSchema,
100
+ phase: webSyncStreamPhaseSchema,
101
+ })
102
+ .strict(),
103
+ z
104
+ .object({
105
+ type: z.literal("unavailable"),
106
+ page: webSyncPageSchema,
107
+ reason: webSyncUnavailableReasonSchema,
108
+ })
109
+ .strict(),
110
+ ]);
111
+ export type WebSyncEvent = z.infer<typeof webSyncEventSchema>;
112
+
113
+ /** A page sends what happened within a second or so as one batch. */
114
+ export const WEB_SYNC_REPORT_MAX_EVENTS = 20;
115
+
116
+ export const reportWebSyncInputSchema = z
117
+ .object({
118
+ events: z.array(webSyncEventSchema).min(1).max(WEB_SYNC_REPORT_MAX_EVENTS),
119
+ })
120
+ .strict();
121
+ export type ReportWebSyncInput = z.infer<typeof reportWebSyncInputSchema>;