thatcher 1.0.35 → 1.0.37
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/package.json +1 -1
- package/src/index.js +8 -2
- package/src/lib/busybase-store.js +30 -3
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -284,9 +284,15 @@ export class Thatcher {
|
|
|
284
284
|
return create(entity, data, user);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
// opts.expectedVersion: optional optimistic-concurrency guard (see
|
|
288
|
+
// busybase-store.js update()) -- a caller that read the row's _version can
|
|
289
|
+
// pass it here to detect (via a thrown {code:'conflict'} error) a concurrent
|
|
290
|
+
// writer landing in between, instead of silently clobbering it. `user` stays
|
|
291
|
+
// its own positional param (unused by the store today, kept for API
|
|
292
|
+
// stability with existing callers); opts is a new, optional 5th param.
|
|
293
|
+
async update(entity, id, data, user, opts = {}) {
|
|
288
294
|
const { update } = await import(resolveModule('./lib/busybase-store.js'));
|
|
289
|
-
return update(entity, id, data,
|
|
295
|
+
return update(entity, id, data, opts);
|
|
290
296
|
}
|
|
291
297
|
|
|
292
298
|
async delete(entity, id) {
|
|
@@ -254,15 +254,42 @@ export async function create(entity, data, user) {
|
|
|
254
254
|
return record;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
// opts.expectedVersion: optional optimistic-concurrency guard. When supplied,
|
|
258
|
+
// the write is conditioned on the row's internal _version counter still
|
|
259
|
+
// matching the value the caller last read (a plain read-then-write otherwise
|
|
260
|
+
// has no way to detect a concurrent writer landing in between -- whichever
|
|
261
|
+
// caller writes second wins outright, silently discarding the first caller's
|
|
262
|
+
// change). A stale expectedVersion throws a distinguishable 'conflict' error
|
|
263
|
+
// instead of clobbering; the caller re-reads and retries or surfaces the
|
|
264
|
+
// conflict. _version increments on every write regardless of whether the
|
|
265
|
+
// guard is used, so a caller can always read the CURRENT _version to guard
|
|
266
|
+
// its NEXT write. A dedicated counter, not updated_at, because updated_at
|
|
267
|
+
// (now(), second-precision epoch) can collide within the same second under a
|
|
268
|
+
// genuine fast race, silently defeating the guard exactly when it matters
|
|
269
|
+
// most; _version is a plain integer increment with no precision ceiling.
|
|
270
|
+
// Backward compatible: omitting opts.expectedVersion is the exact prior
|
|
271
|
+
// unconditional-write behaviour, unchanged for every existing caller; _version
|
|
272
|
+
// is added as a new column, ignored by every reader that doesn't ask for it.
|
|
273
|
+
export async function update(entity, id, data, opts = {}) {
|
|
258
274
|
const spec = specOf(entity);
|
|
259
275
|
const tbl = tableName(entity);
|
|
260
276
|
|
|
261
277
|
const existing = await get(entity, id);
|
|
262
278
|
if (!existing) throw new Error(`${entity} with id ${id} not found`);
|
|
263
279
|
|
|
264
|
-
const
|
|
265
|
-
|
|
280
|
+
const currentVersion = Number(existing._version) || 0;
|
|
281
|
+
const patch = { ...data, updated_at: now(), _version: currentVersion + 1 };
|
|
282
|
+
let builder = client().from(tbl).update(patch).eq('id', id);
|
|
283
|
+
if (opts.expectedVersion != null) {
|
|
284
|
+
builder = builder.eq('_version', opts.expectedVersion);
|
|
285
|
+
}
|
|
286
|
+
const { data: rows, error } = await builder;
|
|
287
|
+
if (error) throw new Error(`BusyBase update failed: ${error.message || error}`);
|
|
288
|
+
if (opts.expectedVersion != null && Array.isArray(rows) && rows.length === 0) {
|
|
289
|
+
const conflictErr = new Error(`${entity} ${id} was modified by another writer since it was last read`);
|
|
290
|
+
conflictErr.code = 'conflict';
|
|
291
|
+
throw conflictErr;
|
|
292
|
+
}
|
|
266
293
|
return get(entity, id);
|
|
267
294
|
}
|
|
268
295
|
|