PageSourceSearch

https://goblin.tools/js/todo-store.js?v=BbqWMxkPuTHTEfOEW4JIF228N-WFNG5onxl8w1n25sI

js goblin.tools collected 2026-10-01 11:37:01 UTC 29,919 bytes, 754 lines download raw bytes

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.