bff-store 0.1.0
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/.claude/settings.local.json +45 -0
- package/CONTEXT.md +53 -0
- package/README.md +223 -0
- package/dist/cli.js +32577 -0
- package/dist/index.d.mts +232 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.mjs +430 -0
- package/dist/package.json +62 -0
- package/dist/server/entry.d.mts +94 -0
- package/dist/server/entry.d.ts +94 -0
- package/dist/server/entry.js +573 -0
- package/dist/server/entry.mjs +533 -0
- package/dist/server-V7WCW4ZB.mjs +530 -0
- package/dist/storage/jsonl-entry.d.mts +42 -0
- package/dist/storage/jsonl-entry.d.ts +42 -0
- package/dist/storage/jsonl-entry.js +112 -0
- package/dist/storage/jsonl-entry.mjs +74 -0
- package/dist/storage/mongodb-entry.d.mts +40 -0
- package/dist/storage/mongodb-entry.d.ts +40 -0
- package/dist/storage/mongodb-entry.js +114 -0
- package/dist/storage/mongodb-entry.mjs +86 -0
- package/docs/BUG_DIAGNOSIS_REMOTE_STORAGE_OPTIONS.md +104 -0
- package/docs/BUG_FIX_REMOTE_STORAGE_OPTIONS.md +63 -0
- package/docs/BUG_FIX_SESSION_2026-06-03.md +171 -0
- package/docs/IMPLEMENTATION.md +333 -0
- package/docs/PLAN.md +153 -0
- package/docs/REMOTE_STORAGE_CONFIG.md +125 -0
- package/docs/SIDECAR_SERVER.md +184 -0
- package/package.json +62 -0
- package/scripts/adapt-dist-package.js +33 -0
- package/src/atomCreator.ts +76 -0
- package/src/createStore.ts +77 -0
- package/src/debouncer.ts +84 -0
- package/src/index.ts +35 -0
- package/src/server/cli.ts +62 -0
- package/src/server/entityIdCache.ts +57 -0
- package/src/server/entry.ts +12 -0
- package/src/server/handlers.ts +271 -0
- package/src/server/index.ts +182 -0
- package/src/server/router.ts +74 -0
- package/src/server.ts +5 -0
- package/src/storage/adapters/remoteStorage.ts +70 -0
- package/src/storage/base.ts +28 -0
- package/src/storage/index.ts +9 -0
- package/src/storage/jsonl-entry.ts +9 -0
- package/src/storage/jsonl.ts +111 -0
- package/src/storage/memory.ts +49 -0
- package/src/storage/mongodb-entry.ts +9 -0
- package/src/storage/mongodb.ts +132 -0
- package/src/storage/protocol.ts +170 -0
- package/src/storage/transport.ts +95 -0
- package/src/types.ts +76 -0
- package/src/useStore.ts +83 -0
- package/tests/atomCreator.test.ts +153 -0
- package/tests/createStore.test.ts +126 -0
- package/tests/debouncer.test.ts +125 -0
- package/tests/server.test.ts +158 -0
- package/tests/storage/jsonl.test.ts +132 -0
- package/tests/storage/memory.test.ts +101 -0
- package/tests/storage/mongodb.test.ts +40 -0
- package/tests/storage/remoteStorage.test.ts +126 -0
- package/tests/useStore.test.tsx +147 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +53 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/storage/jsonl-entry.ts
|
|
31
|
+
var jsonl_entry_exports = {};
|
|
32
|
+
__export(jsonl_entry_exports, {
|
|
33
|
+
createJsonlStorage: () => createJsonlStorage,
|
|
34
|
+
jsonlStorage: () => jsonlStorage
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(jsonl_entry_exports);
|
|
37
|
+
|
|
38
|
+
// src/storage/jsonl.ts
|
|
39
|
+
var fs = __toESM(require("fs"));
|
|
40
|
+
var path = __toESM(require("path"));
|
|
41
|
+
function jsonlStorage(options) {
|
|
42
|
+
const baseDir = options?.dir ?? "./sessions";
|
|
43
|
+
let entityId = "default";
|
|
44
|
+
function getFilePath(eId, key) {
|
|
45
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
46
|
+
return path.join(baseDir, eId, `${safeKey}.jsonl`);
|
|
47
|
+
}
|
|
48
|
+
const storage = {
|
|
49
|
+
async get(key) {
|
|
50
|
+
if (!entityId) return null;
|
|
51
|
+
const filePath = getFilePath(entityId, key);
|
|
52
|
+
if (!fs.existsSync(filePath)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
57
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
58
|
+
if (lines.length === 0) return null;
|
|
59
|
+
const lastLine = lines[lines.length - 1];
|
|
60
|
+
const entry = JSON.parse(lastLine);
|
|
61
|
+
return entry.value;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
async set(key, value) {
|
|
67
|
+
if (!entityId) return;
|
|
68
|
+
const filePath = getFilePath(entityId, key);
|
|
69
|
+
const dir = path.dirname(filePath);
|
|
70
|
+
if (!fs.existsSync(dir)) {
|
|
71
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
const entry = {
|
|
74
|
+
key,
|
|
75
|
+
value,
|
|
76
|
+
timestamp: Date.now()
|
|
77
|
+
};
|
|
78
|
+
const line = JSON.stringify(entry) + "\n";
|
|
79
|
+
fs.appendFileSync(filePath, line, "utf-8");
|
|
80
|
+
},
|
|
81
|
+
async remove(key) {
|
|
82
|
+
if (!entityId) return;
|
|
83
|
+
const filePath = getFilePath(entityId, key);
|
|
84
|
+
if (fs.existsSync(filePath)) {
|
|
85
|
+
fs.unlinkSync(filePath);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
async getMultiple(keys) {
|
|
89
|
+
const result = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const key of keys) {
|
|
91
|
+
const value = await this.get(key);
|
|
92
|
+
if (value !== null) {
|
|
93
|
+
result.set(key, value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
storage,
|
|
101
|
+
name: "jsonl",
|
|
102
|
+
setEntityId(id) {
|
|
103
|
+
entityId = id;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var createJsonlStorage = jsonlStorage;
|
|
108
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
109
|
+
0 && (module.exports = {
|
|
110
|
+
createJsonlStorage,
|
|
111
|
+
jsonlStorage
|
|
112
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/storage/jsonl.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
function jsonlStorage(options) {
|
|
5
|
+
const baseDir = options?.dir ?? "./sessions";
|
|
6
|
+
let entityId = "default";
|
|
7
|
+
function getFilePath(eId, key) {
|
|
8
|
+
const safeKey = key.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
9
|
+
return path.join(baseDir, eId, `${safeKey}.jsonl`);
|
|
10
|
+
}
|
|
11
|
+
const storage = {
|
|
12
|
+
async get(key) {
|
|
13
|
+
if (!entityId) return null;
|
|
14
|
+
const filePath = getFilePath(entityId, key);
|
|
15
|
+
if (!fs.existsSync(filePath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
20
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
21
|
+
if (lines.length === 0) return null;
|
|
22
|
+
const lastLine = lines[lines.length - 1];
|
|
23
|
+
const entry = JSON.parse(lastLine);
|
|
24
|
+
return entry.value;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async set(key, value) {
|
|
30
|
+
if (!entityId) return;
|
|
31
|
+
const filePath = getFilePath(entityId, key);
|
|
32
|
+
const dir = path.dirname(filePath);
|
|
33
|
+
if (!fs.existsSync(dir)) {
|
|
34
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const entry = {
|
|
37
|
+
key,
|
|
38
|
+
value,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
};
|
|
41
|
+
const line = JSON.stringify(entry) + "\n";
|
|
42
|
+
fs.appendFileSync(filePath, line, "utf-8");
|
|
43
|
+
},
|
|
44
|
+
async remove(key) {
|
|
45
|
+
if (!entityId) return;
|
|
46
|
+
const filePath = getFilePath(entityId, key);
|
|
47
|
+
if (fs.existsSync(filePath)) {
|
|
48
|
+
fs.unlinkSync(filePath);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
async getMultiple(keys) {
|
|
52
|
+
const result = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const key of keys) {
|
|
54
|
+
const value = await this.get(key);
|
|
55
|
+
if (value !== null) {
|
|
56
|
+
result.set(key, value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
storage,
|
|
64
|
+
name: "jsonl",
|
|
65
|
+
setEntityId(id) {
|
|
66
|
+
entityId = id;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
var createJsonlStorage = jsonlStorage;
|
|
71
|
+
export {
|
|
72
|
+
createJsonlStorage,
|
|
73
|
+
jsonlStorage
|
|
74
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
interface Storage {
|
|
4
|
+
get<T>(key: string): Promise<T | null>;
|
|
5
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
6
|
+
remove(key: string): Promise<void>;
|
|
7
|
+
/** Optional: get multiple keys at once for batch loading */
|
|
8
|
+
getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
|
|
9
|
+
/** Optional: set multiple keys at once for batch saving */
|
|
10
|
+
setMultiple?<T>(entries: Map<string, T>): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
interface StorageAdapter {
|
|
13
|
+
storage: Storage;
|
|
14
|
+
name: string;
|
|
15
|
+
/** Optional: set the entityId for multi-tenant storage */
|
|
16
|
+
setEntityId?(entityId: string): void;
|
|
17
|
+
}
|
|
18
|
+
type AsyncStorageFactory<T = unknown> = (options: T) => Promise<StorageAdapter>;
|
|
19
|
+
|
|
20
|
+
interface StorageOptions {
|
|
21
|
+
debounceMs?: number;
|
|
22
|
+
}
|
|
23
|
+
interface MongoStorageOptions extends StorageOptions {
|
|
24
|
+
url: string;
|
|
25
|
+
database?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MongoStorageAdapter extends StorageAdapter {
|
|
29
|
+
client: MongoClient;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* MongoDB storage adapter
|
|
34
|
+
* Stores data in collection: {database}.state_{entityId}
|
|
35
|
+
* Each entityId gets its own collection.
|
|
36
|
+
*/
|
|
37
|
+
declare function mongodbStorage(options: MongoStorageOptions): Promise<MongoStorageAdapter>;
|
|
38
|
+
declare const createMongoStorage: AsyncStorageFactory<MongoStorageOptions>;
|
|
39
|
+
|
|
40
|
+
export { type MongoStorageOptions, createMongoStorage, mongodbStorage };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { MongoClient } from 'mongodb';
|
|
2
|
+
|
|
3
|
+
interface Storage {
|
|
4
|
+
get<T>(key: string): Promise<T | null>;
|
|
5
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
6
|
+
remove(key: string): Promise<void>;
|
|
7
|
+
/** Optional: get multiple keys at once for batch loading */
|
|
8
|
+
getMultiple?<T>(keys: string[]): Promise<Map<string, T>>;
|
|
9
|
+
/** Optional: set multiple keys at once for batch saving */
|
|
10
|
+
setMultiple?<T>(entries: Map<string, T>): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
interface StorageAdapter {
|
|
13
|
+
storage: Storage;
|
|
14
|
+
name: string;
|
|
15
|
+
/** Optional: set the entityId for multi-tenant storage */
|
|
16
|
+
setEntityId?(entityId: string): void;
|
|
17
|
+
}
|
|
18
|
+
type AsyncStorageFactory<T = unknown> = (options: T) => Promise<StorageAdapter>;
|
|
19
|
+
|
|
20
|
+
interface StorageOptions {
|
|
21
|
+
debounceMs?: number;
|
|
22
|
+
}
|
|
23
|
+
interface MongoStorageOptions extends StorageOptions {
|
|
24
|
+
url: string;
|
|
25
|
+
database?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface MongoStorageAdapter extends StorageAdapter {
|
|
29
|
+
client: MongoClient;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* MongoDB storage adapter
|
|
34
|
+
* Stores data in collection: {database}.state_{entityId}
|
|
35
|
+
* Each entityId gets its own collection.
|
|
36
|
+
*/
|
|
37
|
+
declare function mongodbStorage(options: MongoStorageOptions): Promise<MongoStorageAdapter>;
|
|
38
|
+
declare const createMongoStorage: AsyncStorageFactory<MongoStorageOptions>;
|
|
39
|
+
|
|
40
|
+
export { type MongoStorageOptions, createMongoStorage, mongodbStorage };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/storage/mongodb-entry.ts
|
|
21
|
+
var mongodb_entry_exports = {};
|
|
22
|
+
__export(mongodb_entry_exports, {
|
|
23
|
+
createMongoStorage: () => createMongoStorage,
|
|
24
|
+
mongodbStorage: () => mongodbStorage
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(mongodb_entry_exports);
|
|
27
|
+
|
|
28
|
+
// src/storage/mongodb.ts
|
|
29
|
+
var import_mongodb = require("mongodb");
|
|
30
|
+
async function mongodbStorage(options) {
|
|
31
|
+
const {
|
|
32
|
+
url,
|
|
33
|
+
database = "jotai_state_store"
|
|
34
|
+
} = options;
|
|
35
|
+
const client = new import_mongodb.MongoClient(url);
|
|
36
|
+
await client.connect();
|
|
37
|
+
const db = client.db(database);
|
|
38
|
+
let currentEntityId = "default";
|
|
39
|
+
function getCollectionName(eId) {
|
|
40
|
+
return `state_${eId}`;
|
|
41
|
+
}
|
|
42
|
+
function getCollection(eId) {
|
|
43
|
+
return db.collection(getCollectionName(eId));
|
|
44
|
+
}
|
|
45
|
+
const storage = {
|
|
46
|
+
async get(key) {
|
|
47
|
+
const collection = getCollection(currentEntityId);
|
|
48
|
+
const entry = await collection.findOne(
|
|
49
|
+
{ key },
|
|
50
|
+
{ sort: { timestamp: -1 } }
|
|
51
|
+
);
|
|
52
|
+
return entry ? entry.value : null;
|
|
53
|
+
},
|
|
54
|
+
async set(key, value) {
|
|
55
|
+
const collection = getCollection(currentEntityId);
|
|
56
|
+
await collection.insertOne({
|
|
57
|
+
key,
|
|
58
|
+
value,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
entityId: currentEntityId
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
async remove(key) {
|
|
64
|
+
const collection = getCollection(currentEntityId);
|
|
65
|
+
await collection.deleteMany({ key });
|
|
66
|
+
},
|
|
67
|
+
async getMultiple(keys) {
|
|
68
|
+
const collection = getCollection(currentEntityId);
|
|
69
|
+
const entries = await collection.find({ key: { $in: keys } }).sort({ timestamp: -1 }).toArray();
|
|
70
|
+
const result = /* @__PURE__ */ new Map();
|
|
71
|
+
const seen = /* @__PURE__ */ new Set();
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!seen.has(entry.key)) {
|
|
74
|
+
result.set(entry.key, entry.value);
|
|
75
|
+
seen.add(entry.key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
},
|
|
80
|
+
async setMultiple(entries) {
|
|
81
|
+
const collection = getCollection(currentEntityId);
|
|
82
|
+
const docs = [];
|
|
83
|
+
entries.forEach((value, key) => {
|
|
84
|
+
docs.push({
|
|
85
|
+
key,
|
|
86
|
+
value,
|
|
87
|
+
timestamp: Date.now(),
|
|
88
|
+
entityId: currentEntityId
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
if (docs.length > 0) {
|
|
92
|
+
await collection.insertMany(docs);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const adapter = {
|
|
97
|
+
storage,
|
|
98
|
+
name: "mongodb",
|
|
99
|
+
client,
|
|
100
|
+
setEntityId(id) {
|
|
101
|
+
currentEntityId = id;
|
|
102
|
+
},
|
|
103
|
+
async close() {
|
|
104
|
+
await client.close();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
return adapter;
|
|
108
|
+
}
|
|
109
|
+
var createMongoStorage = mongodbStorage;
|
|
110
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
111
|
+
0 && (module.exports = {
|
|
112
|
+
createMongoStorage,
|
|
113
|
+
mongodbStorage
|
|
114
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/storage/mongodb.ts
|
|
2
|
+
import { MongoClient } from "mongodb";
|
|
3
|
+
async function mongodbStorage(options) {
|
|
4
|
+
const {
|
|
5
|
+
url,
|
|
6
|
+
database = "jotai_state_store"
|
|
7
|
+
} = options;
|
|
8
|
+
const client = new MongoClient(url);
|
|
9
|
+
await client.connect();
|
|
10
|
+
const db = client.db(database);
|
|
11
|
+
let currentEntityId = "default";
|
|
12
|
+
function getCollectionName(eId) {
|
|
13
|
+
return `state_${eId}`;
|
|
14
|
+
}
|
|
15
|
+
function getCollection(eId) {
|
|
16
|
+
return db.collection(getCollectionName(eId));
|
|
17
|
+
}
|
|
18
|
+
const storage = {
|
|
19
|
+
async get(key) {
|
|
20
|
+
const collection = getCollection(currentEntityId);
|
|
21
|
+
const entry = await collection.findOne(
|
|
22
|
+
{ key },
|
|
23
|
+
{ sort: { timestamp: -1 } }
|
|
24
|
+
);
|
|
25
|
+
return entry ? entry.value : null;
|
|
26
|
+
},
|
|
27
|
+
async set(key, value) {
|
|
28
|
+
const collection = getCollection(currentEntityId);
|
|
29
|
+
await collection.insertOne({
|
|
30
|
+
key,
|
|
31
|
+
value,
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
entityId: currentEntityId
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
async remove(key) {
|
|
37
|
+
const collection = getCollection(currentEntityId);
|
|
38
|
+
await collection.deleteMany({ key });
|
|
39
|
+
},
|
|
40
|
+
async getMultiple(keys) {
|
|
41
|
+
const collection = getCollection(currentEntityId);
|
|
42
|
+
const entries = await collection.find({ key: { $in: keys } }).sort({ timestamp: -1 }).toArray();
|
|
43
|
+
const result = /* @__PURE__ */ new Map();
|
|
44
|
+
const seen = /* @__PURE__ */ new Set();
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!seen.has(entry.key)) {
|
|
47
|
+
result.set(entry.key, entry.value);
|
|
48
|
+
seen.add(entry.key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
async setMultiple(entries) {
|
|
54
|
+
const collection = getCollection(currentEntityId);
|
|
55
|
+
const docs = [];
|
|
56
|
+
entries.forEach((value, key) => {
|
|
57
|
+
docs.push({
|
|
58
|
+
key,
|
|
59
|
+
value,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
entityId: currentEntityId
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
if (docs.length > 0) {
|
|
65
|
+
await collection.insertMany(docs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const adapter = {
|
|
70
|
+
storage,
|
|
71
|
+
name: "mongodb",
|
|
72
|
+
client,
|
|
73
|
+
setEntityId(id) {
|
|
74
|
+
currentEntityId = id;
|
|
75
|
+
},
|
|
76
|
+
async close() {
|
|
77
|
+
await client.close();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return adapter;
|
|
81
|
+
}
|
|
82
|
+
var createMongoStorage = mongodbStorage;
|
|
83
|
+
export {
|
|
84
|
+
createMongoStorage,
|
|
85
|
+
mongodbStorage
|
|
86
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Bug 诊断:`'backend' does not exist in type 'RemoteStorageOptions'`
|
|
2
|
+
|
|
3
|
+
## 现象
|
|
4
|
+
|
|
5
|
+
在 `tests/next_test` 中使用 `bff-store`(通过 `"file:../../dist"` 安装)时,TypeScript 报错:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Object literal may only specify known properties, and 'backend' does not exist in type 'RemoteStorageOptions'.ts(2353)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
触发代码:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { remoteStorage } from 'bff-store';
|
|
15
|
+
|
|
16
|
+
const adapter = remoteStorage({
|
|
17
|
+
backend: 'jsonl',
|
|
18
|
+
jsonlDir: '/tmp/test',
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 根因分析
|
|
23
|
+
|
|
24
|
+
问题由**两个因素叠加**导致:
|
|
25
|
+
|
|
26
|
+
### 因素一(主因):`dist/package.json` 路径错误
|
|
27
|
+
|
|
28
|
+
构建脚本 `npm run build` 直接将项目根目录的 `package.json` 复制到 `dist/`。根目录的 `package.json` 中,`exports`、`main`、`module`、`types` 字段的路径都带有 `./dist/` 前缀:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"module": "dist/index.mjs",
|
|
34
|
+
"types": "dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"import": "./dist/index.mjs",
|
|
39
|
+
"require": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
这些路径在 npm 发布时是正确的(因为 `dist/` 是项目根目录的子目录)。但当通过 `"file:../../dist"` 本地安装时,**包根目录就是 `dist/` 本身**,`./dist/index.d.ts` 就会解析为 `dist/dist/index.d.ts` —— 该文件不存在。
|
|
46
|
+
|
|
47
|
+
| 场景 | 包根目录 | `./dist/index.d.ts` 解析结果 | 存在? |
|
|
48
|
+
|------|----------|---------------------------|------|
|
|
49
|
+
| npm 发布 | 项目根目录 | `项目根/dist/index.d.ts` | 是 |
|
|
50
|
+
| `file:../../dist` | `dist/` | `dist/dist/index.d.ts` | **否** |
|
|
51
|
+
|
|
52
|
+
因此 TypeScript 无法通过本地 symlink 解析到正确的类型声明文件。
|
|
53
|
+
|
|
54
|
+
### 因素二(助推):全局安装了旧版本
|
|
55
|
+
|
|
56
|
+
由于本地 symlink 解析失败,TypeScript 沿目录树向上查找,最终在 `/Users/Admin/node_modules/bff-store/` 找到了全局安装的 **v0.1.1**(当前项目版本为 v0.1.3)。
|
|
57
|
+
|
|
58
|
+
该旧版本的 `RemoteStorageOptions` 接口**没有** `backend` 等新增字段:
|
|
59
|
+
|
|
60
|
+
- v0.1.1(旧):
|
|
61
|
+
```typescript
|
|
62
|
+
interface RemoteStorageOptions {
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
entityId?: string;
|
|
65
|
+
transport?: TransportAdapter;
|
|
66
|
+
protocol?: StorageHttpProtocol;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- v0.1.3(当前源码/dist):
|
|
71
|
+
```typescript
|
|
72
|
+
interface RemoteStorageOptions {
|
|
73
|
+
baseUrl?: string;
|
|
74
|
+
entityId?: string;
|
|
75
|
+
transport?: TransportAdapter;
|
|
76
|
+
protocol?: StorageHttpProtocol;
|
|
77
|
+
backend?: 'mongodb' | 'jsonl'; // 新增
|
|
78
|
+
mongoUrl?: string; // 新增
|
|
79
|
+
mongoDb?: string; // 新增
|
|
80
|
+
jsonlDir?: string; // 新增
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 修复方案
|
|
85
|
+
|
|
86
|
+
新增 `scripts/adapt-dist-package.js`,在构建时将 `dist/package.json` 中的路径从"相对于项目根目录"改写为"相对于 dist/":
|
|
87
|
+
|
|
88
|
+
- `./dist/index.d.ts` → `./index.d.ts`
|
|
89
|
+
- `./dist/index.mjs` → `./index.mjs`
|
|
90
|
+
- `./dist/index.js` → `./index.js`
|
|
91
|
+
- `./dist/storage/...` → `./storage/...`
|
|
92
|
+
- `./dist/server/...` → `./server/...`
|
|
93
|
+
|
|
94
|
+
同时更新 `package.json` 的 `build` 脚本,在 `tsup` 和 `cp` 之间插入该脚本。
|
|
95
|
+
|
|
96
|
+
## 修复文件
|
|
97
|
+
|
|
98
|
+
- **新增**: `scripts/adapt-dist-package.js` — 路径改写脚本
|
|
99
|
+
- **修改**: `package.json` — build 脚本中调用 `node scripts/adapt-dist-package.js`
|
|
100
|
+
|
|
101
|
+
## 验证
|
|
102
|
+
|
|
103
|
+
- `npx tsc --noEmit` in `tests/next_test`:**0 errors**(修复前 4 errors)
|
|
104
|
+
- `npx vitest run`:68/69 通过(1 个 pre-existing failure,与本 bug 无关)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Bug Fix: `'backend' does not exist in type 'RemoteStorageOptions'`
|
|
2
|
+
|
|
3
|
+
## 现象
|
|
4
|
+
|
|
5
|
+
`tests/next_test` 中使用 `"bff-store": "file:../../dist"` 时,TypeScript 报错:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Object literal may only specify known properties, and 'backend' does not exist in type 'RemoteStorageOptions'.ts(2353)
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 根因
|
|
12
|
+
|
|
13
|
+
### 因素一(主因):dist/package.json 路径错误
|
|
14
|
+
|
|
15
|
+
根目录 `package.json` 的 `main`/`module`/`types`/`exports` 字段路径都带有 `./dist/` 前缀:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"exports": { ".": { "types": "./dist/index.d.ts", ... } }
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
当通过 `"file:../../dist"` 本地安装时,包根目录就是 `dist/` 本身,`./dist/index.d.ts` 解析为 `dist/dist/index.d.ts` —— 该文件不存在。
|
|
23
|
+
|
|
24
|
+
| 场景 | 包根目录 | `./dist/index.d.ts` 解析结果 | 存在? |
|
|
25
|
+
|------|----------|---------------------------|------|
|
|
26
|
+
| npm 发布 | 项目根 | `项目根/dist/index.d.ts` | 是 |
|
|
27
|
+
| `file:../../dist` | `dist/` | `dist/dist/index.d.ts` | 否 |
|
|
28
|
+
|
|
29
|
+
### 因素二(助推):全局安装了旧版本
|
|
30
|
+
|
|
31
|
+
本地 symlink 解析失败后,TypeScript 沿目录树向上查找,在 `/Users/Admin/node_modules/bff-store/` 找到全局安装的旧版本(v0.1.1),该版本 `RemoteStorageOptions` 没有 `backend` 等新字段。
|
|
32
|
+
|
|
33
|
+
## 修复方案
|
|
34
|
+
|
|
35
|
+
1. **新增** `scripts/adapt-dist-package.js` — 构建后将 `dist/package.json` 中的路径从"相对于项目根"改写为"相对于 dist/":
|
|
36
|
+
- `dist/index.js` → `index.js`
|
|
37
|
+
- `./dist/index.d.ts` → `./index.d.ts`
|
|
38
|
+
- `./dist/storage/...` → `./storage/...`
|
|
39
|
+
- `./dist/server/...` → `./server/...`
|
|
40
|
+
|
|
41
|
+
2. **修改** `package.json` 的 `build` 脚本:
|
|
42
|
+
```
|
|
43
|
+
"build": "tsup && cp package.json dist/package.json && node scripts/adapt-dist-package.js && for f in dist/*.d.mts; do cp \"$f\" \"${f%.d.mts}.d.ts\"; done"
|
|
44
|
+
```
|
|
45
|
+
完整流程:
|
|
46
|
+
- `tsup` — 构建到 dist/
|
|
47
|
+
- `cp package.json dist/package.json` — 复制配置到 dist
|
|
48
|
+
- `node scripts/adapt-dist-package.js` — 改写路径
|
|
49
|
+
- shell 循环 — 复制 `.d.mts` → `.d.ts`(主入口 tsup 只生成 `.d.mts`)
|
|
50
|
+
|
|
51
|
+
## 修复文件
|
|
52
|
+
|
|
53
|
+
| 文件 | 操作 |
|
|
54
|
+
|------|------|
|
|
55
|
+
| `scripts/adapt-dist-package.js` | 新增 — 路径改写脚本 |
|
|
56
|
+
| `package.json` | 修改 — build 脚本 |
|
|
57
|
+
| `tests/next_test/test-regression-remote-storage.ts` | 新增 — jsonl/mongodb/默认三种场景类型回归测试 |
|
|
58
|
+
| `tests/next_test/src/app/test-regression-store.tsx` | 新增 — createStore + storage 的 .tsx 回归测试 |
|
|
59
|
+
|
|
60
|
+
## 验证
|
|
61
|
+
|
|
62
|
+
- `npm run build`:构建成功,dist/package.json 路径正确
|
|
63
|
+
- `npx tsc --noEmit` in `tests/next_test`:0 errors
|