lib/notes-lifecycle.js
Rama main · réplica de lectura del repositorio original.
// Sticky Note lifecycle.
//
// A note is an attention item, so "done" must never mean "gone". Completing a
// note closes it: the record stays, stays queryable, and stays reversible. Only
// its place on the active canvas changes.
//
// Legacy notes carry no state field. They project as open, with one exception:
// the older `hidden: true` flag already meant "taken off the canvas and hidden
// from agents", which is exactly what closed means, so those project as closed.
// The original `hidden` value is preserved rather than rewritten, and no
// closedAt is invented for a note that never recorded one.
//
// Projection is applied when notes are loaded, so nothing on disk is rewritten
// just by reading. The normalized fields reach the file the next time that note
// is written for some other reason.
var STATE_OPEN = "open";
var STATE_CLOSED = "closed";
var STATES = [STATE_OPEN, STATE_CLOSED];
function isState(value) {
return STATES.indexOf(value) !== -1;
}
// The state a note is in, derived rather than trusted: an unknown or missing
// value falls back to the legacy flag and then to open.
function stateOf(note) {
if (!note) return STATE_OPEN;
if (isState(note.state)) return note.state;
return note.hidden === true ? STATE_CLOSED : STATE_OPEN;
}
function isOpen(note) {
return stateOf(note) === STATE_OPEN;
}
function isClosed(note) {
return stateOf(note) === STATE_CLOSED;
}
// Normalize one note in place. Returns true when anything changed, so a caller
// can decide whether the migration is worth persisting.
function normalize(note) {
if (!note || typeof note !== "object") return false;
var changed = false;
var state = stateOf(note);
if (note.state !== state) {
note.state = state;
changed = true;
}
if (note.closedAt === undefined) {
// A legacy hidden note has no recorded close time and none is invented.
note.closedAt = null;
changed = true;
}
if (note.closedBy === undefined) {
note.closedBy = null;
changed = true;
}
// Keep the legacy flag consistent with the state so an old client that only
// understands `hidden` still takes a closed note off its canvas.
var hidden = state === STATE_CLOSED;
if (note.hidden !== hidden) {
note.hidden = hidden;
changed = true;
}
return changed;
}
function normalizeAll(notes) {
var list = Array.isArray(notes) ? notes : [];
var changed = false;
for (var i = 0; i < list.length; i++) {
if (normalize(list[i])) changed = true;
}
return { notes: list, changed: changed };
}
function openNotes(notes) {
return (notes || []).filter(function (note) { return note && isOpen(note); });
}
function closedNotes(notes) {
return (notes || []).filter(function (note) { return note && isClosed(note); });
}
// Actors are always built from server-bound context. Nothing here reads a
// caller-supplied identity, so a payload cannot claim to be someone else.
function sessionActor(session) {
if (!session) return null;
return {
type: "session",
sessionId: session.localId !== undefined && session.localId !== null ? session.localId : null,
vendor: session.vendor || null,
};
}
function userActor(user) {
return {
type: "user",
userId: (user && user.id) || null,
displayName: (user && (user.displayName || user.username)) || null,
};
}
// Apply the close transition. Idempotent: closing an already-closed note keeps
// the original closedAt and actor rather than restamping it, so the record of
// when it was actually completed survives repeated calls.
function applyClose(note, actor, now) {
if (!note) return false;
normalize(note);
if (isClosed(note)) return false;
note.state = STATE_CLOSED;
note.hidden = true;
note.closedAt = typeof now === "number" ? now : Date.now();
note.closedBy = actor || null;
return true;
}
// Apply the reopen transition. Idempotent in the same way, and it clears the
// close provenance because the note is live again.
function applyReopen(note) {
if (!note) return false;
normalize(note);
if (isOpen(note)) return false;
note.state = STATE_OPEN;
note.hidden = false;
note.closedAt = null;
note.closedBy = null;
return true;
}
module.exports = {
STATE_OPEN: STATE_OPEN,
STATE_CLOSED: STATE_CLOSED,
STATES: STATES,
isState: isState,
stateOf: stateOf,
isOpen: isOpen,
isClosed: isClosed,
normalize: normalize,
normalizeAll: normalizeAll,
openNotes: openNotes,
closedNotes: closedNotes,
sessionActor: sessionActor,
userActor: userActor,
applyClose: applyClose,
applyReopen: applyReopen,
};