@deepseek-ai/dsh-schedule 0.1.7-alpha.2 → 0.1.7-rc.2

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/lib/invariant.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "@js-temporal/polyfill";
1
2
  import { SessionLogOffset } from "@deepseek-ai/dsh-session";
2
3
  const MIN_FOUR_DIGIT_YEAR_MS = Date.parse("0001-01-01T00:00:00.000Z");
3
4
  const MAX_FOUR_DIGIT_YEAR_MS = Date.parse("9999-12-31T23:59:59.999Z");
@@ -30,9 +31,45 @@ function isRecord(value) {
30
31
  }
31
32
  /** Require exactly the named durable object keys. */
32
33
  function hasExactKeys(value, expected) {
33
- const keys = Object.keys(value).sort();
34
- const wanted = [...expected].sort();
35
- return keys.length === wanted.length && keys.every((key, index) => key === wanted[index]);
34
+ const allowed = new Set(expected);
35
+ return expected.every((key) => key in value) && Object.keys(value).every((key) => allowed.has(key));
36
+ }
37
+ /** Require the named durable keys while admitting further optional members. */
38
+ function hasExactKeysWithOptional(value, required, optional) {
39
+ const allowed = new Set([...required, ...optional]);
40
+ return required.every((key) => key in value) && Object.keys(value).every((key) => allowed.has(key));
41
+ }
42
+ /** Stable diagnostic for a title that is missing or empty after trimming. */
43
+ const REQUIRED_TITLE_MESSAGE = "title is required and must be non-empty after trimming.";
44
+ /**
45
+ * Validate one required stored title at the durable boundary.
46
+ *
47
+ * Only records written after titles became required are read: a missing,
48
+ * blank-after-trim, untrimmed, or over-long stored title is invalid, and no name
49
+ * is derived from the instruction.
50
+ * @param value - Untrusted durable title field.
51
+ * @returns The stored title; an invalid title throws ScheduleLogError.
52
+ */
53
+ function decodeStoredTitle(value) {
54
+ if (typeof value !== "string" || value.trim().length === 0) throw new ScheduleLogError(REQUIRED_TITLE_MESSAGE);
55
+ if (value.length > 120) throw new ScheduleLogError(`title must be at most 120 characters`);
56
+ if (value.trim() !== value) throw new ScheduleLogError("title must be a trimmed string");
57
+ return value;
58
+ }
59
+ /**
60
+ * Decode the stored title of one historical `schedule/change` create record.
61
+ *
62
+ * A version-1 event written before names existed has no `title` member, so the
63
+ * key set is required without it and the absent member decodes as undefined. A
64
+ * present title stays subject to the canonical stored form.
65
+ * @param value - Untrusted durable record already known to be an object.
66
+ * @param expected - Exact keys the record may carry; `title` is optional within them.
67
+ * @param message - Diagnostic naming the record's keys.
68
+ * @returns The stored title, or undefined when the historical record predates it.
69
+ */
70
+ function decodeHistoricalRecordTitle(value, expected, message) {
71
+ if (!hasExactKeysWithOptional(value, expected.filter((key) => key !== "title"), ["title"])) throw new ScheduleLogError(message);
72
+ return value["title"] === void 0 ? void 0 : decodeStoredTitle(value["title"]);
36
73
  }
37
74
  /** Validate one stable session-local id at the durable boundary. */
38
75
  function decodeId(value) {
@@ -48,13 +85,16 @@ function decodeInstant(value) {
48
85
  }
49
86
  /** Decode the exact v1 after record shape. */
50
87
  function decodeAfterRecord(value) {
51
- if (!isRecord(value) || !hasExactKeys(value, [
88
+ /* v8 ignore next -- decodeLegacyScheduleRecord rejects a non-object before it dispatches here. */
89
+ if (!isRecord(value)) throw new ScheduleLogError("after schedule must be an object");
90
+ const title = decodeHistoricalRecordTitle(value, [
52
91
  "id",
53
92
  "kind",
93
+ "title",
54
94
  "prompt",
55
95
  "afterSeconds",
56
96
  "scheduledAt"
57
- ])) throw new ScheduleLogError("after schedule must contain exactly id, kind, prompt, afterSeconds, and scheduledAt");
97
+ ], "after schedule must contain exactly id, kind, title, prompt, afterSeconds, and scheduledAt");
58
98
  const prompt = value["prompt"];
59
99
  if (typeof prompt !== "string" || prompt.length === 0 || prompt.trim() !== prompt) throw new ScheduleLogError("after prompt must be non-empty and already trimmed");
60
100
  const afterSeconds = value["afterSeconds"];
@@ -62,6 +102,7 @@ function decodeAfterRecord(value) {
62
102
  return Object.freeze({
63
103
  id: decodeId(value["id"]),
64
104
  kind: "after",
105
+ ...title === void 0 ? {} : { title },
65
106
  prompt,
66
107
  afterSeconds,
67
108
  scheduledAt: decodeInstant(value["scheduledAt"])
@@ -69,45 +110,60 @@ function decodeAfterRecord(value) {
69
110
  }
70
111
  /** Decode the exact v1 absolute one-shot record shape. */
71
112
  function decodeAtRecord(value) {
72
- if (!isRecord(value) || !hasExactKeys(value, [
113
+ /* v8 ignore next -- decodeLegacyScheduleRecord rejects a non-object before it dispatches here. */
114
+ if (!isRecord(value)) throw new ScheduleLogError("at schedule must be an object");
115
+ const title = decodeHistoricalRecordTitle(value, [
73
116
  "id",
74
117
  "kind",
118
+ "title",
75
119
  "prompt",
76
120
  "scheduledAt"
77
- ])) throw new ScheduleLogError("at schedule must contain exactly id, kind, prompt, and scheduledAt");
121
+ ], "at schedule must contain exactly id, kind, title, prompt, and scheduledAt");
78
122
  const prompt = value["prompt"];
79
123
  if (typeof prompt !== "string" || prompt.length === 0 || prompt.trim() !== prompt) throw new ScheduleLogError("at prompt must be non-empty and already trimmed");
80
124
  return Object.freeze({
81
125
  id: decodeId(value["id"]),
82
126
  kind: "at",
127
+ ...title === void 0 ? {} : { title },
83
128
  prompt,
84
129
  scheduledAt: decodeInstant(value["scheduledAt"])
85
130
  });
86
131
  }
87
132
  /** Decode the exact v1 fixed-rate record shape. */
88
133
  function decodeEveryRecord(value) {
89
- if (!isRecord(value) || !hasExactKeys(value, [
134
+ /* v8 ignore next -- decodeLegacyScheduleRecord rejects a non-object before it dispatches here. */
135
+ if (!isRecord(value)) throw new ScheduleLogError("every schedule must be an object");
136
+ const title = decodeHistoricalRecordTitle(value, [
90
137
  "id",
91
138
  "kind",
139
+ "title",
92
140
  "prompt",
93
141
  "everySeconds",
94
142
  "scheduledAt"
95
- ])) throw new ScheduleLogError("every schedule must contain exactly id, kind, prompt, everySeconds, and scheduledAt");
143
+ ], "every schedule must contain exactly id, kind, title, prompt, everySeconds, and scheduledAt");
96
144
  const prompt = value["prompt"];
97
145
  if (typeof prompt !== "string" || prompt.length === 0 || prompt.trim() !== prompt) throw new ScheduleLogError("every prompt must be non-empty and already trimmed");
98
146
  const everySeconds = value["everySeconds"];
99
147
  const interval = typeof everySeconds === "number" ? everySeconds * 1e3 : NaN;
100
- if (!Number.isSafeInteger(everySeconds) || everySeconds < 300 || !Number.isSafeInteger(interval)) throw new ScheduleLogError(`everySeconds must be a safe integer of at least 300`);
148
+ if (!Number.isSafeInteger(everySeconds) || everySeconds < 60 || !Number.isSafeInteger(interval)) throw new ScheduleLogError(`everySeconds must be a safe integer of at least 60`);
101
149
  return Object.freeze({
102
150
  id: decodeId(value["id"]),
103
151
  kind: "every",
152
+ ...title === void 0 ? {} : { title },
104
153
  prompt,
105
154
  everySeconds,
106
155
  scheduledAt: decodeInstant(value["scheduledAt"])
107
156
  });
108
157
  }
109
- /** Decode one current durable record variant by its exact discriminator. */
110
- function decodeScheduleRecord(value) {
158
+ /**
159
+ * Decode only the variants admitted by historical version-1 Session events.
160
+ *
161
+ * A record written before titles existed decodes without that member, so the
162
+ * fold keeps reading a log that the current creation path could not write.
163
+ * @param value - Untrusted durable JSON record from a Session event.
164
+ * @returns Detached frozen record, without a title when the event omitted it.
165
+ */
166
+ function decodeLegacyScheduleRecord(value) {
111
167
  if (!isRecord(value)) throw new ScheduleLogError("schedule record must be an object");
112
168
  switch (value["kind"]) {
113
169
  case "after": return decodeAfterRecord(value);
@@ -134,7 +190,7 @@ function decodeScheduleChange(value) {
134
190
  return Object.freeze({
135
191
  version: 1,
136
192
  operation: "create",
137
- schedule: decodeScheduleRecord(value["schedule"])
193
+ schedule: decodeLegacyScheduleRecord(value["schedule"])
138
194
  });
139
195
  case "delete":
140
196
  if (!hasExactKeys(value, [
@@ -212,9 +268,8 @@ function dispatchedRecord(record, change) {
212
268
  /**
213
269
  * Apply already-decoded Schedule changes to one complete fold value.
214
270
  *
215
- * This is the single transition authority shared by full-log replay and the
216
- * incremental Session projection. One mutable Map/Set pair spans the whole
217
- * batch; the returned arrays are materialized and frozen once.
271
+ * The transition authority for full-log replay. One mutable Map/Set pair spans
272
+ * the whole batch; the returned arrays are materialized and frozen once.
218
273
  * @param folded - complete active records and used-id history before the changes.
219
274
  * @param changes - strictly decoded durable mutations in log order.
220
275
  * @returns the complete fold value after every mutation.
@@ -285,7 +340,14 @@ function validate(events, fail) {
285
340
  fail(error.message);
286
341
  }
287
342
  }
288
- /** Install replay and pre-append validation for the owned event stream. */
343
+ /**
344
+ * Install replay and pre-append validation for the owned event stream.
345
+ *
346
+ * The durable task table is not asserted here: `scheduleDomain` validates every stored
347
+ * record through its table schema when the domain opens, and its only writer stores each
348
+ * task under `task.record.id`, so a check of the write payload would observe the same value
349
+ * the writer had just built.
350
+ */
289
351
  const install = Object.assign((ctx, fail) => {
290
352
  for (const session of ctx.sessions.list()) validate(session.ownEvents(), fail);
291
353
  ctx.on("session/created", (session) => {
@@ -0,0 +1,3 @@
1
+ /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
+
3
+ export declare const TYPERT: unknown