@better-auth/memory-adapter 1.6.16 → 1.6.17

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.
Files changed (2) hide show
  1. package/dist/index.mjs +109 -27
  2. package/package.json +3 -3
package/dist/index.mjs CHANGED
@@ -30,9 +30,85 @@ function insensitiveEndsWith(recordVal, value) {
30
30
  }
31
31
  //#endregion
32
32
  //#region src/memory-adapter.ts
33
+ /**
34
+ * Index a table's rows by their `id` for row-level reconciliation. Every
35
+ * better-auth row carries an `id` (the adapter's join logic already keys on
36
+ * `record.id`), so the id is a stable identity for the three-way merge.
37
+ */
38
+ function indexById(rows) {
39
+ const byId = /* @__PURE__ */ new Map();
40
+ for (const row of rows) byId.set(row.id, row);
41
+ return byId;
42
+ }
43
+ /**
44
+ * Commit a transaction onto the live database with a three-way merge so a
45
+ * concurrent write that interleaved at an `await` point survives.
46
+ *
47
+ * `base` is the snapshot taken when the transaction started; `clone` is that
48
+ * snapshot after the transaction mutated it; `target` is the live database as
49
+ * it stands now (possibly carrying concurrent writes). Replaying only the
50
+ * `base -> clone` delta onto `target` applies the transaction's own creates,
51
+ * updates, and deletes without disturbing rows or tables the transaction never
52
+ * touched. A row the transaction did not change keeps the live version, so a
53
+ * concurrent edit to a different row is preserved. A row the transaction did
54
+ * change wins last-writer-wins over a concurrent edit to the same row, which is
55
+ * acceptable for an in-memory development adapter; isolation is guaranteed only
56
+ * at row/table granularity.
57
+ *
58
+ * `target` is mutated in place so any reference held elsewhere (for example the
59
+ * `db` object the caller passed to `memoryAdapter`) stays valid.
60
+ */
61
+ function mergeTransactionInto(target, base, clone) {
62
+ const models = new Set([...Object.keys(base), ...Object.keys(clone)]);
63
+ for (const model of models) {
64
+ if (!(model in clone)) {
65
+ delete target[model];
66
+ continue;
67
+ }
68
+ const baseById = indexById(base[model] ?? []);
69
+ const cloneRows = clone[model] ?? [];
70
+ const cloneById = indexById(cloneRows);
71
+ const liveRows = target[model] ?? [];
72
+ const merged = [];
73
+ const placed = /* @__PURE__ */ new Set();
74
+ for (const liveRow of liveRows) {
75
+ const id = liveRow.id;
76
+ const baseRow = baseById.get(id);
77
+ const cloneRow = cloneById.get(id);
78
+ if (baseRow !== void 0 && cloneRow === void 0) continue;
79
+ if (cloneRow !== void 0 && rowChanged(baseRow, cloneRow)) merged.push(cloneRow);
80
+ else merged.push(liveRow);
81
+ placed.add(id);
82
+ }
83
+ for (const cloneRow of cloneRows) if (!baseById.has(cloneRow.id) && !placed.has(cloneRow.id)) merged.push(cloneRow);
84
+ target[model] = merged;
85
+ }
86
+ }
87
+ /**
88
+ * Whether the transaction mutated a row, comparing its pre-transaction
89
+ * snapshot to its post-transaction state. Rows hold scalar columns and
90
+ * adapter-serializable values, so JSON equality reliably tells a
91
+ * transaction-made edit apart from an untouched row.
92
+ */
93
+ function rowChanged(baseRow, cloneRow) {
94
+ if (baseRow === void 0) return true;
95
+ return JSON.stringify(baseRow) !== JSON.stringify(cloneRow);
96
+ }
33
97
  const memoryAdapter = (db, config) => {
34
98
  let lazyOptions = null;
35
- const adapterCreator = createAdapterFactory({
99
+ /**
100
+ * Build an adapter factory whose operations read and write `activeDb`.
101
+ * The non-transactional adapter targets the live `db`. A transaction
102
+ * targets an isolated clone so its uncommitted writes are invisible to
103
+ * concurrent operations against the live `db`. A failed transaction leaves
104
+ * the live `db` untouched, and a committed one replays only its own
105
+ * row/table changes, so a concurrent write that interleaved at an `await`
106
+ * point survives either outcome. Isolation is at row/table granularity:
107
+ * the in-memory adapter does not serialize writes, so two operations that
108
+ * edit the same row resolve last-writer-wins. It is built for development
109
+ * and tests, not production concurrency control.
110
+ */
111
+ const buildAdapterFactory = (activeDb) => createAdapterFactory({
36
112
  config: {
37
113
  adapterId: "memory",
38
114
  adapterName: "Memory Adapter",
@@ -40,19 +116,15 @@ const memoryAdapter = (db, config) => {
40
116
  debugLogs: config?.debugLogs || false,
41
117
  supportsArrays: true,
42
118
  customTransformInput(props) {
43
- if (props.options.advanced?.database?.generateId === "serial" && props.field === "id" && props.action === "create") return db[props.model].length + 1;
119
+ if (props.options.advanced?.database?.generateId === "serial" && props.field === "id" && props.action === "create") return activeDb[props.model].length + 1;
44
120
  return props.data;
45
121
  },
46
122
  transaction: async (cb) => {
47
- const clone = structuredClone(db);
48
- try {
49
- return await cb(adapterCreator(lazyOptions));
50
- } catch (error) {
51
- Object.keys(db).forEach((key) => {
52
- db[key] = clone[key];
53
- });
54
- throw error;
55
- }
123
+ const base = structuredClone(activeDb);
124
+ const clone = structuredClone(activeDb);
125
+ const result = await cb(buildAdapterFactory(clone)(lazyOptions));
126
+ mergeTransactionInto(activeDb, base, clone);
127
+ return result;
56
128
  }
57
129
  },
58
130
  adapter: ({ getFieldName, getDefaultFieldName, options, getModelName }) => {
@@ -79,9 +151,9 @@ const memoryAdapter = (db, config) => {
79
151
  };
80
152
  function convertWhereClause(where, model, join, select) {
81
153
  const baseRecords = (() => {
82
- const table = db[model];
154
+ const table = activeDb[model];
83
155
  if (!table) {
84
- logger.error(`[MemoryAdapter] Model ${model} not found in the DB`, Object.keys(db));
156
+ logger.error(`[MemoryAdapter] Model ${model} not found in the DB`, Object.keys(activeDb));
85
157
  throw new Error(`Model ${model} not found`);
86
158
  }
87
159
  const evalClause = (record, clause) => {
@@ -152,9 +224,9 @@ const memoryAdapter = (db, config) => {
152
224
  const nestedEntry = grouped.get(baseId);
153
225
  for (const [joinModel, joinAttr] of Object.entries(join)) {
154
226
  const joinModelName = getModelName(joinModel);
155
- const joinTable = db[joinModelName];
227
+ const joinTable = activeDb[joinModelName];
156
228
  if (!joinTable) {
157
- logger.error(`[MemoryAdapter] JoinOption model ${joinModelName} not found in the DB`, Object.keys(db));
229
+ logger.error(`[MemoryAdapter] JoinOption model ${joinModelName} not found in the DB`, Object.keys(activeDb));
158
230
  throw new Error(`JoinOption model ${joinModelName} not found`);
159
231
  }
160
232
  const matchingRecords = joinTable.filter((joinRecord) => joinRecord[joinAttr.on.to] === baseRecord[joinAttr.on.from]);
@@ -178,9 +250,9 @@ const memoryAdapter = (db, config) => {
178
250
  }
179
251
  return {
180
252
  create: async ({ model, data }) => {
181
- if (options.advanced?.database?.generateId === "serial") data.id = db[getModelName(model)].length + 1;
182
- if (!db[model]) db[model] = [];
183
- db[model].push(data);
253
+ if (options.advanced?.database?.generateId === "serial") data.id = activeDb[getModelName(model)].length + 1;
254
+ if (!activeDb[model]) activeDb[model] = [];
255
+ activeDb[model].push(data);
184
256
  return data;
185
257
  },
186
258
  findOne: async ({ model, where, select, join }) => {
@@ -210,9 +282,10 @@ const memoryAdapter = (db, config) => {
210
282
  },
211
283
  count: async ({ model, where }) => {
212
284
  if (where) return convertWhereClause(where, model).length;
213
- return db[model].length;
285
+ return activeDb[model].length;
214
286
  },
215
287
  update: async ({ model, where, update }) => {
288
+ if (where.length === 0) return null;
216
289
  const res = convertWhereClause(where, model);
217
290
  res.forEach((record) => {
218
291
  Object.assign(record, update);
@@ -220,15 +293,16 @@ const memoryAdapter = (db, config) => {
220
293
  return res[0] || null;
221
294
  },
222
295
  delete: async ({ model, where }) => {
223
- const table = db[model];
296
+ if (where.length === 0) return;
297
+ const table = activeDb[model];
224
298
  const res = convertWhereClause(where, model);
225
- db[model] = table.filter((record) => !res.includes(record));
299
+ activeDb[model] = table.filter((record) => !res.includes(record));
226
300
  },
227
301
  deleteMany: async ({ model, where }) => {
228
- const table = db[model];
302
+ const table = activeDb[model];
229
303
  const res = convertWhereClause(where, model);
230
304
  let count = 0;
231
- db[model] = table.filter((record) => {
305
+ activeDb[model] = table.filter((record) => {
232
306
  if (res.includes(record)) {
233
307
  count++;
234
308
  return false;
@@ -238,22 +312,30 @@ const memoryAdapter = (db, config) => {
238
312
  return count;
239
313
  },
240
314
  consumeOne: async ({ model, where }) => {
241
- const table = db[model];
315
+ const table = activeDb[model];
242
316
  const target = convertWhereClause(where, model)[0];
243
317
  if (!target) return null;
244
- db[model] = table.filter((record) => record !== target);
318
+ activeDb[model] = table.filter((record) => record !== target);
245
319
  return target;
246
320
  },
247
- updateMany({ model, where, update }) {
321
+ incrementOne: async ({ model, where, increment, set }) => {
322
+ const target = convertWhereClause(where, model)[0];
323
+ if (!target) return null;
324
+ for (const [field, delta] of Object.entries(increment)) target[field] = (typeof target[field] === "number" ? target[field] : 0) + delta;
325
+ if (set) Object.assign(target, set);
326
+ return target;
327
+ },
328
+ updateMany: async ({ model, where, update }) => {
248
329
  const res = convertWhereClause(where, model);
249
330
  res.forEach((record) => {
250
331
  Object.assign(record, update);
251
332
  });
252
- return res[0] || null;
333
+ return res.length;
253
334
  }
254
335
  };
255
336
  }
256
337
  });
338
+ const adapterCreator = buildAdapterFactory(db);
257
339
  return (options) => {
258
340
  lazyOptions = options;
259
341
  return adapterCreator(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/memory-adapter",
3
- "version": "1.6.16",
3
+ "version": "1.6.17",
4
4
  "description": "Memory adapter for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,13 +36,13 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@better-auth/utils": "0.4.1",
39
- "@better-auth/core": "^1.6.16"
39
+ "@better-auth/core": "^1.6.17"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@better-auth/utils": "0.4.1",
43
43
  "tsdown": "0.21.1",
44
44
  "typescript": "^5.9.3",
45
- "@better-auth/core": "1.6.16"
45
+ "@better-auth/core": "1.6.17"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "tsdown",