trekoon 0.3.6 → 0.3.8

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.
@@ -27,10 +27,23 @@ function normalizeArray(value) {
27
27
 
28
28
  /**
29
29
  * @param {{ id?: string }} record
30
- * @returns {string}
30
+ * @returns {string|null}
31
31
  */
32
32
  function getId(record) {
33
- return typeof record?.id === "string" && record.id.length > 0 ? record.id : crypto.randomUUID();
33
+ return typeof record?.id === "string" && record.id.length > 0 ? record.id : null;
34
+ }
35
+
36
+ function normalizeTimestamp(value, fallback) {
37
+ const normalized = Number(value);
38
+ return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
39
+ }
40
+
41
+ function normalizeText(value, fallback = "") {
42
+ return String(value ?? fallback).replace(/\\n/g, "\n");
43
+ }
44
+
45
+ function normalizeOwner(value) {
46
+ return typeof value === "string" ? value : null;
34
47
  }
35
48
 
36
49
  /**
@@ -55,19 +68,27 @@ export function normalizeSnapshot(rawSnapshot) {
55
68
  const rawTasks = normalizeArray(rawSnapshot?.tasks);
56
69
  const rawSubtasks = normalizeArray(rawSnapshot?.subtasks);
57
70
  const rawDependencies = normalizeArray(rawSnapshot?.dependencies);
71
+
58
72
  const taskIndex = new Map();
59
73
  const subtaskIndex = new Map();
60
74
 
61
- const tasks = rawTasks.map((task) => {
75
+ const tasks = rawTasks.flatMap((task) => {
76
+ const taskId = getId(task);
77
+ if (!taskId) {
78
+ return [];
79
+ }
80
+
81
+ const createdAt = normalizeTimestamp(task.createdAt, Date.now());
62
82
  const normalizedTask = {
63
- id: getId(task),
83
+ id: taskId,
64
84
  kind: "task",
65
85
  epicId: task.epicId ?? task.epic?.id ?? null,
66
- title: String(task.title ?? "Untitled task"),
67
- description: String(task.description ?? "").replace(/\\n/g, "\n"),
86
+ title: normalizeText(task.title, "Untitled task"),
87
+ description: normalizeText(task.description),
68
88
  status: normalizeStatus(task.status),
69
- createdAt: Number(task.createdAt ?? Date.now()),
70
- updatedAt: Number(task.updatedAt ?? task.createdAt ?? Date.now()),
89
+ owner: normalizeOwner(task.owner),
90
+ createdAt,
91
+ updatedAt: normalizeTimestamp(task.updatedAt, createdAt),
71
92
  blockedBy: [],
72
93
  blocks: [],
73
94
  dependencyIds: [],
@@ -77,19 +98,26 @@ export function normalizeSnapshot(rawSnapshot) {
77
98
  };
78
99
 
79
100
  taskIndex.set(normalizedTask.id, normalizedTask);
80
- return normalizedTask;
101
+ return [normalizedTask];
81
102
  });
82
103
 
83
- const subtasks = rawSubtasks.map((subtask) => {
104
+ const subtasks = rawSubtasks.flatMap((subtask) => {
105
+ const subtaskId = getId(subtask);
106
+ if (!subtaskId) {
107
+ return [];
108
+ }
109
+
110
+ const createdAt = normalizeTimestamp(subtask.createdAt, Date.now());
84
111
  const normalizedSubtask = {
85
- id: getId(subtask),
112
+ id: subtaskId,
86
113
  kind: "subtask",
87
114
  taskId: subtask.taskId ?? subtask.task?.id ?? null,
88
- title: String(subtask.title ?? "Untitled subtask"),
89
- description: String(subtask.description ?? "").replace(/\\n/g, "\n"),
115
+ title: normalizeText(subtask.title, "Untitled subtask"),
116
+ description: normalizeText(subtask.description),
90
117
  status: normalizeStatus(subtask.status),
91
- createdAt: Number(subtask.createdAt ?? Date.now()),
92
- updatedAt: Number(subtask.updatedAt ?? subtask.createdAt ?? Date.now()),
118
+ owner: normalizeOwner(subtask.owner),
119
+ createdAt,
120
+ updatedAt: normalizeTimestamp(subtask.updatedAt, createdAt),
93
121
  blockedBy: [],
94
122
  blocks: [],
95
123
  dependencyIds: [],
@@ -98,7 +126,7 @@ export function normalizeSnapshot(rawSnapshot) {
98
126
  };
99
127
 
100
128
  subtaskIndex.set(normalizedSubtask.id, normalizedSubtask);
101
- return normalizedSubtask;
129
+ return [normalizedSubtask];
102
130
  });
103
131
 
104
132
  for (const subtask of subtasks) {
@@ -108,13 +136,22 @@ export function normalizeSnapshot(rawSnapshot) {
108
136
  }
109
137
  }
110
138
 
111
- const dependencies = rawDependencies.map((dependency) => ({
112
- id: getId(dependency),
113
- sourceId: String(dependency.sourceId ?? ""),
114
- sourceKind: dependency.sourceKind === "subtask" ? "subtask" : "task",
115
- dependsOnId: String(dependency.dependsOnId ?? ""),
116
- dependsOnKind: dependency.dependsOnKind === "subtask" ? "subtask" : "task",
117
- }));
139
+ const dependencies = rawDependencies.flatMap((dependency) => {
140
+ const dependencyId = getId(dependency);
141
+ const sourceId = typeof dependency?.sourceId === "string" && dependency.sourceId.length > 0 ? dependency.sourceId : null;
142
+ const dependsOnId = typeof dependency?.dependsOnId === "string" && dependency.dependsOnId.length > 0 ? dependency.dependsOnId : null;
143
+ if (!dependencyId || !sourceId || !dependsOnId) {
144
+ return [];
145
+ }
146
+
147
+ return [{
148
+ id: dependencyId,
149
+ sourceId,
150
+ sourceKind: dependency.sourceKind === "subtask" ? "subtask" : "task",
151
+ dependsOnId,
152
+ dependsOnKind: dependency.dependsOnKind === "subtask" ? "subtask" : "task",
153
+ }];
154
+ });
118
155
 
119
156
  const lookupNode = (kind, id) => {
120
157
  if (kind === "subtask") {
@@ -136,23 +173,28 @@ export function normalizeSnapshot(rawSnapshot) {
136
173
  }
137
174
  }
138
175
 
139
- const epics = rawEpics.map((epic) => {
176
+ const epics = rawEpics.flatMap((epic) => {
140
177
  const epicId = getId(epic);
178
+ if (!epicId) {
179
+ return [];
180
+ }
181
+
141
182
  const epicTasks = tasks.filter((task) => task.epicId === epicId);
183
+ const createdAt = normalizeTimestamp(epic.createdAt, Date.now());
142
184
  const normalizedEpic = {
143
185
  id: epicId,
144
186
  title: String(epic.title ?? "Untitled epic"),
145
- description: String(epic.description ?? "").replace(/\\n/g, "\n"),
187
+ description: normalizeText(epic.description),
146
188
  status: normalizeStatus(String(epic.status ?? "todo")),
147
- createdAt: Number(epic.createdAt ?? Date.now()),
148
- updatedAt: Number(epic.updatedAt ?? epic.createdAt ?? Date.now()),
189
+ createdAt,
190
+ updatedAt: normalizeTimestamp(epic.updatedAt, createdAt),
149
191
  taskIds: epicTasks.map((task) => task.id),
150
192
  counts: deriveCounts(epicTasks),
151
193
  searchText: "",
152
194
  };
153
195
 
154
196
  normalizedEpic.searchText = [normalizedEpic.title, normalizedEpic.description, ...epicTasks.map((task) => task.title)].join(" ").toLowerCase();
155
- return normalizedEpic;
197
+ return [normalizedEpic];
156
198
  });
157
199
 
158
200
  for (const subtask of subtasks) {
@@ -195,6 +237,51 @@ export function normalizeSnapshot(rawSnapshot) {
195
237
  };
196
238
  }
197
239
 
240
+ function mergeRecordsById(existingRecords, incomingRecords, deletedIds = []) {
241
+ const deletedIdSet = new Set(deletedIds);
242
+ const nextRecords = existingRecords.filter((record) => !deletedIdSet.has(record.id));
243
+ const indexById = new Map(nextRecords.map((record, index) => [record.id, index]));
244
+
245
+ for (const record of incomingRecords) {
246
+ if (typeof record?.id !== "string" || record.id.length === 0) {
247
+ continue;
248
+ }
249
+
250
+ const existingIndex = indexById.get(record.id);
251
+ if (existingIndex === undefined) {
252
+ indexById.set(record.id, nextRecords.length);
253
+ nextRecords.push(record);
254
+ continue;
255
+ }
256
+
257
+ nextRecords[existingIndex] = record;
258
+ }
259
+
260
+ return nextRecords;
261
+ }
262
+
263
+ export function applySnapshotDelta(snapshot, delta) {
264
+ const baseSnapshot = snapshot && typeof snapshot === "object"
265
+ ? snapshot
266
+ : { generatedAt: null, epics: [], tasks: [], subtasks: [], dependencies: [] };
267
+
268
+ if (!delta || typeof delta !== "object") {
269
+ return baseSnapshot;
270
+ }
271
+
272
+ return {
273
+ generatedAt: delta.generatedAt ?? baseSnapshot.generatedAt ?? null,
274
+ epics: mergeRecordsById(baseSnapshot.epics ?? [], normalizeArray(delta.epics), normalizeArray(delta.deletedEpicIds)),
275
+ tasks: mergeRecordsById(baseSnapshot.tasks ?? [], normalizeArray(delta.tasks), normalizeArray(delta.deletedTaskIds)),
276
+ subtasks: mergeRecordsById(baseSnapshot.subtasks ?? [], normalizeArray(delta.subtasks), normalizeArray(delta.deletedSubtaskIds)),
277
+ dependencies: mergeRecordsById(
278
+ baseSnapshot.dependencies ?? [],
279
+ normalizeArray(delta.dependencies),
280
+ normalizeArray(delta.deletedDependencyIds),
281
+ ),
282
+ };
283
+ }
284
+
198
285
  const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
199
286
 
200
287
  /**
@@ -204,8 +291,14 @@ const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium",
204
291
  * @returns {string}
205
292
  */
206
293
  export function formatDate(timestamp) {
207
- if (!timestamp) return "Unknown";
208
- return dateFormatter.format(timestamp);
294
+ const normalized = Number(timestamp);
295
+ if (!Number.isFinite(normalized) || normalized <= 0) return "Unknown";
296
+
297
+ try {
298
+ return dateFormatter.format(normalized);
299
+ } catch {
300
+ return "Unknown";
301
+ }
209
302
  }
210
303
 
211
304
  /**