notes
All repositories: gitoria
5.7 KB
// lib/notes.hl — THE NOTES (ticket notes#1) and their SYNC (ticket notes#2). One hl:mpackdb table, storage/mpackdb/notes.db:// pk @id index owner { owner (users @id), text (Markdown), created, edited, gone, local } times = ms// gone = ms a note was deleted (0 = alive; the row stays as a TOMBSTONE so other devices learn of the delete);// local = the id the creating device gave the note before it had a server id (makes a retried push idempotent)// The SUBJECT of a note is not stored: it is the first non-empty line of the text, without leading `#`// (CONCEPT.md "Decisions"); the browser (notes-offline.js) derives it and orders the sidebar by `edited`.// Every function takes the owner's user id and only ever reaches that user's notes. Every write to the table is here.// The browser keeps its own copy (IndexedDB) and pushes/pulls through /api/notes (lib/api.hl). A note's VERSION is its// `edited` (ms); the browser remembers the `edited` it last saw of a note (`base`).// Looked up in lib/notes-helpers.hl: textError (size limit), isAlive (tombstones), stampOf (version stamps), copyMark.import { MPackDB } from 'hl:mpackdb'import { now } from 'hl:time'import { storageDir, countOfList } from './util.hl'import { textError, isAlive, stampOf, copyMark } from './notes-helpers.hl'static notesTable = new MPackDB(file = storageDir + '/notes.db', primaryKey = '@id', indexes = ['owner'])// one note of this owner, or nullstatic noteOf = (userId, id) => {if (id == null || hlTypeName(id) != 'String' || id == '') { return null }n = notesTable.fetch(id)if (n == null || n.owner != userId || !isAlive(n)) { return null }return n}// what the browser needs to compare: id, edited, gone (no text) of every note of the owner, tombstones includedstatic metaRows = (userId) => {found = notesTable.find('owner', userId)out = []let i = 0n = countOfList(found)while (i < n) {out.push({ id = found[i].id edited = found[i].edited gone = isAlive(found[i]) ? 0 : found[i].gone })i = i + 1}return out}// one note with its text, or nullstatic noteView = (userId, id) => {n = noteOf(userId, id)return n == null ? null : { id = n.id text = n.text edited = n.edited created = n.created }}static newNote = (userId, text, local, edited) => {t = now()id = notesTable.put({ owner = userId text = text created = t edited = stampOf(edited, 0) gone = 0 local = local })return id}// a note pushed by a browser: { id (server id or ''), local (the browser's own id of a never-synced note), text,// base (the `edited` the browser last saw, 0 = never), at (when the browser last edited it) }// → { id, edited, text, copy? }: `text` is what the note is NOW (the browser takes it); `copy` = { id, edited, text }// is the version that lost a conflict, kept as a note of its own — nothing is ever lost.static pushNote = (userId, id, local, text, base, at) => {bad = textError(text)if (bad != null) { return { error = bad } }let n = nullif (id != '') { n = noteOf(userId, id) }if (n == null && local != '') {// a retry after a lost answer: the note the same browser already mademine = notesTable.find('owner', userId)let i = 0c = countOfList(mine)while (i < c && n == null) {if (isAlive(mine[i]) && mine[i].local == local) { n = mine[i] }i = i + 1}}if (n == null) {// a new note (or one deleted elsewhere while this browser edited it: the edit becomes a note again)nid = newNote(userId, text, local, at > 0 ? at : now())if (nid == null) { return { error = 'could not store the note: ' + notesTable.lastError() } }made = notesTable.fetch(nid)return { id = nid edited = made.edited text = text }}if (n.text == text) { return { id = n.id edited = n.edited text = n.text } }if (n.edited == base) {ed = stampOf(at, n.edited)notesTable.update(n.id, { id = n.id owner = userId text = text created = n.created edited = ed gone = 0 local = n.local == null ? '' : n.local })after = notesTable.fetch(n.id)if (after == null || after.text != text) { return { error = 'could not store the note: ' + notesTable.lastError() } }return { id = n.id edited = after.edited text = text }}// CONFLICT: changed here AND there. The later edit wins (last write wins); the other version becomes a copy.serverText = n.textserverEdited = n.editedif (at > serverEdited) {cid = newNote(userId, serverText + copyMark, '', serverEdited)ed = stampOf(at, n.edited)notesTable.update(n.id, { id = n.id owner = userId text = text created = n.created edited = ed gone = 0 local = n.local == null ? '' : n.local })after = notesTable.fetch(n.id)cp = cid == null ? null : notesTable.fetch(cid)return { id = n.id edited = after.edited text = text copy = cp == null ? null : { id = cp.id edited = cp.edited text = cp.text } }}cid = newNote(userId, text + copyMark, '', at)cp = cid == null ? null : notesTable.fetch(cid)return { id = n.id edited = n.edited text = n.text copy = cp == null ? null : { id = cp.id edited = cp.edited text = cp.text } }}// a delete pushed by a browser. Deleted elsewhere already = fine. Changed elsewhere since the browser's `base`// = NOT deleted (the changed note is kept; the browser takes it back). Deleted = a tombstone (text '', gone = now).static removeNote = (userId, id, base) => {raw = id == null || hlTypeName(id) != 'String' || id == '' ? null : notesTable.fetch(id)if (raw == null || raw.owner != userId) { return { ok = true } }if (!isAlive(raw)) { return { ok = true } }if (raw.edited != base) { return { kept = true id = raw.id edited = raw.edited text = raw.text } }notesTable.update(id, { id = id owner = userId text = '' created = raw.created edited = raw.edited gone = now() local = raw.local == null ? '' : raw.local })return { ok = true }}
Branches
- mainmain branch
Latest commits
- 2149e902notes mission 002 (4/4): code order — README file map + same-output test, STATUS, LOG, report; tests/letcount.py, tests/realdata-baseline.mjs, tests/realdata-compare.pymre
- 47db4fc1notes mission 002 (3/4): code order — let only where reassigned (58 dropped; 34 left: 19 reassigned, 15 loop-bound); gates 50/0 + 18/0, live-data run = step 2mre
- 3f8cb383notes mission 002 (2/4): code order — topics, map, thin wrappers: lib/util.hl, lib/notes(-helpers).hl, lib/users.hl (+userOfLoginCode, tagOf), lib/api(-helpers).hl; project.hl = map; login routes take &req/&sessions (failed-login reason now kept); dead notes#1 functions removed; gates 50/0 + 18/0mre
- 8cda5412notes mission 002 (1/4): code order — files moved: lib/notes.hl, lib/users.hl, lib/jsoncheck.hl, components/styles.hl (imports only); gates 50/0 + 18/0, live-data run identicalmre
- 47dad68bnotes: Hybriel master 06617221 (plugin allocators 3a781359 + 413f60e4, mpackdb 2cb7ae5e, http1 773de63e); gates 50/0 + 18/0mre
- a4a2b2aeantcolony#40: tracker missions moved too — references to them in missions/reports/LOG.md updatedmre
- 3dea0ef2notes: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 50/0mre
- e27c7d71notes: Hybriel master 8efba065 (#126 GC by bytes, #48 lambda params copy; audit: nothing to fix; gate 50/0)mre
- 124613b6antcolony#40: mission references point to the moved missionsmre
- 1ca2f34dantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- 2bebebdanotes: Hybriel master ff51cf46 (re-vendor round)mre
- 3eff126dnotes#3: installable app (manifest + own icon/favicon; notes' own sw.js kept)mre
- 9883c540deploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- eee693b8deploy.sh: never send .git or .gitignore to Byrodinmre
- c8904061State of 2026-09-27, before the move to gitoriamre