gh-as-db 1.0.0 → 1.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/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
- branch?: string; // Optional: branch to use (defaults to 'main')
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**: Data is fetched once and cached in memory. Subsequent reads are near-instant.
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
 
@@ -2,6 +2,7 @@ export interface GitHubDBConfig {
2
2
  accessToken: string;
3
3
  owner: string;
4
4
  repo: string;
5
+ cacheTTL?: number;
5
6
  }
6
7
  export type Schema = Record<string, any>;
7
8
  export type MiddlewareOperation = "create" | "update" | "read" | "delete";
package/dist/index.d.ts CHANGED
@@ -1 +1,4 @@
1
- export declare const version = "0.1.0";
1
+ export { GitHubDB } from "./ui/github-db.js";
2
+ export { Collection } from "./ui/collection.js";
3
+ export * from "./core/types.js";
4
+ export declare const version = "1.0.1";
package/dist/index.js CHANGED
@@ -1 +1,4 @@
1
- export const version = "0.1.0";
1
+ export { GitHubDB } from "./ui/github-db.js";
2
+ export { Collection } from "./ui/collection.js";
3
+ export * from "./core/types.js";
4
+ export const version = "1.0.1";
@@ -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() > entry.expiry) {
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
- const cached = this.cache.get(path);
45
- if (cached) {
46
- return cached;
49
+ // 1. Check fresh cache (blind trust)
50
+ const fresh = this.cache.get(path);
51
+ if (fresh) {
52
+ return fresh;
47
53
  }
48
- const response = await this.octokit.repos.getContent({
49
- owner: this.config.owner,
50
- repo: this.config.repo,
51
- path,
52
- });
53
- if (Array.isArray(response.data)) {
54
- throw new Error("Path is a directory, not a file");
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
- if (!("content" in response.data) || !("sha" in response.data)) {
57
- throw new Error("No content or SHA in response");
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
- try {
71
- const existing = await this.octokit.repos.getContent({
72
- owner: this.config.owner,
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
- catch (error) {
81
- if (error.status !== 404) {
82
- throw error;
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
- this.cache.delete(path);
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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "gh-as-db",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Use a private GitHub repository as a database for your application.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
7
8
  "types": "./dist/index.d.ts",
8
9
  "exports": {
9
10
  ".": {