gh-as-db 1.0.1 → 1.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/README.md
CHANGED
|
@@ -71,7 +71,7 @@ const db = new GitHubDB({
|
|
|
71
71
|
accessToken: string; // GitHub PAT with 'repo' scope
|
|
72
72
|
owner: string; // Repository owner
|
|
73
73
|
repo: string; // Repository name
|
|
74
|
-
|
|
74
|
+
cacheTTL?: number; // Optional: Cache TTL in ms. Default is 0 (strict consistency).
|
|
75
75
|
});
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -155,7 +155,8 @@ For small projects, side-projects, or internal tools, setting up a database serv
|
|
|
155
155
|
|
|
156
156
|
## Performance
|
|
157
157
|
|
|
158
|
-
- **Caching**:
|
|
158
|
+
- **Consistent Caching**: Uses **Conditional GET** (`If-None-Match`) to ensure data is always up-to-date even across multiple instances (e.g., serverless), while minimizing API costs.
|
|
159
|
+
- **Write-Through**: Updates the local cache immediately after a write, preventing 404s during redirects.
|
|
159
160
|
- **Indexing**: Automatic in-memory indexing on all fields makes querying fast even as data grows.
|
|
160
161
|
- **Optimistic Concurrency**: Uses Git SHAs to ensure that you don't overwrite changes made by another client.
|
|
161
162
|
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4,14 +4,14 @@ export class MemoryCacheProvider {
|
|
|
4
4
|
const entry = this.cache.get(key);
|
|
5
5
|
if (!entry)
|
|
6
6
|
return null;
|
|
7
|
-
if (entry.expiry !== null && Date.now()
|
|
7
|
+
if (entry.expiry !== null && Date.now() >= entry.expiry) {
|
|
8
8
|
this.cache.delete(key);
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
11
|
return entry.data;
|
|
12
12
|
}
|
|
13
13
|
set(key, value, ttl) {
|
|
14
|
-
const expiry = ttl ? Date.now() + ttl : null;
|
|
14
|
+
const expiry = ttl !== undefined ? Date.now() + ttl : null;
|
|
15
15
|
this.cache.set(key, { data: value, expiry });
|
|
16
16
|
}
|
|
17
17
|
delete(key) {
|
|
@@ -4,6 +4,8 @@ export declare class GitHubStorageProvider implements IStorageProvider {
|
|
|
4
4
|
private config;
|
|
5
5
|
private octokit;
|
|
6
6
|
private cache;
|
|
7
|
+
private staleCache;
|
|
8
|
+
private readonly DEFAULT_TTL;
|
|
7
9
|
constructor(config: GitHubDBConfig, cache?: ICacheProvider);
|
|
8
10
|
testConnection(): Promise<boolean>;
|
|
9
11
|
exists(path: string): Promise<boolean>;
|
|
@@ -5,10 +5,12 @@ export class GitHubStorageProvider {
|
|
|
5
5
|
config;
|
|
6
6
|
octokit;
|
|
7
7
|
cache;
|
|
8
|
+
staleCache = new Map();
|
|
9
|
+
DEFAULT_TTL = 0; // Default to 0 for consistency
|
|
8
10
|
constructor(config, cache) {
|
|
9
11
|
this.config = config;
|
|
10
12
|
this.octokit = new Octokit({
|
|
11
|
-
auth: config.accessToken,
|
|
13
|
+
auth: this.config.accessToken,
|
|
12
14
|
});
|
|
13
15
|
this.cache = cache || new MemoryCacheProvider();
|
|
14
16
|
}
|
|
@@ -25,6 +27,9 @@ export class GitHubStorageProvider {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
async exists(path) {
|
|
30
|
+
if (this.cache.get(path) || this.staleCache.has(path)) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
28
33
|
try {
|
|
29
34
|
await this.octokit.repos.getContent({
|
|
30
35
|
owner: this.config.owner,
|
|
@@ -41,45 +46,69 @@ export class GitHubStorageProvider {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
async readJson(path) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
// 1. Check fresh cache (blind trust)
|
|
50
|
+
const fresh = this.cache.get(path);
|
|
51
|
+
if (fresh) {
|
|
52
|
+
return fresh;
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// 2. Check stale cache for conditional request
|
|
55
|
+
const stale = this.staleCache.get(path);
|
|
56
|
+
try {
|
|
57
|
+
const response = await this.octokit.repos.getContent({
|
|
58
|
+
owner: this.config.owner,
|
|
59
|
+
repo: this.config.repo,
|
|
60
|
+
path,
|
|
61
|
+
headers: stale ? { "if-none-match": `"${stale.sha}"` } : {},
|
|
62
|
+
});
|
|
63
|
+
if (Array.isArray(response.data)) {
|
|
64
|
+
throw new Error("Path is a directory, not a file");
|
|
65
|
+
}
|
|
66
|
+
if (!("content" in response.data) || !("sha" in response.data)) {
|
|
67
|
+
throw new Error("No content or SHA in response");
|
|
68
|
+
}
|
|
69
|
+
const content = Buffer.from(response.data.content, "base64").toString("utf-8");
|
|
70
|
+
const result = {
|
|
71
|
+
data: JSON.parse(content),
|
|
72
|
+
sha: response.data.sha,
|
|
73
|
+
};
|
|
74
|
+
// Update both caches
|
|
75
|
+
const ttl = this.config.cacheTTL ?? this.DEFAULT_TTL;
|
|
76
|
+
this.cache.set(path, result, ttl);
|
|
77
|
+
this.staleCache.set(path, result);
|
|
78
|
+
return result;
|
|
55
79
|
}
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error.status === 304 && stale) {
|
|
82
|
+
// Re-cache as fresh and return stale data
|
|
83
|
+
const ttl = this.config.cacheTTL ?? this.DEFAULT_TTL;
|
|
84
|
+
this.cache.set(path, stale, ttl);
|
|
85
|
+
return stale;
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
58
88
|
}
|
|
59
|
-
const content = Buffer.from(response.data.content, "base64").toString("utf-8");
|
|
60
|
-
const result = {
|
|
61
|
-
data: JSON.parse(content),
|
|
62
|
-
sha: response.data.sha,
|
|
63
|
-
};
|
|
64
|
-
this.cache.set(path, result);
|
|
65
|
-
return result;
|
|
66
89
|
}
|
|
67
90
|
async writeJson(path, content, message, sha) {
|
|
68
91
|
let internalSha = sha;
|
|
69
92
|
if (!internalSha) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
repo: this.config.repo,
|
|
74
|
-
path,
|
|
75
|
-
});
|
|
76
|
-
if (!Array.isArray(existing.data) && "sha" in existing.data) {
|
|
77
|
-
internalSha = existing.data.sha;
|
|
78
|
-
}
|
|
93
|
+
const cached = this.cache.get(path) || this.staleCache.get(path);
|
|
94
|
+
if (cached) {
|
|
95
|
+
internalSha = cached.sha;
|
|
79
96
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
97
|
+
else {
|
|
98
|
+
try {
|
|
99
|
+
const existing = await this.octokit.repos.getContent({
|
|
100
|
+
owner: this.config.owner,
|
|
101
|
+
repo: this.config.repo,
|
|
102
|
+
path,
|
|
103
|
+
});
|
|
104
|
+
if (!Array.isArray(existing.data) && "sha" in existing.data) {
|
|
105
|
+
internalSha = existing.data.sha;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.status !== 404) {
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
83
112
|
}
|
|
84
113
|
}
|
|
85
114
|
}
|
|
@@ -93,7 +122,10 @@ export class GitHubStorageProvider {
|
|
|
93
122
|
sha: internalSha,
|
|
94
123
|
});
|
|
95
124
|
const newSha = response.data.content?.sha || "";
|
|
96
|
-
|
|
125
|
+
const result = { data: content, sha: newSha };
|
|
126
|
+
const ttl = this.config.cacheTTL ?? this.DEFAULT_TTL;
|
|
127
|
+
this.cache.set(path, result, ttl);
|
|
128
|
+
this.staleCache.set(path, result);
|
|
97
129
|
return newSha;
|
|
98
130
|
}
|
|
99
131
|
catch (error) {
|
package/dist/ui/collection.js
CHANGED
|
@@ -139,6 +139,9 @@ export class Collection {
|
|
|
139
139
|
case "lte":
|
|
140
140
|
return val <= filter.value;
|
|
141
141
|
case "contains":
|
|
142
|
+
if (Array.isArray(val)) {
|
|
143
|
+
return val.includes(filter.value);
|
|
144
|
+
}
|
|
142
145
|
return (typeof val === "string" && val.includes(filter.value));
|
|
143
146
|
case "in":
|
|
144
147
|
return Array.isArray(filter.value) && filter.value.includes(val);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-as-db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Use a private GitHub repository as a database for your application.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -61,4 +61,4 @@
|
|
|
61
61
|
"commander": "^13.1.0",
|
|
62
62
|
"enquirer": "^2.4.1"
|
|
63
63
|
}
|
|
64
|
-
}
|
|
64
|
+
}
|