dsh-quicknote 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -74,10 +74,13 @@ function registerRoutes(ctx, store) {
74
74
  {
75
75
  path: `${API}/notes/create`,
76
76
  methods: ["POST"],
77
- fetch: wrap(async (request) => json(200, {
78
- ok: true,
79
- note: await store.add((await body(request)).text)
80
- }))
77
+ fetch: wrap(async (request) => {
78
+ const input = await body(request);
79
+ return json(200, {
80
+ ok: true,
81
+ note: await store.add(input.text, input.origin)
82
+ });
83
+ })
81
84
  },
82
85
  {
83
86
  path: `${API}/notes/update`,
@@ -99,8 +102,23 @@ function registerRoutes(ctx, store) {
99
102
  methods: ["POST"],
100
103
  fetch: wrap(async (request) => json(200, {
101
104
  ok: true,
102
- removed: await store.remove((await body(request)).id)
105
+ note: await store.remove((await body(request)).id)
103
106
  }))
107
+ },
108
+ {
109
+ path: `${API}/notes/restore`,
110
+ methods: ["POST"],
111
+ fetch: wrap(async (request) => {
112
+ try {
113
+ return json(200, {
114
+ ok: true,
115
+ note: await store.restore((await body(request)).note)
116
+ });
117
+ } catch (error) {
118
+ if (String(error?.message) === "invalid-note") throw new HttpError(400, "invalid-note");
119
+ throw error;
120
+ }
121
+ })
104
122
  }
105
123
  ];
106
124
  for (const route of routes) scope.effect(() => registry.register({
@@ -113,9 +131,43 @@ function registerRoutes(ctx, store) {
113
131
  }
114
132
  // src/host/store.ts
115
133
  const MAX_TEXT = 2e4;
134
+ function parseNote(value) {
135
+ if (value === null || typeof value !== "object") return null;
136
+ const raw = value;
137
+ if (typeof raw.id !== "string" || raw.id === "" || raw.id.length > 128) return null;
138
+ if (typeof raw.text !== "string") return null;
139
+ const text = raw.text.trim();
140
+ if (text === "" || text.length > MAX_TEXT) return null;
141
+ const createdAt = typeof raw.createdAt === "number" && Number.isFinite(raw.createdAt) ? raw.createdAt : Date.now();
142
+ const note = {
143
+ id: raw.id,
144
+ text,
145
+ done: raw.done === true,
146
+ createdAt
147
+ };
148
+ const origin = parseOrigin(raw.origin);
149
+ if (origin) note.origin = origin;
150
+ return note;
151
+ }
152
+ function parseOrigin(value) {
153
+ if (value === null || typeof value !== "object") return void 0;
154
+ const sessionId = String(value.sessionId ?? "");
155
+ const title = String(value.title ?? "");
156
+ if (sessionId === "" && title === "") return void 0;
157
+ return {
158
+ sessionId,
159
+ title
160
+ };
161
+ }
116
162
  /** A small durable store: one JSON file, atomic whole-file writes, newest first. */
117
163
  function createStore(file) {
118
164
  let cache = null;
165
+ let tail = Promise.resolve();
166
+ function serialized(run) {
167
+ const result = tail.then(run);
168
+ tail = result.then(() => void 0, () => void 0);
169
+ return result;
170
+ }
119
171
  async function load() {
120
172
  if (cache) return cache;
121
173
  try {
@@ -128,46 +180,70 @@ function createStore(file) {
128
180
  }
129
181
  async function save(notes) {
130
182
  await mkdir(dirname(file), { recursive: true });
131
- const temporary = `${file}.${process.pid}.tmp`;
183
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
132
184
  await writeFile(temporary, `${JSON.stringify({ notes }, null, 2)}\n`, { mode: 384 });
133
185
  await rename(temporary, file);
134
186
  cache = notes;
135
187
  }
136
188
  return {
137
189
  file,
138
- async list() {
139
- return [...await load()];
190
+ list() {
191
+ return serialized(async () => [...await load()]);
192
+ },
193
+ add(text, origin) {
194
+ return serialized(async () => {
195
+ const value = String(text ?? "").trim();
196
+ if (value === "") throw new Error("empty-note");
197
+ if (value.length > MAX_TEXT) throw new Error("note-too-long");
198
+ const notes = await load();
199
+ const createdAt = Math.max(Date.now(), (notes[0]?.createdAt ?? 0) + 1);
200
+ const note = {
201
+ id: randomUUID(),
202
+ text: value,
203
+ done: false,
204
+ createdAt
205
+ };
206
+ const cleanOrigin = parseOrigin(origin);
207
+ if (cleanOrigin) note.origin = cleanOrigin;
208
+ await save([note, ...notes]);
209
+ return note;
210
+ });
140
211
  },
141
- async add(text) {
142
- const value = String(text ?? "").trim();
143
- if (value === "") throw new Error("empty-note");
144
- if (value.length > MAX_TEXT) throw new Error("note-too-long");
145
- const note = {
146
- id: randomUUID(),
147
- text: value,
148
- done: false,
149
- createdAt: Date.now()
150
- };
151
- await save([note, ...await load()]);
152
- return note;
212
+ update(id, done) {
213
+ return serialized(async () => {
214
+ const notes = await load();
215
+ const current = notes.find((note) => note.id === id);
216
+ if (!current) return null;
217
+ const updated = {
218
+ ...current,
219
+ done
220
+ };
221
+ await save(notes.map((note) => note.id === id ? updated : note));
222
+ return updated;
223
+ });
153
224
  },
154
- async update(id, done) {
155
- const notes = await load();
156
- const current = notes.find((note) => note.id === id);
157
- if (!current) return null;
158
- const updated = {
159
- ...current,
160
- done
161
- };
162
- await save(notes.map((note) => note.id === id ? updated : note));
163
- return updated;
225
+ remove(id) {
226
+ return serialized(async () => {
227
+ const notes = await load();
228
+ const removed = notes.find((note) => note.id === id) ?? null;
229
+ if (!removed) return null;
230
+ await save(notes.filter((note) => note.id !== id));
231
+ return removed;
232
+ });
164
233
  },
165
- async remove(id) {
166
- const notes = await load();
167
- const next = notes.filter((note) => note.id !== id);
168
- if (next.length === notes.length) return false;
169
- await save(next);
170
- return true;
234
+ restore(value) {
235
+ return serialized(async () => {
236
+ const note = parseNote(value);
237
+ if (!note) throw new Error("invalid-note");
238
+ const notes = await load();
239
+ const existing = notes.find((item) => item.id === note.id);
240
+ if (existing) return existing;
241
+ const index = notes.findIndex((item) => item.createdAt <= note.createdAt);
242
+ const next = [...notes];
243
+ next.splice(index === -1 ? notes.length : index, 0, note);
244
+ await save(next);
245
+ return note;
246
+ });
171
247
  }
172
248
  };
173
249
  }
package/llms.txt CHANGED
@@ -28,3 +28,5 @@
28
28
  - Package: dsh-quicknote
29
29
  - Patch row: `dsh-quicknote`
30
30
  - Client module id: `dsh-quicknote`
31
+ - Storage: `~/.dsh/quicknote/notes.json`
32
+ - Routes: `/api/quicknote/notes`, `/create`, `/update`, `/delete`, `/restore`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quicknote",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Capture a note from the DSH composer, keep it out of the way, and manage the list on one page.",
5
5
  "license": "MIT",
6
6
  "author": "dsh-kit contributors",
@@ -71,7 +71,7 @@
71
71
  "type": "git",
72
72
  "url": "git+https://github.com/dsh-kit/dsh-quicknote.git"
73
73
  },
74
- "homepage": "https://github.com/dsh-kit/dsh-quicknote",
74
+ "homepage": "https://dsh-kit.github.io/dsh-quicknote/",
75
75
  "bugs": {
76
76
  "url": "https://github.com/dsh-kit/dsh-quicknote/issues"
77
77
  }