agentic-documents 0.0.0-stage → 0.2.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +237 -2
- package/SECURITY.md +23 -0
- package/SKILL.md +35 -0
- package/dist/chunk-5Y66UCTE.js +7 -0
- package/dist/chunk-FZA5LQV3.js +150 -0
- package/dist/chunk-GYQFTF2E.js +389 -0
- package/dist/chunk-IU3YIQ4Y.js +232 -0
- package/dist/chunk-R5XUTBC3.js +177 -0
- package/dist/chunk-TFCRPEEM.js +224 -0
- package/dist/chunk-YBBQNCMC.js +744 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +165 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +44 -0
- package/dist/live-worker.d.ts +20 -0
- package/dist/live-worker.js +27 -0
- package/dist/live.d.ts +73 -0
- package/dist/live.js +12 -0
- package/dist/mcp-P2C75IIG.js +40 -0
- package/dist/service-BKkxEnaJ.d.ts +181 -0
- package/dist/worker.d.ts +50 -0
- package/dist/worker.js +15 -0
- package/llms.txt +15 -0
- package/package.json +104 -4
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_LIVE_BYTES
|
|
3
|
+
} from "./chunk-FZA5LQV3.js";
|
|
4
|
+
import {
|
|
5
|
+
AgenticDoc,
|
|
6
|
+
DOC_ERROR_CODES,
|
|
7
|
+
DocError,
|
|
8
|
+
PRINCIPAL_KINDS
|
|
9
|
+
} from "./chunk-YBBQNCMC.js";
|
|
10
|
+
|
|
11
|
+
// src/live.ts
|
|
12
|
+
import * as Y from "yjs";
|
|
13
|
+
import { SyncClient } from "agentic-realtime/client";
|
|
14
|
+
async function refusal(response, fallback) {
|
|
15
|
+
const { error } = await response.json().catch(() => ({}));
|
|
16
|
+
const code = DOC_ERROR_CODES.find((known) => known === error?.code) ?? (response.status >= 500 ? "MISCONFIGURED" : "INVALID_INPUT");
|
|
17
|
+
return new DocError(code, typeof error?.message === "string" ? error.message : `${fallback} (${response.status})`);
|
|
18
|
+
}
|
|
19
|
+
var base = (server) => server.replace(/\/$/, "");
|
|
20
|
+
async function whoAmI(server, token) {
|
|
21
|
+
const response = await fetch(`${base(server)}/api/v1/me`, { headers: { authorization: `Bearer ${token}` } }).catch((error) => {
|
|
22
|
+
throw unreachable(error);
|
|
23
|
+
});
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
if (response.status === 401 || response.status === 403) throw new DocError("FORBIDDEN", `the server refused the token (${response.status})`);
|
|
26
|
+
throw await refusal(response, "the server could not say who the token is");
|
|
27
|
+
}
|
|
28
|
+
const body = await response.json();
|
|
29
|
+
const { kind, name, scopes, role } = body;
|
|
30
|
+
if (!PRINCIPAL_KINDS.includes(kind) || typeof name !== "string" || role !== "editor" && role !== "viewer") {
|
|
31
|
+
throw new DocError("INVALID_INPUT", "the server sent an identity this client does not know");
|
|
32
|
+
}
|
|
33
|
+
const granted = Array.isArray(scopes) ? scopes.filter((s) => typeof s === "string") : [];
|
|
34
|
+
return { author: { kind, name, ...granted.length ? { scopes: granted } : {} }, role };
|
|
35
|
+
}
|
|
36
|
+
var LiveDoc = class _LiveDoc {
|
|
37
|
+
constructor(doc, me, client, api, refusedAtStart) {
|
|
38
|
+
this.doc = doc;
|
|
39
|
+
this.me = me;
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.api = api;
|
|
42
|
+
this.refusedAtStart = refusedAtStart;
|
|
43
|
+
}
|
|
44
|
+
doc;
|
|
45
|
+
me;
|
|
46
|
+
client;
|
|
47
|
+
api;
|
|
48
|
+
refusedAtStart;
|
|
49
|
+
static async join(options) {
|
|
50
|
+
const me = await whoAmI(options.server, options.token);
|
|
51
|
+
const url = new URL(`${base(options.server)}/api/v1/docs/${encodeURIComponent(options.id)}/ws`);
|
|
52
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
53
|
+
const client = await SyncClient.connect({
|
|
54
|
+
url: url.toString(),
|
|
55
|
+
headers: { authorization: `Bearer ${options.token}` },
|
|
56
|
+
presence: { name: options.name ?? me.author.name },
|
|
57
|
+
reconnect: options.reconnect
|
|
58
|
+
});
|
|
59
|
+
const api = (path, init = {}) => fetch(`${base(options.server)}/api/v1/docs/${encodeURIComponent(options.id)}${path}`, {
|
|
60
|
+
...init,
|
|
61
|
+
headers: { ...init.headers, authorization: `Bearer ${options.token}` }
|
|
62
|
+
});
|
|
63
|
+
try {
|
|
64
|
+
if (options.seed !== false) {
|
|
65
|
+
const stored = await api("/state");
|
|
66
|
+
if (stored.ok) {
|
|
67
|
+
const state = new Uint8Array(await stored.arrayBuffer());
|
|
68
|
+
if (state.length > MAX_LIVE_BYTES) throw new DocError("INVALID_INPUT", `the stored document is over ${MAX_LIVE_BYTES} bytes, the most a live room takes`);
|
|
69
|
+
Y.applyUpdate(client.doc, state);
|
|
70
|
+
} else if (stored.status !== 404) throw await refusal(stored, "could not read the stored document");
|
|
71
|
+
await client.flush();
|
|
72
|
+
if (me.role === "editor" && client.denied.length > 0) throw new DocError("FORBIDDEN", `the room refused the stored document: ${client.denied[0]}`);
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
await client.disconnect();
|
|
76
|
+
throw error instanceof DocError ? error : new DocError("INVALID_INPUT", `could not open the room: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
return new _LiveDoc(new AgenticDoc(client.doc, options.doc), me, client, api, client.denied.length);
|
|
79
|
+
}
|
|
80
|
+
/** Saves the room's state to the stored document, through the Blobs port. A viewer cannot save. */
|
|
81
|
+
async save() {
|
|
82
|
+
if (this.me.role === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
83
|
+
await this.client.flush();
|
|
84
|
+
const saved = await this.api("/checkpoint", { method: "POST", headers: { "content-type": "application/octet-stream" }, body: new Uint8Array(this.doc.encode()) });
|
|
85
|
+
if (!saved.ok) throw await refusal(saved, "the server did not save the document");
|
|
86
|
+
}
|
|
87
|
+
/** The other people and agents in the room. `machine` is true for an agent. */
|
|
88
|
+
get peers() {
|
|
89
|
+
return this.client.peers;
|
|
90
|
+
}
|
|
91
|
+
/** Why the room dropped an update, in order. */
|
|
92
|
+
get denied() {
|
|
93
|
+
return this.client.denied.slice(this.refusedAtStart);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Runs one change as the token's author and waits until the room has it. A viewer fails before the change.
|
|
97
|
+
* After a refusal from the room the local copy still holds the change. Leave and join again.
|
|
98
|
+
*/
|
|
99
|
+
async edit(run) {
|
|
100
|
+
if (this.me.role === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
|
|
101
|
+
const seen = this.denied.length;
|
|
102
|
+
const result = run(this.doc, this.me.author);
|
|
103
|
+
await this.client.flush();
|
|
104
|
+
if (this.denied.length > seen) throw new DocError("FORBIDDEN", `the room refused the edit: ${this.denied[seen]}`);
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
/** Waits until the room has every change made so far. */
|
|
108
|
+
flush() {
|
|
109
|
+
return this.client.flush();
|
|
110
|
+
}
|
|
111
|
+
async leave() {
|
|
112
|
+
await this.client.disconnect();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
function unreachable(error) {
|
|
116
|
+
if (error instanceof DocError) return error;
|
|
117
|
+
return new DocError("MISCONFIGURED", `could not reach the live server: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
}
|
|
119
|
+
var LiveService = class {
|
|
120
|
+
constructor(options) {
|
|
121
|
+
this.options = options;
|
|
122
|
+
}
|
|
123
|
+
options;
|
|
124
|
+
async join(id) {
|
|
125
|
+
try {
|
|
126
|
+
return await LiveDoc.join({ ...this.options, id });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw unreachable(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** The room is only a copy, so a new document is made in the store. A room seeds from it on the next join. */
|
|
132
|
+
async create(id, markdown) {
|
|
133
|
+
const response = await fetch(`${base(this.options.server)}/api/v1/docs`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { authorization: `Bearer ${this.options.token}`, "content-type": "application/json" },
|
|
136
|
+
body: JSON.stringify({ id, markdown })
|
|
137
|
+
}).catch((error) => {
|
|
138
|
+
throw unreachable(error);
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok) throw await refusal(response, "the server refused the request");
|
|
141
|
+
}
|
|
142
|
+
async read(id, run) {
|
|
143
|
+
const live = await this.join(id);
|
|
144
|
+
try {
|
|
145
|
+
return run(this.exists(live, id));
|
|
146
|
+
} finally {
|
|
147
|
+
await live.leave();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async write(id, run) {
|
|
151
|
+
const live = await this.join(id);
|
|
152
|
+
try {
|
|
153
|
+
this.exists(live, id);
|
|
154
|
+
const result = await live.edit((doc) => run(doc));
|
|
155
|
+
try {
|
|
156
|
+
await live.save();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
159
|
+
throw new DocError(error instanceof DocError ? error.code : "MISCONFIGURED", `the room has the change, but it is not saved: ${why}. Do not repeat the change.`);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
} finally {
|
|
163
|
+
await live.leave();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// A document that nobody made has an empty room and an empty log.
|
|
167
|
+
exists(live, id) {
|
|
168
|
+
if (live.doc.edits().length === 0) throw new DocError("NO_DOCUMENT", `no document with id "${id}"`);
|
|
169
|
+
return live.doc;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export {
|
|
174
|
+
whoAmI,
|
|
175
|
+
LiveDoc,
|
|
176
|
+
LiveService
|
|
177
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DocError
|
|
3
|
+
} from "./chunk-YBBQNCMC.js";
|
|
4
|
+
|
|
5
|
+
// src/adapters/file-blobs.ts
|
|
6
|
+
import { createHash, randomUUID } from "crypto";
|
|
7
|
+
import { link, mkdir, open, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { assertBlobPutOptions, assertResponseHeaders, parseRegion } from "agentic-identity";
|
|
10
|
+
var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
11
|
+
var missing = (error) => error.code === "ENOENT";
|
|
12
|
+
var HEADER = 4;
|
|
13
|
+
function pack(data, contentType) {
|
|
14
|
+
const head = Buffer.from(JSON.stringify(contentType === void 0 ? {} : { contentType }));
|
|
15
|
+
const length = Buffer.alloc(HEADER);
|
|
16
|
+
length.writeUInt32BE(head.length);
|
|
17
|
+
return Buffer.concat([length, head, data]);
|
|
18
|
+
}
|
|
19
|
+
function unpack(file) {
|
|
20
|
+
const end = HEADER + file.readUInt32BE(0);
|
|
21
|
+
const { contentType } = JSON.parse(file.subarray(HEADER, end).toString("utf8"));
|
|
22
|
+
return {
|
|
23
|
+
data: new Uint8Array(file.subarray(end)),
|
|
24
|
+
version: createHash("sha256").update(file).digest("hex"),
|
|
25
|
+
...contentType === void 0 ? {} : { contentType }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createLocker(root, options = {}) {
|
|
29
|
+
const staleMs = options.lockStaleMs ?? 1e4;
|
|
30
|
+
const timeoutMs = options.lockTimeoutMs ?? 5e3;
|
|
31
|
+
async function clearStale(lock) {
|
|
32
|
+
let token;
|
|
33
|
+
let age;
|
|
34
|
+
try {
|
|
35
|
+
token = await readFile(lock, "utf8");
|
|
36
|
+
age = Date.now() - (await stat(lock)).mtimeMs;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (missing(error)) return true;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (!(age > staleMs)) return false;
|
|
42
|
+
const moved = `${lock}.stale-${randomUUID()}`;
|
|
43
|
+
try {
|
|
44
|
+
await rename(lock, moved);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (missing(error)) return true;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
if (await readFile(moved, "utf8").catch(() => token) !== token) await link(moved, lock).catch(() => void 0);
|
|
50
|
+
await rm(moved, { force: true });
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
async function locked(full, run) {
|
|
54
|
+
await mkdir(root, { recursive: true });
|
|
55
|
+
const lock = `${full}.lock`;
|
|
56
|
+
const token = randomUUID();
|
|
57
|
+
const deadline = Date.now() + timeoutMs;
|
|
58
|
+
for (; ; ) {
|
|
59
|
+
try {
|
|
60
|
+
const file = await open(lock, "wx");
|
|
61
|
+
await file.writeFile(token).finally(() => file.close());
|
|
62
|
+
break;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error.code !== "EEXIST") throw error;
|
|
65
|
+
if (await clearStale(lock)) continue;
|
|
66
|
+
if (Date.now() >= deadline) throw new DocError("LOCKED", "another writer holds this document; try again");
|
|
67
|
+
await sleep(5 + Math.random() * 20);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const held = async () => await readFile(lock, "utf8").catch(() => null) === token;
|
|
71
|
+
try {
|
|
72
|
+
return await run(held);
|
|
73
|
+
} finally {
|
|
74
|
+
if (await held()) await rm(lock, { force: true });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return locked;
|
|
78
|
+
}
|
|
79
|
+
function createFileBlobs(dir, options = {}) {
|
|
80
|
+
const root = resolve(dir);
|
|
81
|
+
const locked = createLocker(root, options);
|
|
82
|
+
const now = options.now ?? Date.now;
|
|
83
|
+
const path = (key) => {
|
|
84
|
+
if (key === "") throw new Error("blob key must not be empty");
|
|
85
|
+
return resolve(root, createHash("sha256").update(key).digest("hex"));
|
|
86
|
+
};
|
|
87
|
+
async function read(full) {
|
|
88
|
+
try {
|
|
89
|
+
return unpack(await readFile(full));
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (missing(error)) return null;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
region: options.region === void 0 ? "unknown" : parseRegion(options.region),
|
|
97
|
+
async put(key, data, opts) {
|
|
98
|
+
const full = path(key);
|
|
99
|
+
assertBlobPutOptions(opts);
|
|
100
|
+
return locked(full, async (held) => {
|
|
101
|
+
if (opts?.ifNoneMatch === "*" || opts?.ifMatch !== void 0) {
|
|
102
|
+
const current = await read(full);
|
|
103
|
+
if (opts.ifNoneMatch === "*" ? current !== null : current?.version !== opts.ifMatch) return null;
|
|
104
|
+
}
|
|
105
|
+
const file = pack(data, opts?.contentType);
|
|
106
|
+
const temp = `${full}.${randomUUID()}.tmp`;
|
|
107
|
+
try {
|
|
108
|
+
await writeFile(temp, file);
|
|
109
|
+
if (!await held()) throw new DocError("LOCKED", "the lock was taken over while writing; try again");
|
|
110
|
+
await rename(temp, full);
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(temp, { force: true });
|
|
113
|
+
}
|
|
114
|
+
return { version: createHash("sha256").update(file).digest("hex") };
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
get: (key) => read(path(key)),
|
|
118
|
+
async head(key) {
|
|
119
|
+
const found = await read(path(key));
|
|
120
|
+
return found ? { size: found.data.length, version: found.version, ...found.contentType === void 0 ? {} : { contentType: found.contentType } } : null;
|
|
121
|
+
},
|
|
122
|
+
async delete(key) {
|
|
123
|
+
const full = path(key);
|
|
124
|
+
await locked(full, () => rm(full, { force: true }));
|
|
125
|
+
},
|
|
126
|
+
/** A local path with its expiry. Nothing serves or checks it: it exists so the port is whole. */
|
|
127
|
+
async signedUrl(key, opts) {
|
|
128
|
+
const full = path(key);
|
|
129
|
+
const { method, expiresInSeconds } = opts;
|
|
130
|
+
if (!Number.isInteger(expiresInSeconds) || expiresInSeconds < 1 || expiresInSeconds > 604800) {
|
|
131
|
+
throw new RangeError("expiresInSeconds must be an integer from 1 to 604800");
|
|
132
|
+
}
|
|
133
|
+
assertResponseHeaders(opts);
|
|
134
|
+
const extra = [
|
|
135
|
+
opts.responseContentDisposition === void 0 ? "" : `&disposition=${encodeURIComponent(opts.responseContentDisposition)}`,
|
|
136
|
+
opts.responseContentType === void 0 ? "" : `&type=${encodeURIComponent(opts.responseContentType)}`
|
|
137
|
+
].join("");
|
|
138
|
+
return `file://${full}?method=${method}&expires=${now() + expiresInSeconds * 1e3}${extra}`;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/adapters/file-store.ts
|
|
144
|
+
import { createMemoryStore, StoreConflictError } from "agentic-identity";
|
|
145
|
+
import { resolve as resolve2 } from "path";
|
|
146
|
+
var KEY = "store";
|
|
147
|
+
var ATTEMPTS = 64;
|
|
148
|
+
function createFileStore(dir, options = {}) {
|
|
149
|
+
const { shared: _shared, ...rest } = options;
|
|
150
|
+
const memoryOptions = { region: rest.region, now: rest.now };
|
|
151
|
+
const blobs = createFileBlobs(dir, options);
|
|
152
|
+
const locked = createLocker(resolve2(dir), options);
|
|
153
|
+
const mutex = resolve2(dir, "store-mutex");
|
|
154
|
+
const region = createMemoryStore(memoryOptions).region;
|
|
155
|
+
async function load() {
|
|
156
|
+
const found = await blobs.get(KEY);
|
|
157
|
+
const rows = /* @__PURE__ */ new Map();
|
|
158
|
+
const memory = createMemoryStore(memoryOptions);
|
|
159
|
+
if (found) {
|
|
160
|
+
for (const row of JSON.parse(new TextDecoder().decode(found.data))) {
|
|
161
|
+
rows.set(JSON.stringify([row[0], row[1]]), row);
|
|
162
|
+
await memory.put(row[0], row[1], row[2]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { memory, rows, version: found?.version };
|
|
166
|
+
}
|
|
167
|
+
async function save(rows, changes, version) {
|
|
168
|
+
if (changes.length === 0) return true;
|
|
169
|
+
for (const [collection, id, doc] of changes) {
|
|
170
|
+
const key = JSON.stringify([collection, id]);
|
|
171
|
+
if (doc === null) rows.delete(key);
|
|
172
|
+
else rows.set(key, [collection, id, doc]);
|
|
173
|
+
}
|
|
174
|
+
const data = new TextEncoder().encode(JSON.stringify([...rows.values()]));
|
|
175
|
+
const saved = await blobs.put(KEY, data, version === void 0 ? { ifNoneMatch: "*" } : { ifMatch: version });
|
|
176
|
+
return saved !== null;
|
|
177
|
+
}
|
|
178
|
+
const recording = (ops, changes) => ({
|
|
179
|
+
...ops,
|
|
180
|
+
put: async (collection, id, doc) => {
|
|
181
|
+
await ops.put(collection, id, doc);
|
|
182
|
+
changes.push([collection, id, doc]);
|
|
183
|
+
},
|
|
184
|
+
delete: async (collection, id) => {
|
|
185
|
+
await ops.delete(collection, id);
|
|
186
|
+
changes.push([collection, id, null]);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
const transaction = async (fn) => {
|
|
190
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
191
|
+
const done = await locked(mutex, async (held) => {
|
|
192
|
+
const { memory, rows, version } = await load();
|
|
193
|
+
let changes = [];
|
|
194
|
+
const result = await memory.transaction((tx) => {
|
|
195
|
+
changes = [];
|
|
196
|
+
return fn(recording(tx, changes));
|
|
197
|
+
});
|
|
198
|
+
if (changes.length > 0 && !await held()) throw new DocError("LOCKED", "the lock was taken over during the transaction; try again");
|
|
199
|
+
return await save(rows, changes, version) ? { result } : void 0;
|
|
200
|
+
});
|
|
201
|
+
if (done) return done.result;
|
|
202
|
+
}
|
|
203
|
+
throw new StoreConflictError(`transaction kept conflicting after ${ATTEMPTS} attempts`);
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
region,
|
|
207
|
+
get: async (collection, id) => (await load()).memory.get(collection, id),
|
|
208
|
+
query: async (collection, query) => (await load()).memory.query(collection, query),
|
|
209
|
+
put: (collection, id, doc) => transaction(async (tx) => {
|
|
210
|
+
await tx.get(collection, id);
|
|
211
|
+
await tx.put(collection, id, doc);
|
|
212
|
+
}),
|
|
213
|
+
delete: (collection, id) => transaction(async (tx) => {
|
|
214
|
+
await tx.get(collection, id);
|
|
215
|
+
await tx.delete(collection, id);
|
|
216
|
+
}),
|
|
217
|
+
transaction
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export {
|
|
222
|
+
createFileBlobs,
|
|
223
|
+
createFileStore
|
|
224
|
+
};
|