gitoriaLog in with ident

notes

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit47db4fc147db4fc1notes 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 2mre47db4fc1/lib/notes.hl

5.7 KB

  1. // lib/notes.hl — THE NOTES (ticket notes#1) and their SYNC (ticket notes#2). One hl:mpackdb table, storage/mpackdb/notes.db:
  2. // pk @id index owner { owner (users @id), text (Markdown), created, edited, gone, local } times = ms
  3. // gone = ms a note was deleted (0 = alive; the row stays as a TOMBSTONE so other devices learn of the delete);
  4. // local = the id the creating device gave the note before it had a server id (makes a retried push idempotent)
  5. // The SUBJECT of a note is not stored: it is the first non-empty line of the text, without leading `#`
  6. // (CONCEPT.md "Decisions"); the browser (notes-offline.js) derives it and orders the sidebar by `edited`.
  7. // Every function takes the owner's user id and only ever reaches that user's notes. Every write to the table is here.
  8. // The browser keeps its own copy (IndexedDB) and pushes/pulls through /api/notes (lib/api.hl). A note's VERSION is its
  9. // `edited` (ms); the browser remembers the `edited` it last saw of a note (`base`).
  10. // Looked up in lib/notes-helpers.hl: textError (size limit), isAlive (tombstones), stampOf (version stamps), copyMark.
  11. import { MPackDB } from 'hl:mpackdb'
  12. import { now } from 'hl:time'
  13. import { storageDir, countOfList } from './util.hl'
  14. import { textError, isAlive, stampOf, copyMark } from './notes-helpers.hl'
  15. static notesTable = new MPackDB(file = storageDir + '/notes.db', primaryKey = '@id', indexes = ['owner'])
  16. // one note of this owner, or null
  17. static noteOf = (userId, id) => {
  18. if (id == null || hlTypeName(id) != 'String' || id == '') { return null }
  19. n = notesTable.fetch(id)
  20. if (n == null || n.owner != userId || !isAlive(n)) { return null }
  21. return n
  22. }
  23. // what the browser needs to compare: id, edited, gone (no text) of every note of the owner, tombstones included
  24. static metaRows = (userId) => {
  25. found = notesTable.find('owner', userId)
  26. out = []
  27. let i = 0
  28. n = countOfList(found)
  29. while (i < n) {
  30. out.push({ id = found[i].id edited = found[i].edited gone = isAlive(found[i]) ? 0 : found[i].gone })
  31. i = i + 1
  32. }
  33. return out
  34. }
  35. // one note with its text, or null
  36. static noteView = (userId, id) => {
  37. n = noteOf(userId, id)
  38. return n == null ? null : { id = n.id text = n.text edited = n.edited created = n.created }
  39. }
  40. static newNote = (userId, text, local, edited) => {
  41. t = now()
  42. id = notesTable.put({ owner = userId text = text created = t edited = stampOf(edited, 0) gone = 0 local = local })
  43. return id
  44. }
  45. // a note pushed by a browser: { id (server id or ''), local (the browser's own id of a never-synced note), text,
  46. // base (the `edited` the browser last saw, 0 = never), at (when the browser last edited it) }
  47. // → { id, edited, text, copy? }: `text` is what the note is NOW (the browser takes it); `copy` = { id, edited, text }
  48. // is the version that lost a conflict, kept as a note of its own — nothing is ever lost.
  49. static pushNote = (userId, id, local, text, base, at) => {
  50. bad = textError(text)
  51. if (bad != null) { return { error = bad } }
  52. let n = null
  53. if (id != '') { n = noteOf(userId, id) }
  54. if (n == null && local != '') {
  55. // a retry after a lost answer: the note the same browser already made
  56. mine = notesTable.find('owner', userId)
  57. let i = 0
  58. c = countOfList(mine)
  59. while (i < c && n == null) {
  60. if (isAlive(mine[i]) && mine[i].local == local) { n = mine[i] }
  61. i = i + 1
  62. }
  63. }
  64. if (n == null) {
  65. // a new note (or one deleted elsewhere while this browser edited it: the edit becomes a note again)
  66. nid = newNote(userId, text, local, at > 0 ? at : now())
  67. if (nid == null) { return { error = 'could not store the note: ' + notesTable.lastError() } }
  68. made = notesTable.fetch(nid)
  69. return { id = nid edited = made.edited text = text }
  70. }
  71. if (n.text == text) { return { id = n.id edited = n.edited text = n.text } }
  72. if (n.edited == base) {
  73. ed = stampOf(at, n.edited)
  74. notesTable.update(n.id, { id = n.id owner = userId text = text created = n.created edited = ed gone = 0 local = n.local == null ? '' : n.local })
  75. after = notesTable.fetch(n.id)
  76. if (after == null || after.text != text) { return { error = 'could not store the note: ' + notesTable.lastError() } }
  77. return { id = n.id edited = after.edited text = text }
  78. }
  79. // CONFLICT: changed here AND there. The later edit wins (last write wins); the other version becomes a copy.
  80. serverText = n.text
  81. serverEdited = n.edited
  82. if (at > serverEdited) {
  83. cid = newNote(userId, serverText + copyMark, '', serverEdited)
  84. ed = stampOf(at, n.edited)
  85. notesTable.update(n.id, { id = n.id owner = userId text = text created = n.created edited = ed gone = 0 local = n.local == null ? '' : n.local })
  86. after = notesTable.fetch(n.id)
  87. cp = cid == null ? null : notesTable.fetch(cid)
  88. return { id = n.id edited = after.edited text = text copy = cp == null ? null : { id = cp.id edited = cp.edited text = cp.text } }
  89. }
  90. cid = newNote(userId, text + copyMark, '', at)
  91. cp = cid == null ? null : notesTable.fetch(cid)
  92. return { id = n.id edited = n.edited text = n.text copy = cp == null ? null : { id = cp.id edited = cp.edited text = cp.text } }
  93. }
  94. // a delete pushed by a browser. Deleted elsewhere already = fine. Changed elsewhere since the browser's `base`
  95. // = NOT deleted (the changed note is kept; the browser takes it back). Deleted = a tombstone (text '', gone = now).
  96. static removeNote = (userId, id, base) => {
  97. raw = id == null || hlTypeName(id) != 'String' || id == '' ? null : notesTable.fetch(id)
  98. if (raw == null || raw.owner != userId) { return { ok = true } }
  99. if (!isAlive(raw)) { return { ok = true } }
  100. if (raw.edited != base) { return { kept = true id = raw.id edited = raw.edited text = raw.text } }
  101. notesTable.update(id, { id = id owner = userId text = '' created = raw.created edited = raw.edited gone = now() local = raw.local == null ? '' : raw.local })
  102. return { ok = true }
  103. }

Branches

Latest commits

  • 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