@objectstack/driver-memory 3.0.10 → 3.0.11
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +8 -0
- package/dist/index.d.mts +126 -1
- package/dist/index.d.ts +126 -1
- package/dist/index.js +304 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +299 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -1
- package/src/memory-driver.test.ts +1 -1
- package/src/memory-driver.ts +179 -0
- package/src/persistence/file-adapter.ts +103 -0
- package/src/persistence/index.ts +4 -0
- package/src/persistence/local-storage-adapter.ts +60 -0
- package/src/persistence/persistence.test.ts +215 -0
package/dist/index.mjs
CHANGED
|
@@ -1,14 +1,171 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/persistence/local-storage-adapter.ts
|
|
12
|
+
var local_storage_adapter_exports = {};
|
|
13
|
+
__export(local_storage_adapter_exports, {
|
|
14
|
+
LocalStoragePersistenceAdapter: () => LocalStoragePersistenceAdapter
|
|
15
|
+
});
|
|
16
|
+
var _LocalStoragePersistenceAdapter, LocalStoragePersistenceAdapter;
|
|
17
|
+
var init_local_storage_adapter = __esm({
|
|
18
|
+
"src/persistence/local-storage-adapter.ts"() {
|
|
19
|
+
"use strict";
|
|
20
|
+
_LocalStoragePersistenceAdapter = class _LocalStoragePersistenceAdapter {
|
|
21
|
+
// 4.5MB warning threshold
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.storageKey = options?.key || "objectstack:memory-db";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load persisted data from localStorage.
|
|
27
|
+
* Returns null if no data exists.
|
|
28
|
+
*/
|
|
29
|
+
async load() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = localStorage.getItem(this.storageKey);
|
|
32
|
+
if (!raw) return null;
|
|
33
|
+
return JSON.parse(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Save data to localStorage.
|
|
40
|
+
* Warns if data size approaches the ~5MB localStorage limit.
|
|
41
|
+
*/
|
|
42
|
+
async save(db) {
|
|
43
|
+
const json = JSON.stringify(db);
|
|
44
|
+
if (json.length > _LocalStoragePersistenceAdapter.SIZE_WARNING_BYTES) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`[ObjectStack] localStorage persistence data size (${(json.length / 1024 / 1024).toFixed(2)}MB) is approaching the ~5MB limit. Consider using a different persistence strategy.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(this.storageKey, json);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error("[ObjectStack] Failed to persist data to localStorage:", e?.message || e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Flush is a no-op for localStorage (writes are synchronous).
|
|
57
|
+
*/
|
|
58
|
+
async flush() {
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
_LocalStoragePersistenceAdapter.SIZE_WARNING_BYTES = 4.5 * 1024 * 1024;
|
|
62
|
+
LocalStoragePersistenceAdapter = _LocalStoragePersistenceAdapter;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// src/persistence/file-adapter.ts
|
|
67
|
+
var file_adapter_exports = {};
|
|
68
|
+
__export(file_adapter_exports, {
|
|
69
|
+
FileSystemPersistenceAdapter: () => FileSystemPersistenceAdapter
|
|
70
|
+
});
|
|
71
|
+
import * as fs from "fs";
|
|
72
|
+
import * as path from "path";
|
|
73
|
+
var FileSystemPersistenceAdapter;
|
|
74
|
+
var init_file_adapter = __esm({
|
|
75
|
+
"src/persistence/file-adapter.ts"() {
|
|
76
|
+
"use strict";
|
|
77
|
+
FileSystemPersistenceAdapter = class {
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.dirty = false;
|
|
80
|
+
this.timer = null;
|
|
81
|
+
this.currentDb = null;
|
|
82
|
+
this.filePath = options?.path || path.join(".objectstack", "data", "memory-driver.json");
|
|
83
|
+
this.autoSaveInterval = options?.autoSaveInterval ?? 2e3;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Load persisted data from disk.
|
|
87
|
+
* Returns null if no file exists.
|
|
88
|
+
*/
|
|
89
|
+
async load() {
|
|
90
|
+
try {
|
|
91
|
+
if (!fs.existsSync(this.filePath)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const raw = fs.readFileSync(this.filePath, "utf-8");
|
|
95
|
+
const data = JSON.parse(raw);
|
|
96
|
+
return data;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Save data to disk using atomic write (temp file + rename).
|
|
103
|
+
*/
|
|
104
|
+
async save(db) {
|
|
105
|
+
this.currentDb = db;
|
|
106
|
+
this.dirty = true;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Flush pending writes to disk immediately.
|
|
110
|
+
*/
|
|
111
|
+
async flush() {
|
|
112
|
+
if (!this.dirty || !this.currentDb) return;
|
|
113
|
+
await this.writeToDisk(this.currentDb);
|
|
114
|
+
this.dirty = false;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Start the auto-save timer.
|
|
118
|
+
*/
|
|
119
|
+
startAutoSave() {
|
|
120
|
+
if (this.timer) return;
|
|
121
|
+
this.timer = setInterval(async () => {
|
|
122
|
+
if (this.dirty && this.currentDb) {
|
|
123
|
+
await this.writeToDisk(this.currentDb);
|
|
124
|
+
this.dirty = false;
|
|
125
|
+
}
|
|
126
|
+
}, this.autoSaveInterval);
|
|
127
|
+
if (this.timer) {
|
|
128
|
+
this.timer.unref();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Stop the auto-save timer and flush pending writes.
|
|
133
|
+
*/
|
|
134
|
+
async stopAutoSave() {
|
|
135
|
+
if (this.timer) {
|
|
136
|
+
clearInterval(this.timer);
|
|
137
|
+
this.timer = null;
|
|
138
|
+
}
|
|
139
|
+
await this.flush();
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Atomic write: write to temp file, then rename.
|
|
143
|
+
*/
|
|
144
|
+
async writeToDisk(db) {
|
|
145
|
+
const dir = path.dirname(this.filePath);
|
|
146
|
+
if (!fs.existsSync(dir)) {
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
}
|
|
149
|
+
const tmpPath = this.filePath + ".tmp";
|
|
150
|
+
const json = JSON.stringify(db, null, 2);
|
|
151
|
+
fs.writeFileSync(tmpPath, json, "utf-8");
|
|
152
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
1
158
|
// src/memory-driver.ts
|
|
2
159
|
import { createLogger } from "@objectstack/core";
|
|
3
160
|
import { Query, Aggregator } from "mingo";
|
|
4
161
|
|
|
5
162
|
// src/memory-matcher.ts
|
|
6
|
-
function getValueByPath(obj,
|
|
7
|
-
if (
|
|
163
|
+
function getValueByPath(obj, path2) {
|
|
164
|
+
if (path2 === "_id" && obj._id === void 0 && obj.id !== void 0) {
|
|
8
165
|
return obj.id;
|
|
9
166
|
}
|
|
10
|
-
if (!
|
|
11
|
-
return
|
|
167
|
+
if (!path2.includes(".")) return obj[path2];
|
|
168
|
+
return path2.split(".").reduce((o, i) => o ? o[i] : void 0, obj);
|
|
12
169
|
}
|
|
13
170
|
|
|
14
171
|
// src/memory-driver.ts
|
|
@@ -19,6 +176,7 @@ var InMemoryDriver = class {
|
|
|
19
176
|
this.version = "1.0.0";
|
|
20
177
|
this.idCounters = /* @__PURE__ */ new Map();
|
|
21
178
|
this.transactions = /* @__PURE__ */ new Map();
|
|
179
|
+
this.persistenceAdapter = null;
|
|
22
180
|
this.supports = {
|
|
23
181
|
// Transaction & Connection Management
|
|
24
182
|
transactions: true,
|
|
@@ -72,6 +230,31 @@ var InMemoryDriver = class {
|
|
|
72
230
|
// Lifecycle
|
|
73
231
|
// ===================================
|
|
74
232
|
async connect() {
|
|
233
|
+
await this.initPersistence();
|
|
234
|
+
if (this.persistenceAdapter) {
|
|
235
|
+
const persisted = await this.persistenceAdapter.load();
|
|
236
|
+
if (persisted) {
|
|
237
|
+
for (const [objectName, records] of Object.entries(persisted)) {
|
|
238
|
+
this.db[objectName] = records;
|
|
239
|
+
for (const record of records) {
|
|
240
|
+
if (record.id && typeof record.id === "string") {
|
|
241
|
+
const parts = record.id.split("-");
|
|
242
|
+
const lastPart = parts[parts.length - 1];
|
|
243
|
+
const counter = parseInt(lastPart, 10);
|
|
244
|
+
if (!isNaN(counter)) {
|
|
245
|
+
const current = this.idCounters.get(objectName) || 0;
|
|
246
|
+
if (counter > current) {
|
|
247
|
+
this.idCounters.set(objectName, counter);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.logger.info("InMemory Database restored from persistence", {
|
|
254
|
+
tables: Object.keys(persisted).length
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
75
258
|
if (this.config.initialData) {
|
|
76
259
|
for (const [objectName, records] of Object.entries(this.config.initialData)) {
|
|
77
260
|
const table = this.getTable(objectName);
|
|
@@ -86,8 +269,17 @@ var InMemoryDriver = class {
|
|
|
86
269
|
} else {
|
|
87
270
|
this.logger.info("InMemory Database Connected (Virtual)");
|
|
88
271
|
}
|
|
272
|
+
if (this.persistenceAdapter?.startAutoSave) {
|
|
273
|
+
this.persistenceAdapter.startAutoSave();
|
|
274
|
+
}
|
|
89
275
|
}
|
|
90
276
|
async disconnect() {
|
|
277
|
+
if (this.persistenceAdapter) {
|
|
278
|
+
if (this.persistenceAdapter.stopAutoSave) {
|
|
279
|
+
await this.persistenceAdapter.stopAutoSave();
|
|
280
|
+
}
|
|
281
|
+
await this.persistenceAdapter.flush();
|
|
282
|
+
}
|
|
91
283
|
const tableCount = Object.keys(this.db).length;
|
|
92
284
|
const recordCount = Object.values(this.db).reduce((sum, table) => sum + table.length, 0);
|
|
93
285
|
this.db = {};
|
|
@@ -167,6 +359,7 @@ var InMemoryDriver = class {
|
|
|
167
359
|
updated_at: data.updated_at || (/* @__PURE__ */ new Date()).toISOString()
|
|
168
360
|
};
|
|
169
361
|
table.push(newRecord);
|
|
362
|
+
this.markDirty();
|
|
170
363
|
this.logger.debug("Record created", { object, id: newRecord.id, tableSize: table.length });
|
|
171
364
|
return { ...newRecord };
|
|
172
365
|
}
|
|
@@ -191,6 +384,7 @@ var InMemoryDriver = class {
|
|
|
191
384
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
192
385
|
};
|
|
193
386
|
table[index] = updatedRecord;
|
|
387
|
+
this.markDirty();
|
|
194
388
|
this.logger.debug("Record updated", { object, id });
|
|
195
389
|
return { ...updatedRecord };
|
|
196
390
|
}
|
|
@@ -223,6 +417,7 @@ var InMemoryDriver = class {
|
|
|
223
417
|
return false;
|
|
224
418
|
}
|
|
225
419
|
table.splice(index, 1);
|
|
420
|
+
this.markDirty();
|
|
226
421
|
this.logger.debug("Record deleted", { object, id, tableSize: table.length });
|
|
227
422
|
return true;
|
|
228
423
|
}
|
|
@@ -271,6 +466,7 @@ var InMemoryDriver = class {
|
|
|
271
466
|
table[index] = updated;
|
|
272
467
|
}
|
|
273
468
|
}
|
|
469
|
+
if (count > 0) this.markDirty();
|
|
274
470
|
this.logger.debug("UpdateMany completed", { object, count });
|
|
275
471
|
return { count };
|
|
276
472
|
}
|
|
@@ -292,6 +488,7 @@ var InMemoryDriver = class {
|
|
|
292
488
|
this.db[object] = [];
|
|
293
489
|
}
|
|
294
490
|
const count = initialLength - this.db[object].length;
|
|
491
|
+
if (count > 0) this.markDirty();
|
|
295
492
|
this.logger.debug("DeleteMany completed", { object, count });
|
|
296
493
|
return { count };
|
|
297
494
|
}
|
|
@@ -339,6 +536,7 @@ var InMemoryDriver = class {
|
|
|
339
536
|
const tx = this.transactions.get(txId);
|
|
340
537
|
this.db = tx.snapshot;
|
|
341
538
|
this.transactions.delete(txId);
|
|
539
|
+
this.markDirty();
|
|
342
540
|
this.logger.debug("Transaction rolled back", { txId });
|
|
343
541
|
}
|
|
344
542
|
// ===================================
|
|
@@ -350,6 +548,7 @@ var InMemoryDriver = class {
|
|
|
350
548
|
async clear() {
|
|
351
549
|
this.db = {};
|
|
352
550
|
this.idCounters.clear();
|
|
551
|
+
this.markDirty();
|
|
353
552
|
this.logger.debug("All data cleared");
|
|
354
553
|
}
|
|
355
554
|
/**
|
|
@@ -586,8 +785,8 @@ var InMemoryDriver = class {
|
|
|
586
785
|
return null;
|
|
587
786
|
}
|
|
588
787
|
}
|
|
589
|
-
setValueByPath(obj,
|
|
590
|
-
const parts =
|
|
788
|
+
setValueByPath(obj, path2, value) {
|
|
789
|
+
const parts = path2.split(".");
|
|
591
790
|
let current = obj;
|
|
592
791
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
593
792
|
const part = parts[i];
|
|
@@ -674,8 +873,100 @@ var InMemoryDriver = class {
|
|
|
674
873
|
const timestamp = Date.now();
|
|
675
874
|
return `${key}-${timestamp}-${counter}`;
|
|
676
875
|
}
|
|
876
|
+
// ===================================
|
|
877
|
+
// Persistence
|
|
878
|
+
// ===================================
|
|
879
|
+
/**
|
|
880
|
+
* Mark the database as dirty, triggering persistence save.
|
|
881
|
+
*/
|
|
882
|
+
markDirty() {
|
|
883
|
+
if (this.persistenceAdapter) {
|
|
884
|
+
this.persistenceAdapter.save(this.db);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Flush pending persistence writes to ensure data is safely stored.
|
|
889
|
+
*/
|
|
890
|
+
async flush() {
|
|
891
|
+
if (this.persistenceAdapter) {
|
|
892
|
+
await this.persistenceAdapter.flush();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Detect whether the current runtime is a browser environment.
|
|
897
|
+
*/
|
|
898
|
+
isBrowserEnvironment() {
|
|
899
|
+
return typeof globalThis.localStorage !== "undefined";
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Initialize the persistence adapter based on configuration.
|
|
903
|
+
* Defaults to 'auto' when persistence is not specified.
|
|
904
|
+
* Use `persistence: false` to explicitly disable persistence.
|
|
905
|
+
*/
|
|
906
|
+
async initPersistence() {
|
|
907
|
+
const persistence = this.config.persistence === void 0 ? "auto" : this.config.persistence;
|
|
908
|
+
if (persistence === false) return;
|
|
909
|
+
if (typeof persistence === "string") {
|
|
910
|
+
if (persistence === "auto") {
|
|
911
|
+
if (this.isBrowserEnvironment()) {
|
|
912
|
+
const { LocalStoragePersistenceAdapter: LocalStoragePersistenceAdapter2 } = await Promise.resolve().then(() => (init_local_storage_adapter(), local_storage_adapter_exports));
|
|
913
|
+
this.persistenceAdapter = new LocalStoragePersistenceAdapter2();
|
|
914
|
+
this.logger.debug("Auto-detected browser environment, using localStorage persistence");
|
|
915
|
+
} else {
|
|
916
|
+
const { FileSystemPersistenceAdapter: FileSystemPersistenceAdapter2 } = await Promise.resolve().then(() => (init_file_adapter(), file_adapter_exports));
|
|
917
|
+
this.persistenceAdapter = new FileSystemPersistenceAdapter2();
|
|
918
|
+
this.logger.debug("Auto-detected Node.js environment, using file persistence");
|
|
919
|
+
}
|
|
920
|
+
} else if (persistence === "file") {
|
|
921
|
+
const { FileSystemPersistenceAdapter: FileSystemPersistenceAdapter2 } = await Promise.resolve().then(() => (init_file_adapter(), file_adapter_exports));
|
|
922
|
+
this.persistenceAdapter = new FileSystemPersistenceAdapter2();
|
|
923
|
+
} else if (persistence === "local") {
|
|
924
|
+
const { LocalStoragePersistenceAdapter: LocalStoragePersistenceAdapter2 } = await Promise.resolve().then(() => (init_local_storage_adapter(), local_storage_adapter_exports));
|
|
925
|
+
this.persistenceAdapter = new LocalStoragePersistenceAdapter2();
|
|
926
|
+
} else {
|
|
927
|
+
throw new Error(`Unknown persistence type: "${persistence}". Use 'file', 'local', or 'auto'.`);
|
|
928
|
+
}
|
|
929
|
+
} else if ("adapter" in persistence && persistence.adapter) {
|
|
930
|
+
this.persistenceAdapter = persistence.adapter;
|
|
931
|
+
} else if ("type" in persistence) {
|
|
932
|
+
if (persistence.type === "auto") {
|
|
933
|
+
if (this.isBrowserEnvironment()) {
|
|
934
|
+
const { LocalStoragePersistenceAdapter: LocalStoragePersistenceAdapter2 } = await Promise.resolve().then(() => (init_local_storage_adapter(), local_storage_adapter_exports));
|
|
935
|
+
this.persistenceAdapter = new LocalStoragePersistenceAdapter2({
|
|
936
|
+
key: persistence.key
|
|
937
|
+
});
|
|
938
|
+
this.logger.debug("Auto-detected browser environment, using localStorage persistence");
|
|
939
|
+
} else {
|
|
940
|
+
const { FileSystemPersistenceAdapter: FileSystemPersistenceAdapter2 } = await Promise.resolve().then(() => (init_file_adapter(), file_adapter_exports));
|
|
941
|
+
this.persistenceAdapter = new FileSystemPersistenceAdapter2({
|
|
942
|
+
path: persistence.path,
|
|
943
|
+
autoSaveInterval: persistence.autoSaveInterval
|
|
944
|
+
});
|
|
945
|
+
this.logger.debug("Auto-detected Node.js environment, using file persistence");
|
|
946
|
+
}
|
|
947
|
+
} else if (persistence.type === "file") {
|
|
948
|
+
const { FileSystemPersistenceAdapter: FileSystemPersistenceAdapter2 } = await Promise.resolve().then(() => (init_file_adapter(), file_adapter_exports));
|
|
949
|
+
this.persistenceAdapter = new FileSystemPersistenceAdapter2({
|
|
950
|
+
path: persistence.path,
|
|
951
|
+
autoSaveInterval: persistence.autoSaveInterval
|
|
952
|
+
});
|
|
953
|
+
} else if (persistence.type === "local") {
|
|
954
|
+
const { LocalStoragePersistenceAdapter: LocalStoragePersistenceAdapter2 } = await Promise.resolve().then(() => (init_local_storage_adapter(), local_storage_adapter_exports));
|
|
955
|
+
this.persistenceAdapter = new LocalStoragePersistenceAdapter2({
|
|
956
|
+
key: persistence.key
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (this.persistenceAdapter) {
|
|
961
|
+
this.logger.debug("Persistence adapter initialized");
|
|
962
|
+
}
|
|
963
|
+
}
|
|
677
964
|
};
|
|
678
965
|
|
|
966
|
+
// src/index.ts
|
|
967
|
+
init_file_adapter();
|
|
968
|
+
init_local_storage_adapter();
|
|
969
|
+
|
|
679
970
|
// src/memory-analytics.ts
|
|
680
971
|
import { createLogger as createLogger2 } from "@objectstack/core";
|
|
681
972
|
var MemoryAnalyticsService = class {
|
|
@@ -1086,7 +1377,9 @@ var index_default = {
|
|
|
1086
1377
|
}
|
|
1087
1378
|
};
|
|
1088
1379
|
export {
|
|
1380
|
+
FileSystemPersistenceAdapter,
|
|
1089
1381
|
InMemoryDriver,
|
|
1382
|
+
LocalStoragePersistenceAdapter,
|
|
1090
1383
|
MemoryAnalyticsService,
|
|
1091
1384
|
index_default as default
|
|
1092
1385
|
};
|