1/** 2 * TodoStore - Single source of truth for Magic Todo 3 * 4 * All todo data lives here. The global `todos` array is a proxy to this store. 5 * All mutations go through dispatch(), which manages history and notifies subscribers. 6 * 7 * In-memory indexes (_idIndex, _childIndex) provide O(1) lookups by id and 8 * by parentId. These are maintained automatically by dispatch(). 9 */ 10 11const TodoStore = (function() { 12 // ========================================================================= 13 // PRIVATE STATE 14 // ========================================================================= 15 16 var _state = { 17 todos: [], // Array of Task objects (order matters for display) 18 tabs: [], // Array of { id, name, order, timestamp } 19 activeTabId: null, // id of currently-viewed tab (device-local; not synced) 20 history: [], // Array of snapshots for undo 21 forwardHistory: [], // Array of snapshots for redo 22 timestamp: 0, // Last modification timestamp (for sync) 23 lastModified: null, 24 isLoading: false, 25 activeOperations: new Map() 26 }; 27 28 // In-memory indexes for O(1) lookups 29 var _idIndex = new Map(); // id -> Task 30 var _childIndex = new Map(); // parentId -> [Task] (uses _ROOT_KEY for null/undefined) 31 32 var _ROOT_KEY = '__root__'; // sentinel key for top-level items in _childIndex 33 34 // Subscribers 35 var _listeners = []; 36 37 // Maximum history entries 38 var MAX_HISTORY = 3; 39 40 // ========================================================================= 41 // INDEX HELPERS 42 // ========================================================================= 43 44 /** 45 * Normalize a parentId for use as a _childIndex key. 46 * null and undefined both map to _ROOT_KEY. 47 */ 48 function _parentKey(parentId) { 49 return (parentId == null) ? _ROOT_KEY : parentId; 50 } 51 52 /** 53 * Coerce legacy numeric ids to strings, in place. 54 * 55 * Accounts from before the switch to crypto.randomUUID() stored ids as 56 * NUMBERS. The wire format, the server DTO, and these in-memory indexes 57 * (Maps, whose keys compare by strict identity) are all string-typed, so a 58 * numeric id anywhere breaks sync (a 400 on push) and makes cross-type
59 * lookups silently miss. Every id-like field is coerced with String() so 60 * links survive: the same number always maps to the same string, so a 61 * child's parentId still matches its parent's id and a todo's tabId still 62 * matches its tab. Already-string ids are left untouched. 63 */ 64 function _normalizeLegacyIds(todos, tabs) { 65 if (tabs) { 66 for (var i = 0; i < tabs.length; i++) { 67 if (typeof tabs[i].id === 'number') tabs[i].id = String(tabs[i].id); 68 } 69 } 70 if (todos) { 71 for (var j = 0; j < todos.length; j++) { 72 var t = todos[j]; 73 if (typeof t.id === 'number') t.id = String(t.id); 74 if (typeof t.parentId === 'number') t.parentId = String(t.parentId); 75 if (typeof t.tabId === 'number') t.tabId = String(t.tabId); 76 } 77 } 78 } 79 80 /** 81 * Rebuild both indexes from scratch based on _state.todos. 82 */ 83 function _rebuildIndexes() { 84 _idIndex.clear(); 85 _childIndex.clear(); 86 87 for (var i = 0; i < _state.todos.length; i++) { 88 var todo = _state.todos[i]; 89 _idIndex.set(todo.id, todo); 90 _indexChild(todo); 91 } 92 } 93 94 /** 95 * Add a single todo to the child index. 96 */ 97 function _indexChild(todo) { 98 var key = _parentKey(todo.parentId); 99 var children = _childIndex.get(key); 100 if (!children) { 101 children = []; 102 _childIndex.set(key, children); 103 } 104 children.push(todo); 105 } 106 107 /** 108 * Remove a single todo from the child index. 109 */ 110 function _unindexChild(todo) { 111 var key = _parentKey(todo.parentId); 112 var children = _childIndex.get(key); 113 if (children) { 114 var idx = children.indexOf(todo); 115 if (idx !== -1) { 116 children.splice(idx, 1); 117 } 118 if (children.length === 0) { 119 _childIndex.delete(key); 120 } 121 } 122 } 123 124 // ========================================================================= 125 // SUBSCRIBERS 126 // ========================================================================= 127 128 /** 129 * Subscribe to state changes 130 */ 131 function subscribe(callback) { 132 _listeners.push(callback); 133 return function unsubscribe() { 134 var index = _listeners.indexOf(callback); 135 if (index > -1) { 136 _listeners.splice(index, 1); 137 } 138 }; 139 } 140 141 /** 142 * Notify all subscribers of state change 143 */ 144 function _notify(action, payload) { 145 _listeners.forEach(function(callback) { 146 try { 147 callback(_state, action, payload); 148 } catch (e) { 149 console.error('TodoStore subscriber error:', e); 150 } 151 }); 152 } 153 154 // ========================================================================= 155 // HISTORY 156 // ========================================================================= 157 158 /** 159 * Clone a todo for history (minimal representation) 160 */ 161 function _cloneTodo(todo) { 162 return { 163 id: todo.id, 164 parentId: todo.parentId, 165 text: todo.text, 166 isCompleted: todo.isCompleted, 167 category: todo.category, 168 estimate: todo.estimate, 169 timestamp: todo.timestamp, 170 tabId: todo.tabId, 171 deadline: todo.deadline, 172 elapsed: todo.elapsed 173 }; 174 } 175 176 function _cloneTab(tab) { 177 return { id: tab.id, name: tab.name, order: tab.order, timestamp: tab.timestamp }; 178 } 179 180 function _buildSnapshot() { 181 return { 182 todos: _state.todos.map(_cloneTodo), 183 tabs: _state.tabs.map(_cloneTab), 184 activeTabId: _state.activeTabId 185 }; 186 } 187 188 function _restoreSnapshot(snapshot) { 189 // Back-compat: old history entries (pre-tabs) were plain arrays of todo clones. 190 var todoClones = Array.isArray(snapshot) ? snapshot : (snapsh
190ot.todos || []); 191 _state.todos = todoClones.map(function(obj) { 192 if (typeof Task !== 'undefined') return Task.fromObject(obj); 193 return obj; 194 }); 195 if (!Array.isArray(snapshot)) { 196 if (Array.isArray(snapshot.tabs)) _state.tabs = snapshot.tabs.map(_cloneTab); 197 if (snapshot.activeTabId !== undefined) _state.activeTabId = snapshot.activeTabId; 198 } 199 _state.timestamp = Date.now(); 200 _state.lastModified = Date.now(); 201 _rebuildIndexes(); 202 } 203 204 /** 205 * Push current state to history (for undo) 206 */ 207 function _pushHistory() { 208 // Skip during initial load 209 if (window.initialLoadInProgress) return; 210 211 var snapshot = _buildSnapshot(); 212 213 // Avoid duplicate history entries 214 if (_state.history.length > 0) { 215 var lastSnapshot = _state.history[_state.history.length - 1]; 216 if (JSON.stringify(lastSnapshot) === JSON.stringify(snapshot)) { 217 return; 218 } 219 } 220 221 _state.history.push(snapshot); 222 223 // Limit history size 224 if (_state.history.length > MAX_HISTORY) { 225 _state.history.shift(); 226 } 227 228 // Clear forward history on new action 229 _state.forwardHistory = []; 230 231 // History rides along in the IDB write on every save() (via 232 // TodoDB.saveAll's meta store). No standalone LS write. 233 } 234 235 // ========================================================================= 236 // LOOKUP HELPERS 237 // ========================================================================= 238 239 /** 240 * Find a todo by ID - O(1) via index 241 */ 242 function _findTodo(todoId) { 243 // Try direct match first (same type) 244 var todo = _idIndex.get(todoId); 245 if (todo) return todo; 246 247 // Loose equality fallback: numeric string "123" vs number 123 248 // This maintains backward compatibility with code that mixes types 249 if (typeof todoId === 'string') { 250 var asNum = Number(todoId); 251 if (!isNaN(asNum)) return _idIndex.get(asNum); 252 } else if (typeof todoId === 'number') { 253 return _idIndex.get(String(todoId)); 254 } 255 256 return undefined; 257 } 258 259 /** 260 * Find index of a todo by ID (needed for splice-based mutations) 261 */ 262 function _findTodoIndex(todoId) { 263 return _state.todos.findIndex(function(t) { 264 return t.id == todoId || t.Id == todoId; 265 }); 266 } 267 268 // ========================================================================= 269 // DISPATCH 270 // ========================================================================= 271 272 /** 273 * Dispatch an action to mutate state 274 */ 275 function dispatch(action, payload) { 276 if (payload === undefined) payload = {}; 277 var todoId = payload.todoId; 278 var todo = payload.todo; 279 var skipHistory = payload.skipHistory; 280 var result = null; 281 282 switch (action) { 283 case 'INIT': 284 // Initialize with todos (from localStorage). Coerce any legacy 285 // numeric ids to strings up front so the store, the id indexes, 286 // and everything that syncs from here are string-consistent. 287 _normalizeLegacyIds(payload.todos, payload.tabs); 288 _state.todos = payload.todos || []; 289 _state.tabs = payload.tabs || []; 290 _state.activeTabId = payload.activeTabId ?? null; 291 _state.timestamp = payload.timestamp || Date.now(); 292 _state.history = payload.history || []; 293 _state.forwardHistory = []; 294 _state.lastModified = Date.now(); 295 _rebuildIndexes(); 296 break; 297 298 case 'ADD_TODO': 299 if (!skipHistory) _pushHistory(); 300 if (payload.prepend) { 301 _state.todos.unshift(todo); 302 } else { 303 _state.todos.push(todo); 304 } 305 _idIndex.set(todo.id, todo); 306 if (payload.prepend) { 307 // `_indexChild` appends to the end of the parent's child 308 // list, which contradicts an unshift into the flat array. 309 // Left disagreeing, `normaliseTodoOrder`'s child-index DFS 310 // decides the array is wrong and moves the just-added task 311 // to the BOTTOM â then persists and (for Pro) uploads that. 312 // Rebuilding keeps both views on the array's own order. 313 _rebuildIndexes(); 314 } else { 315 _indexChild(todo); 316 } 317 _state.timestamp = Date.now(); 318 _state.lastModified = Date.now(); 319 result = todo; 320 break; 321 322 case 'UPDATE_TODO': 323 if (!skipHistory) _pushHistory(); 324 var updateTarget = _findTodo(todoId); 325 if (updateTarget) { 326 // Track old parentId in case it changes (need to re-index) 327 var oldParentId = updateTarget.parentId; 328 329 // Merge updates - handle both camelCase and PascalCase 330 if (payload.updates.Text !== undefined) updateTarget.text = payload.updates.Text; 331 if (payload.updates.text !== undefined) updateTarget.text = payload.updates.text; 332 if (payload.updates.IsCompleted !== undefined) updateTarget.isCompleted = payload.updates.IsCompleted; 333 if (payload.updates.isCompleted !== undefined) updateTarget.isCompleted = payload.updates.isCompleted; 334 if (payload.updates.Category !== undefined) updateTarget.category = payload.updates.Category; 335 if (payload.updates.category !== undefined) updateTarget.category = payload.updates.category; 336 if (payload.updates.Estimate !== undefined) updateTarget.estimate = payload.updates.Estimate; 337 if (payload.updates.estimate !== undefined) updateTarget.estimate = payload.updates.estimate; 338 if (payload.updates.Deadline !== undefined) updateTarget.deadline = payload.updates.Deadline || undefined; 339 if (payload.updates.deadline !== undefined) updateTarget.deadline = payload.updates.deadline || undefined; 340 if (payload.updates.Elapsed !== undefined) updateTarget.elapsed
340= payload.updates.Elapsed || undefined; 341 if (payload.updates.elapsed !== undefined) updateTarget.elapsed = payload.updates.elapsed || undefined; 342 if (payload.updates.ParentId !== undefined) updateTarget.parentId = payload.updates.ParentId; 343 if (payload.updates.parentId !== undefined) updateTarget.parentId = payload.updates.parentId; 344 // tabId is which tab the item belongs to. The sync layer sets 345 // it so an item always carries the tab it actually lives on 346 // (its parent's/root's tab), keeping the flat tabId consistent 347 // with the hierarchy â divergence here orphans subtasks. 348 if (payload.updates.tabId !== undefined) updateTarget.tabId = payload.updates.tabId; 349 350 // Re-index if parentId changed 351 if (updateTarget.parentId !== oldParentId) { 352 // Remove from old parent's children list 353 var oldKey = _parentKey(oldParentId); 354 var oldChildren = _childIndex.get(oldKey); 355 if (oldChildren) { 356 var oldIdx = oldChildren.indexOf(updateTarget); 357 if (oldIdx !== -1) oldChildren.splice(oldIdx, 1); 358 if (oldChildren.length === 0) _childIndex.delete(oldKey); 359 } 360 // Add to new parent's children list 361 _indexChild(updateTarget); 362 } 363 364 // Normally an edit stamps "now". But a sync-applied update 365 // passes the SERVER's origin timestamp so the adopted item 366 // keeps its true last-writer time â otherwise it would look 367 // freshly-edited and the sync layer's last-writer-wins could 368 // let a merely-adopted value out-rank a genuinely newer edit. 369 updateTarget.timestamp = (payload.updates.timestamp !== undefined) 370 ? payload.updates.timestamp 371 : Date.now(); 372 _state.timestamp = Date.now(); 373 _state.lastModified = Date.now(); 374 result = updateTarget; 375 } 376 break; 377 378 case 'DELETE_TODO': 379 if (!skipHistory) _pushHistory(); 380 var deleteIndex = _findTodoIndex(todoId); 381 if (deleteIndex !== -1) { 382 result = _state.todos.splice(deleteIndex, 1)[0]; 383 _idIndex.delete(result.id); 384 _unindexChild(result); 385 _state.timestamp = Date.now(); 386 _state.lastModified = Date.now(); 387 } 388 break; 389 390 case 'DELETE_CHILDREN': 391 if (!skipHistory) _pushHistory(); 392 // Find all children recursively 393 var toDelete = new Set(); 394 var findChildren = function(parentId) { 395 var children = _childIndex.get(_parentKey(parentId)) || []; 396 for (var i = 0; i < children.length; i++) { 397 var child = children[i]; 398 if (!toDelete.has(child.id)) { 399 toDelete.add(child.id); 400 findChildren(child.id); 401 } 402 } 403 }; 404 findChildren(todoId); 405 406 // Remove from array and indexes 407 _state.todos = _state.todos.filter(function(t) { 408 if (toDelete.has(t.id)) { 409 _idIndex.delete(t.id); 410 _unindexChild(t); 411 return false; 412 } 413 return true; 414 }); 415 _state.timestamp = Date.now(); 416 _state.lastModified = Date.now(); 417 result = Array.from(toDelete); 418 break; 419 420 case 'REPLACE_ALL': 421 // Used for sync, imports, bulk clear, etc. 422 if (!skipHistory) _pushHistory(); 423 _state.todos = payload.todos || []; 424 if (payload.tabs !== undefined) { 425 _state.tabs = payload.tabs || []; 426 } 427 if (payload.activeTabId !== undefined) { 428 _state.activeTabId = payload.activeTabId; 429 } 430 // When restoring from IndexedDB (or any full-state load) we 431 // want the undo stack that shipped alongside the snapshot, 432 // not the empty one the hydrating tab started with. 433 if (Array.isArray(payload.history)) { 434 _state.history = payload.history;
435 _state.forwardHistory = []; 436 } 437 if (payload.timestamp) { 438 _state.timestamp = payload.timestamp; 439 } else { 440 _state.timestamp = Date.now(); 441 } 442 _state.lastModified = Date.now(); 443 _rebuildIndexes(); 444 break; 445 446 case 'CLEAR_ALL': 447 if (!skipHistory) _pushHistory(); 448 _state.todos = []; 449 _state.timestamp = Date.now(); 450 _state.lastModified = Date.now(); 451 _rebuildIndexes(); 452 break; 453 454 case 'REORDER': 455 // Reorder todos based on new order. 456 // 457 // Semantics: "use this ordering for the items in 458 // payload.todos; preserve every other todo currently in 459 // state, in its existing relative order, appended 460 // afterwards." This matters because some callers only 461 // know about a slice of the store â drag-drop's 462 // `updateTodoOrder` walks the DOM, which only contains 463 // the active tab's items. Without preservation the 464 // assignment below would silently delete every 465 // non-rendered todo (other tabs, collapsed subtrees) 466 // and the next save would propagate the wipe to IDB. 467 // History still has them, so undo recovers, but the 468 // user sees their other tabs go empty until they undo. 469 if (!skipHistory) _pushHistory(); 470 var passedTodos = payload.todos || []; 471 var passedIds = new Set(); 472 for (var pti = 0; pti < passedTodos.length; pti++) { 473 var pt = passedTodos[pti]; 474 if (pt && pt.id !== undefined && pt.id !== null) { 475 passedIds.add(String(pt.id)); 476 } 477 } 478 var preserved = []; 479 for (var psi = 0; psi < _state.todos.length; psi++) { 480 var existing = _state.todos[psi]; 481 if (existing && !passedIds.has(String(existing.id))) { 482 preserved.push(existing); 483 } 484 } 485 if (preserved.length > 0 && window.todoDebugMode) { 486 console.log('TodoStore REORDER: ' + passedTodos.length 487 + ' explicit, ' + preserved.length + ' preserved (not in payload)'); 488 } 489 _state.todos = passedTodos.concat(preserved); 490 _state.timestamp = Date.now(); 491 _state.lastModified = Date.now(); 492 // Rebuild child index (order within children may have changed) 493 _rebuildIndexes(); 494 break; 495 496 case 'ADD_TAB': 497 if (!skipHistory) _pushHistory(); 498 if (payload.tab) { 499 _state.tabs.push(payload.tab); 500 _state.timestamp = Date.now(); 501 _state.lastModified = Date.now(); 502 result = payload.tab; 503 } 504 break; 505 506 case 'UPDATE_TAB': 507 if (!skipHistory) _pushHistory(); 508 var tabTarget = _state.tabs.find(function(t) { return t.id === payload.tabId; }); 509 if (tabTarget && payload.updates) { 510 if (payload.updates.name !== undefined) tabTarget.name = payload.updates.name; 511 if (payload.updates.order !== undefined) tabTarget.order = payload.updates.order; 512 // An explicit timestamp means the caller is ADOPTING a value 513 // written elsewhere (the sync layer applying the server's 514 // copy) and knows when it was actually authored. Stamping 515 // Date.now() over it destroyed the recency evidence that a 516 // later last-writer-wins comparison needs, and let a merely 517 // adopted value out-rank a genuinely newer edit. 518 tabTarget.timestamp = (payload.updates.timestamp !== undefined) 519 ? payload.updates.timestamp 520 : Date.now(); 521 _state.timestamp = Date.now(); 522 _state.lastModified = Date.now(); 523 result = tabTarget; 524 } 525 break; 526
527 case 'DELETE_TAB': 528 if (!skipHistory) _pushHistory(); 529 var delTabId = payload.tabId; 530 var remainingTabs = _state.tabs.filter(function(t) { return t.id !== delTabId; }); 531 if (remainingTabs.length !== _state.tabs.length) { 532 _state.tabs = remainingTabs; 533 // Cascade-delete all todos whose tabId matches 534 _state.todos = _state.todos.filter(function(t) { return t.tabId !== delTabId; }); 535 // Reassign activeTabId if it was the deleted tab 536 if (_state.activeTabId === delTabId) { 537 var fallback = _state.tabs.slice().sort(function(a, b) { return a.order - b.order; })[0]; 538 _state.activeTabId = fallback ? fallback.id : null; 539 } 540 _state.timestamp = Date.now(); 541 _state.lastModified = Date.now(); 542 _rebuildIndexes(); 543 result = delTabId; 544 } 545 break; 546 547 case 'SET_ACTIVE_TAB': 548 _state.activeTabId = payload.tabId ?? null; 549 _state.lastModified = Date.now(); 550 result = _state.activeTabId; 551 break; 552 553 case 'UNDO': 554 if (_state.history.length > 0) { 555 // Save current state to forward history 556 _state.forwardHistory.push(_buildSnapshot()); 557 var previousState = _state.history.pop(); 558 _restoreSnapshot(previousState); 559 result = true; 560 // History is persisted to IDB by the caller's save(). 561 } 562 break; 563 564 case 'REDO': 565 if (_state.forwardHistory.length > 0) { 566 // Save current state to history 567 _state.history.push(_buildSnapshot()); 568 var forwardState = _state.forwardHistory.pop(); 569 _restoreSnapshot(forwardState); 570 result = true; 571 // History is persisted to IDB by the caller's save(). 572 } 573 break; 574 575 case 'SET_LOADING': 576 _state.isLoading = payload.isLoading; 577 break; 578 579 case 'SET_OPERATION': 580 if (!_state.activeOperations.has(todoId)) { 581 _state.activeOperations.set(todoId, new Set()); 582 } 583 if (payload.active) { 584 _state.activeOperations.get(todoId).add(payload.operationType); 585 } else { 586 _state.activeOperations.get(todoId).delete(payload.operationType); 587 if (_state.activeOperations.get(todoId).size === 0) { 588 _state.activeOperations.delete(todoId); 589 } 590 } 591 break; 592 593 default: 594 console.warn('TodoStore: Unknown action', action); 595 return null; 596 } 597 598 _notify(action, payload); 599 return result; 600 } 601 602 // ========================================================================= 603 // GETTERS 604 // ========================================================================= 605 606 /** 607 * Get current state 608 */ 609 function getState() { 610 return { 611 todos: _state.todos, 612 tabs: _state.tabs, 613 activeTabId: _state.activeTabId, 614 history: _state.history, 615 forwardHistory: _state.forwardHistory, 616 timestamp: _state.timestamp, 617 lastModified: _state.lastModified, 618 isLoading: _state.isLoading, 619 activeOperations: _state.activeOperations 620 }; 621 } 622 623 function getTabs() { 624 return _state.tabs; 625 } 626 627 function getActiveTabId() { 628 return _state.activeTabId; 629 } 630 631 /** 632 * Get all todos (direct reference) 633 */ 634 function getTodos() { 635 return _state.todos; 636 } 637 638 /** 639 * Get timestamp 640 */ 641 function getTimestamp() { 642 return _state.timestamp; 643 } 644 645 /** 646 * Set timestamp (for sync) 647 */ 648 function setTimestamp(ts) { 649 _state.timestamp = ts; 650 } 651 652 /** 653 * Get a single todo by ID - O(1) 654 */ 655 function getTodo(todoId) { 656 return _findTodo(todoId); 657 } 658 659 /** 660 * Get children of a todo - O(1) via child index 661 */ 662 function getChildren(parentId) { 663 var key = _parentKey(parentId); 664 var children = _childIndex.get(key); 665 return children ? children.slice() : []; // Return a copy to prevent external mutation 666 } 667 668 /** 669 * Get all descendants of a todo 670 */ 671 function getDescendants(parentId) { 672 var descendants = []; 673 var queue = [parentId]; 674 675 while (queue.length > 0) { 676 var currentId = queue.shift(); 677 var children = getChildren(currentId); 678 for (var i = 0; i < children.length; i++) { 679 descendants.push(children[i]); 680 queue.push(children[i].id); 681 } 682 } 683 684 return descendants; 685 } 686 687 /** 688 * Get ancestors of a todo 689 */ 690 function getAncestors(todoId) { 691 var ancestors = []; 692 var current = _findTodo(todoId); 693 694 while (current) { 695 var parentId = current.parentId ?? current.ParentId; 696 if (!parentId) break; 697 698 var parent = _findTodo(parentId); 699 if (parent) { 700 ancestors.push(parent); 701 current = parent; 702 } else { 703 break; 704 } 705 } 706 707 return ancestors; 708 } 709 710 function hasActiveOperation(todoId, operationType) { 711 var ops = _state.activeOperations.get(todoId); 712 return ops && ops.has(operationType); 713 } 714 715 function hasAnyActiveOperation(operationType) { 716 for (var ops of _state.activeOperations.values()) { 717 if (ops.has(operationType)) return true; 718 } 719 return false; 720 } 721 722 function canUndo() { 723 return _state.history.length > 0;
724 } 725 726 function canRedo() { 727 return _state.forwardHistory.length > 0; 728 } 729 730 // Public API 731 return { 732 subscribe: subscribe, 733 dispatch: dispatch, 734 getState: getState, 735 getTodos: getTodos, 736 getTodo: getTodo, 737 getTabs: getTabs, 738 getActiveTabId: getActiveTabId, 739 getTimestamp: getTimestamp, 740 setTimestamp: setTimestamp, 741 getChildren: getChildren, 742 getDescendants: getDescendants, 743 getAncestors: getAncestors, 744 hasActiveOperation: hasActiveOperation, 745 hasAnyActiveOperation: hasAnyActiveOperation, 746 canUndo: canUndo, 747 canRedo: canRedo 748 }; 749})(); 750 751// Export for use in other modules 752if (typeof window !== 'undefined') { 753 window.TodoStore = TodoStore; 754}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.