local-lite 0.0.19 → 0.0.20
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 +5 -1
- package/dist/actions/store.js +1 -1
- package/dist/core/client.d.ts +15 -0
- package/dist/core/client.js +46 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/keys/generator.d.ts +38 -0
- package/dist/keys/generator.js +136 -0
- package/dist/keys/index.d.ts +5 -0
- package/dist/keys/index.js +21 -0
- package/dist/keys/manager.d.ts +59 -0
- package/dist/keys/manager.js +127 -0
- package/dist/keys/signed-token.d.ts +18 -0
- package/dist/keys/signed-token.js +171 -0
- package/dist/keys/store.d.ts +23 -0
- package/dist/keys/store.js +109 -0
- package/dist/keys/types.d.ts +95 -0
- package/dist/keys/types.js +2 -0
- package/dist/kv/index.d.ts +3 -0
- package/dist/kv/index.js +19 -0
- package/dist/kv/manager.d.ts +52 -0
- package/dist/kv/manager.js +164 -0
- package/dist/kv/store.d.ts +13 -0
- package/dist/kv/store.js +85 -0
- package/dist/kv/types.d.ts +27 -0
- package/dist/kv/types.js +2 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +2 -0
- package/dist/react/kv.d.ts +9 -0
- package/dist/react/kv.js +53 -0
- package/dist/react/search.d.ts +19 -0
- package/dist/react/search.js +49 -0
- package/dist/reactive/event-emitter.d.ts +1 -0
- package/dist/reactive/event-emitter.js +9 -6
- package/dist/scheduler/cron.d.ts +6 -0
- package/dist/scheduler/cron.js +62 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.js +20 -0
- package/dist/scheduler/manager.d.ts +59 -0
- package/dist/scheduler/manager.js +186 -0
- package/dist/scheduler/store.d.ts +20 -0
- package/dist/scheduler/store.js +134 -0
- package/dist/scheduler/types.d.ts +44 -0
- package/dist/scheduler/types.js +2 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.js +19 -0
- package/dist/search/manager.d.ts +42 -0
- package/dist/search/manager.js +123 -0
- package/dist/search/store.d.ts +33 -0
- package/dist/search/store.js +137 -0
- package/dist/search/types.d.ts +49 -0
- package/dist/search/types.js +2 -0
- package/dist/storage/driver.d.ts +25 -0
- package/dist/storage/driver.js +92 -0
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +20 -0
- package/dist/storage/manager.d.ts +48 -0
- package/dist/storage/manager.js +149 -0
- package/dist/storage/store.d.ts +14 -0
- package/dist/storage/store.js +116 -0
- package/dist/storage/types.d.ts +44 -0
- package/dist/storage/types.js +2 -0
- package/package.json +26 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NodeFileDriver = exports.MemoryFileDriver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Universal in-memory driver (ideal for tests, memory caches, and browser storage fallback).
|
|
6
|
+
*/
|
|
7
|
+
class MemoryFileDriver {
|
|
8
|
+
files = new Map();
|
|
9
|
+
async write(key, data) {
|
|
10
|
+
this.files.set(key, new Uint8Array(data));
|
|
11
|
+
return `memory://${key}`;
|
|
12
|
+
}
|
|
13
|
+
async read(key) {
|
|
14
|
+
const file = this.files.get(key);
|
|
15
|
+
return file ? new Uint8Array(file) : null;
|
|
16
|
+
}
|
|
17
|
+
async delete(key) {
|
|
18
|
+
this.files.delete(key);
|
|
19
|
+
}
|
|
20
|
+
async exists(key) {
|
|
21
|
+
return this.files.has(key);
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.files.clear();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.MemoryFileDriver = MemoryFileDriver;
|
|
28
|
+
/**
|
|
29
|
+
* Node.js filesystem driver for writing blobs to disk.
|
|
30
|
+
*/
|
|
31
|
+
class NodeFileDriver {
|
|
32
|
+
baseDir;
|
|
33
|
+
constructor(baseDir = './.local-lite-blobs') {
|
|
34
|
+
this.baseDir = baseDir;
|
|
35
|
+
}
|
|
36
|
+
async getFs() {
|
|
37
|
+
try {
|
|
38
|
+
const fs = await import('node:fs/promises');
|
|
39
|
+
const path = await import('node:path');
|
|
40
|
+
return { fs, path };
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('NodeFileDriver is only supported in Node.js environments.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async getResolvedPath(key) {
|
|
47
|
+
const { path } = await this.getFs();
|
|
48
|
+
// sanitize key to prevent directory traversal
|
|
49
|
+
const safeKey = key.replace(/\.\./g, '_');
|
|
50
|
+
return path.resolve(this.baseDir, safeKey);
|
|
51
|
+
}
|
|
52
|
+
async write(key, data) {
|
|
53
|
+
const { fs, path } = await this.getFs();
|
|
54
|
+
const filePath = await this.getResolvedPath(key);
|
|
55
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
56
|
+
await fs.writeFile(filePath, data);
|
|
57
|
+
return filePath;
|
|
58
|
+
}
|
|
59
|
+
async read(key) {
|
|
60
|
+
const { fs } = await this.getFs();
|
|
61
|
+
const filePath = await this.getResolvedPath(key);
|
|
62
|
+
try {
|
|
63
|
+
const buffer = await fs.readFile(filePath);
|
|
64
|
+
return new Uint8Array(buffer);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async delete(key) {
|
|
71
|
+
const { fs } = await this.getFs();
|
|
72
|
+
const filePath = await this.getResolvedPath(key);
|
|
73
|
+
try {
|
|
74
|
+
await fs.unlink(filePath);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// file might already be removed
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async exists(key) {
|
|
81
|
+
const { fs } = await this.getFs();
|
|
82
|
+
const filePath = await this.getResolvedPath(key);
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(filePath);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.NodeFileDriver = NodeFileDriver;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types.js"), exports);
|
|
18
|
+
__exportStar(require("./driver.js"), exports);
|
|
19
|
+
__exportStar(require("./store.js"), exports);
|
|
20
|
+
__exportStar(require("./manager.js"), exports);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { BlobRecord, BlobStorageManagerOptions, BlobUploadStatus, FileDriver, RemoteStorageUploader, SaveBlobOptions } from './types.js';
|
|
3
|
+
export declare class BlobStorageManager {
|
|
4
|
+
private db;
|
|
5
|
+
private driver;
|
|
6
|
+
private uploader?;
|
|
7
|
+
private network?;
|
|
8
|
+
private tableName;
|
|
9
|
+
constructor(db: DBExecutable, options?: BlobStorageManagerOptions);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
setDriver(driver: FileDriver): void;
|
|
12
|
+
setUploader(uploader: RemoteStorageUploader): void;
|
|
13
|
+
/**
|
|
14
|
+
* Saves a binary blob or file locally. Records metadata in SQLite and writes binary to driver.
|
|
15
|
+
* If online and uploader configured, can immediately trigger upload.
|
|
16
|
+
*/
|
|
17
|
+
save(key: string, data: Uint8Array | string, options?: SaveBlobOptions): Promise<BlobRecord>;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves blob record and raw binary data.
|
|
20
|
+
*/
|
|
21
|
+
get(key: string): Promise<{
|
|
22
|
+
record: BlobRecord;
|
|
23
|
+
data: Uint8Array;
|
|
24
|
+
} | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Reads raw binary data for key.
|
|
27
|
+
*/
|
|
28
|
+
getData(key: string): Promise<Uint8Array | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves blob metadata record.
|
|
31
|
+
*/
|
|
32
|
+
getRecord(key: string): Promise<BlobRecord | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Lists blob metadata records, optionally filtered by upload status.
|
|
35
|
+
*/
|
|
36
|
+
list(status?: BlobUploadStatus): Promise<BlobRecord[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Deletes the blob binary and metadata record.
|
|
39
|
+
*/
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Flushes and syncs pending or failed uploads to the remote storage uploader.
|
|
43
|
+
*/
|
|
44
|
+
syncPending(): Promise<{
|
|
45
|
+
uploaded: number;
|
|
46
|
+
failed: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BlobStorageManager = void 0;
|
|
4
|
+
const sha2_js_1 = require("@noble/hashes/sha2.js");
|
|
5
|
+
const utils_js_1 = require("@noble/hashes/utils.js");
|
|
6
|
+
const uuid_js_1 = require("../utils/uuid.js");
|
|
7
|
+
const store_js_1 = require("./store.js");
|
|
8
|
+
const driver_js_1 = require("./driver.js");
|
|
9
|
+
class BlobStorageManager {
|
|
10
|
+
db;
|
|
11
|
+
driver;
|
|
12
|
+
uploader;
|
|
13
|
+
network;
|
|
14
|
+
tableName;
|
|
15
|
+
constructor(db, options = {}) {
|
|
16
|
+
this.db = db;
|
|
17
|
+
this.driver = options.driver ?? new driver_js_1.MemoryFileDriver();
|
|
18
|
+
this.uploader = options.uploader;
|
|
19
|
+
this.network = options.network;
|
|
20
|
+
this.tableName = options.tableName ?? store_js_1.DEFAULT_BLOBS_TABLE;
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
await store_js_1.BlobStore.ensureSchema(this.db, this.tableName);
|
|
24
|
+
}
|
|
25
|
+
setDriver(driver) {
|
|
26
|
+
this.driver = driver;
|
|
27
|
+
}
|
|
28
|
+
setUploader(uploader) {
|
|
29
|
+
this.uploader = uploader;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Saves a binary blob or file locally. Records metadata in SQLite and writes binary to driver.
|
|
33
|
+
* If online and uploader configured, can immediately trigger upload.
|
|
34
|
+
*/
|
|
35
|
+
async save(key, data, options = {}) {
|
|
36
|
+
const bytes = typeof data === 'string' ? (0, utils_js_1.utf8ToBytes)(data) : data;
|
|
37
|
+
const hash = (0, utils_js_1.bytesToHex)((0, sha2_js_1.sha256)(bytes));
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const localPath = await this.driver.write(key, bytes);
|
|
40
|
+
const existing = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
41
|
+
const id = existing ? existing.id : (0, uuid_js_1.generateUUID)();
|
|
42
|
+
const record = {
|
|
43
|
+
id,
|
|
44
|
+
key,
|
|
45
|
+
name: options.name || key.split('/').pop() || key,
|
|
46
|
+
mimeType: options.mimeType || 'application/octet-stream',
|
|
47
|
+
size: bytes.byteLength,
|
|
48
|
+
localPath,
|
|
49
|
+
remoteUrl: existing?.remoteUrl || null,
|
|
50
|
+
status: 'PENDING',
|
|
51
|
+
sha256: hash,
|
|
52
|
+
metadata: options.metadata || existing?.metadata,
|
|
53
|
+
createdAt: existing ? existing.createdAt : now,
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
};
|
|
56
|
+
await store_js_1.BlobStore.insertOrUpdate(this.db, this.tableName, record);
|
|
57
|
+
if (options.autoUpload !== false && this.uploader) {
|
|
58
|
+
const isOnline = this.network ? await this.network.isOnline() : true;
|
|
59
|
+
if (isOnline) {
|
|
60
|
+
// Fire and update status
|
|
61
|
+
try {
|
|
62
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'UPLOADING');
|
|
63
|
+
const res = await this.uploader.upload(record, bytes);
|
|
64
|
+
record.status = 'UPLOADED';
|
|
65
|
+
record.remoteUrl = res.remoteUrl;
|
|
66
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'UPLOADED', res.remoteUrl);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
record.status = 'FAILED';
|
|
70
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, id, 'FAILED');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return record;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves blob record and raw binary data.
|
|
78
|
+
*/
|
|
79
|
+
async get(key) {
|
|
80
|
+
const record = await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
81
|
+
if (!record)
|
|
82
|
+
return null;
|
|
83
|
+
const data = await this.driver.read(key);
|
|
84
|
+
if (!data)
|
|
85
|
+
return null;
|
|
86
|
+
return { record, data };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reads raw binary data for key.
|
|
90
|
+
*/
|
|
91
|
+
async getData(key) {
|
|
92
|
+
return await this.driver.read(key);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Retrieves blob metadata record.
|
|
96
|
+
*/
|
|
97
|
+
async getRecord(key) {
|
|
98
|
+
return await store_js_1.BlobStore.getByKey(this.db, this.tableName, key);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Lists blob metadata records, optionally filtered by upload status.
|
|
102
|
+
*/
|
|
103
|
+
async list(status) {
|
|
104
|
+
return await store_js_1.BlobStore.list(this.db, this.tableName, status);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Deletes the blob binary and metadata record.
|
|
108
|
+
*/
|
|
109
|
+
async delete(key) {
|
|
110
|
+
await this.driver.delete(key);
|
|
111
|
+
await store_js_1.BlobStore.deleteByKey(this.db, this.tableName, key);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Flushes and syncs pending or failed uploads to the remote storage uploader.
|
|
115
|
+
*/
|
|
116
|
+
async syncPending() {
|
|
117
|
+
if (!this.uploader) {
|
|
118
|
+
return { uploaded: 0, failed: 0 };
|
|
119
|
+
}
|
|
120
|
+
const isOnline = this.network ? await this.network.isOnline() : true;
|
|
121
|
+
if (!isOnline) {
|
|
122
|
+
return { uploaded: 0, failed: 0 };
|
|
123
|
+
}
|
|
124
|
+
const pending = await store_js_1.BlobStore.list(this.db, this.tableName, 'PENDING');
|
|
125
|
+
const failedList = await store_js_1.BlobStore.list(this.db, this.tableName, 'FAILED');
|
|
126
|
+
const queue = [...pending, ...failedList];
|
|
127
|
+
let uploaded = 0;
|
|
128
|
+
let failed = 0;
|
|
129
|
+
for (const item of queue) {
|
|
130
|
+
try {
|
|
131
|
+
const data = await this.driver.read(item.key);
|
|
132
|
+
if (!data) {
|
|
133
|
+
failed++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, item.id, 'UPLOADING');
|
|
137
|
+
const res = await this.uploader.upload(item, data);
|
|
138
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, item.id, 'UPLOADED', res.remoteUrl);
|
|
139
|
+
uploaded++;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
await store_js_1.BlobStore.updateStatus(this.db, this.tableName, item.id, 'FAILED');
|
|
143
|
+
failed++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { uploaded, failed };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.BlobStorageManager = BlobStorageManager;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DBExecutable } from '../core/schema-manager.js';
|
|
2
|
+
import { BlobRecord, BlobUploadStatus } from './types.js';
|
|
3
|
+
export declare const DEFAULT_BLOBS_TABLE = "_local_blobs";
|
|
4
|
+
export declare function mapBlobRow(r: any): BlobRecord;
|
|
5
|
+
export declare class BlobStore {
|
|
6
|
+
static ensureSchema(db: DBExecutable, tableName?: string): Promise<void>;
|
|
7
|
+
static insertOrUpdate(db: DBExecutable, tableName: string | undefined, record: BlobRecord): Promise<void>;
|
|
8
|
+
static getByKey(db: DBExecutable, tableName: string | undefined, key: string): Promise<BlobRecord | null>;
|
|
9
|
+
static getById(db: DBExecutable, tableName: string | undefined, id: string): Promise<BlobRecord | null>;
|
|
10
|
+
static list(db: DBExecutable, tableName?: string, status?: BlobUploadStatus): Promise<BlobRecord[]>;
|
|
11
|
+
static updateStatus(db: DBExecutable, tableName: string | undefined, id: string, status: BlobUploadStatus, remoteUrl?: string | null): Promise<void>;
|
|
12
|
+
static deleteByKey(db: DBExecutable, tableName: string | undefined, key: string): Promise<void>;
|
|
13
|
+
static clear(db: DBExecutable, tableName?: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BlobStore = exports.DEFAULT_BLOBS_TABLE = void 0;
|
|
4
|
+
exports.mapBlobRow = mapBlobRow;
|
|
5
|
+
exports.DEFAULT_BLOBS_TABLE = '_local_blobs';
|
|
6
|
+
function mapBlobRow(r) {
|
|
7
|
+
let meta;
|
|
8
|
+
if (r.metadata) {
|
|
9
|
+
try {
|
|
10
|
+
meta = typeof r.metadata === 'string' ? JSON.parse(r.metadata) : r.metadata;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
meta = undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
id: r.id,
|
|
18
|
+
key: r.key,
|
|
19
|
+
name: r.name,
|
|
20
|
+
mimeType: r.mime_type,
|
|
21
|
+
size: Number(r.size || 0),
|
|
22
|
+
localPath: r.local_path,
|
|
23
|
+
remoteUrl: r.remote_url || null,
|
|
24
|
+
status: r.status || 'PENDING',
|
|
25
|
+
sha256: r.sha256 || null,
|
|
26
|
+
metadata: meta,
|
|
27
|
+
createdAt: Number(r.created_at || 0),
|
|
28
|
+
updatedAt: Number(r.updated_at || 0),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
class BlobStore {
|
|
32
|
+
static async ensureSchema(db, tableName = exports.DEFAULT_BLOBS_TABLE) {
|
|
33
|
+
await db.execute(`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
key TEXT UNIQUE NOT NULL,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
mime_type TEXT NOT NULL,
|
|
38
|
+
size INTEGER NOT NULL,
|
|
39
|
+
local_path TEXT NOT NULL,
|
|
40
|
+
remote_url TEXT,
|
|
41
|
+
status TEXT NOT NULL,
|
|
42
|
+
sha256 TEXT,
|
|
43
|
+
metadata TEXT,
|
|
44
|
+
created_at INTEGER NOT NULL,
|
|
45
|
+
updated_at INTEGER NOT NULL
|
|
46
|
+
);`);
|
|
47
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_status ON ${tableName} (status);`);
|
|
48
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS idx_${tableName}_key ON ${tableName} (key);`);
|
|
49
|
+
}
|
|
50
|
+
static async insertOrUpdate(db, tableName = exports.DEFAULT_BLOBS_TABLE, record) {
|
|
51
|
+
const metaStr = record.metadata ? JSON.stringify(record.metadata) : null;
|
|
52
|
+
await db.execute(`INSERT INTO ${tableName} (id, key, name, mime_type, size, local_path, remote_url, status, sha256, metadata, created_at, updated_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
55
|
+
name = excluded.name,
|
|
56
|
+
mime_type = excluded.mime_type,
|
|
57
|
+
size = excluded.size,
|
|
58
|
+
local_path = excluded.local_path,
|
|
59
|
+
remote_url = excluded.remote_url,
|
|
60
|
+
status = excluded.status,
|
|
61
|
+
sha256 = excluded.sha256,
|
|
62
|
+
metadata = excluded.metadata,
|
|
63
|
+
updated_at = excluded.updated_at;`, [
|
|
64
|
+
record.id,
|
|
65
|
+
record.key,
|
|
66
|
+
record.name,
|
|
67
|
+
record.mimeType,
|
|
68
|
+
record.size,
|
|
69
|
+
record.localPath,
|
|
70
|
+
record.remoteUrl ?? null,
|
|
71
|
+
record.status,
|
|
72
|
+
record.sha256 ?? null,
|
|
73
|
+
metaStr,
|
|
74
|
+
record.createdAt,
|
|
75
|
+
record.updatedAt,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
static async getByKey(db, tableName = exports.DEFAULT_BLOBS_TABLE, key) {
|
|
79
|
+
const rows = await db.execute(`SELECT * FROM ${tableName} WHERE key = ? LIMIT 1;`, [key]);
|
|
80
|
+
if (!rows || rows.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
return mapBlobRow(rows[0]);
|
|
83
|
+
}
|
|
84
|
+
static async getById(db, tableName = exports.DEFAULT_BLOBS_TABLE, id) {
|
|
85
|
+
const rows = await db.execute(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1;`, [id]);
|
|
86
|
+
if (!rows || rows.length === 0)
|
|
87
|
+
return null;
|
|
88
|
+
return mapBlobRow(rows[0]);
|
|
89
|
+
}
|
|
90
|
+
static async list(db, tableName = exports.DEFAULT_BLOBS_TABLE, status) {
|
|
91
|
+
let rows;
|
|
92
|
+
if (status) {
|
|
93
|
+
rows = await db.execute(`SELECT * FROM ${tableName} WHERE status = ? ORDER BY created_at DESC;`, [status]);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
rows = await db.execute(`SELECT * FROM ${tableName} ORDER BY created_at DESC;`);
|
|
97
|
+
}
|
|
98
|
+
return (rows || []).map(mapBlobRow);
|
|
99
|
+
}
|
|
100
|
+
static async updateStatus(db, tableName = exports.DEFAULT_BLOBS_TABLE, id, status, remoteUrl) {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
if (remoteUrl !== undefined) {
|
|
103
|
+
await db.execute(`UPDATE ${tableName} SET status = ?, remote_url = ?, updated_at = ? WHERE id = ?;`, [status, remoteUrl, now, id]);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await db.execute(`UPDATE ${tableName} SET status = ?, updated_at = ? WHERE id = ?;`, [status, now, id]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
static async deleteByKey(db, tableName = exports.DEFAULT_BLOBS_TABLE, key) {
|
|
110
|
+
await db.execute(`DELETE FROM ${tableName} WHERE key = ?;`, [key]);
|
|
111
|
+
}
|
|
112
|
+
static async clear(db, tableName = exports.DEFAULT_BLOBS_TABLE) {
|
|
113
|
+
await db.execute(`DELETE FROM ${tableName};`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.BlobStore = BlobStore;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type BlobUploadStatus = 'PENDING' | 'UPLOADING' | 'UPLOADED' | 'FAILED';
|
|
2
|
+
export interface BlobRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
size: number;
|
|
8
|
+
localPath: string;
|
|
9
|
+
remoteUrl?: string | null;
|
|
10
|
+
status: BlobUploadStatus;
|
|
11
|
+
sha256?: string | null;
|
|
12
|
+
metadata?: Record<string, any>;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
}
|
|
16
|
+
export interface SaveBlobOptions {
|
|
17
|
+
name?: string;
|
|
18
|
+
mimeType?: string;
|
|
19
|
+
metadata?: Record<string, any>;
|
|
20
|
+
/**
|
|
21
|
+
* If true and an uploader is configured and online, triggers immediate upload.
|
|
22
|
+
* Default: true
|
|
23
|
+
*/
|
|
24
|
+
autoUpload?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface FileDriver {
|
|
27
|
+
write(key: string, data: Uint8Array): Promise<string>;
|
|
28
|
+
read(key: string): Promise<Uint8Array | null>;
|
|
29
|
+
delete(key: string): Promise<void>;
|
|
30
|
+
exists(key: string): Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
export interface RemoteStorageUploader {
|
|
33
|
+
upload(blob: BlobRecord, data: Uint8Array): Promise<{
|
|
34
|
+
remoteUrl: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface BlobStorageManagerOptions {
|
|
38
|
+
driver?: FileDriver;
|
|
39
|
+
uploader?: RemoteStorageUploader;
|
|
40
|
+
network?: {
|
|
41
|
+
isOnline(): boolean | Promise<boolean>;
|
|
42
|
+
};
|
|
43
|
+
tableName?: string;
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -105,6 +105,31 @@
|
|
|
105
105
|
"types": "./dist/actions/index.d.ts",
|
|
106
106
|
"import": "./dist/actions/index.js",
|
|
107
107
|
"default": "./dist/actions/index.js"
|
|
108
|
+
},
|
|
109
|
+
"./keys": {
|
|
110
|
+
"types": "./dist/keys/index.d.ts",
|
|
111
|
+
"import": "./dist/keys/index.js",
|
|
112
|
+
"default": "./dist/keys/index.js"
|
|
113
|
+
},
|
|
114
|
+
"./kv": {
|
|
115
|
+
"types": "./dist/kv/index.d.ts",
|
|
116
|
+
"import": "./dist/kv/index.js",
|
|
117
|
+
"default": "./dist/kv/index.js"
|
|
118
|
+
},
|
|
119
|
+
"./search": {
|
|
120
|
+
"types": "./dist/search/index.d.ts",
|
|
121
|
+
"import": "./dist/search/index.js",
|
|
122
|
+
"default": "./dist/search/index.js"
|
|
123
|
+
},
|
|
124
|
+
"./storage": {
|
|
125
|
+
"types": "./dist/storage/index.d.ts",
|
|
126
|
+
"import": "./dist/storage/index.js",
|
|
127
|
+
"default": "./dist/storage/index.js"
|
|
128
|
+
},
|
|
129
|
+
"./scheduler": {
|
|
130
|
+
"types": "./dist/scheduler/index.d.ts",
|
|
131
|
+
"import": "./dist/scheduler/index.js",
|
|
132
|
+
"default": "./dist/scheduler/index.js"
|
|
108
133
|
}
|
|
109
134
|
},
|
|
110
135
|
"scripts": {
|