@mengine/storage 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/dist/auto-reconnect-0HwaV1v8.js +250 -0
- package/dist/base-B0LSXWTK.d.ts +191 -0
- package/dist/idb.d.ts +119 -0
- package/dist/idb.js +225 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/pg.d.ts +671 -0
- package/dist/pg.js +241 -0
- package/package.json +58 -0
package/dist/pg.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { n as BaseDocStorage, t as AutoReconnectConnection } from "./auto-reconnect-0HwaV1v8.js";
|
|
2
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
3
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
4
|
+
import pg from "pg";
|
|
5
|
+
import { customType, integer, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core";
|
|
6
|
+
//#region src/impls/pg/schema.ts
|
|
7
|
+
/**
|
|
8
|
+
* Postgres bytea maps to a Node `Buffer` on the wire; the storage contract works in `Uint8Array`,
|
|
9
|
+
* so we convert in both directions instead of leaking `Buffer` past this boundary.
|
|
10
|
+
*/
|
|
11
|
+
const bytea = customType({
|
|
12
|
+
dataType() {
|
|
13
|
+
return "bytea";
|
|
14
|
+
},
|
|
15
|
+
fromDriver(value) {
|
|
16
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
17
|
+
},
|
|
18
|
+
toDriver(value) {
|
|
19
|
+
return Buffer.from(value);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const docSnapshots = pgTable("doc_snapshots", {
|
|
23
|
+
docId: text("doc_id").primaryKey(),
|
|
24
|
+
data: bytea("data").notNull(),
|
|
25
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
26
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
|
|
27
|
+
});
|
|
28
|
+
const docUpdates = pgTable("doc_updates", {
|
|
29
|
+
docId: text("doc_id").notNull(),
|
|
30
|
+
seq: integer("seq").notNull(),
|
|
31
|
+
data: bytea("data").notNull(),
|
|
32
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
|
|
33
|
+
}, (table) => [primaryKey({ columns: [table.docId, table.seq] })]);
|
|
34
|
+
const schema = {
|
|
35
|
+
docSnapshots,
|
|
36
|
+
docUpdates
|
|
37
|
+
};
|
|
38
|
+
const migrations = [[
|
|
39
|
+
`CREATE TABLE IF NOT EXISTS "doc_snapshots" (
|
|
40
|
+
"doc_id" text PRIMARY KEY NOT NULL,
|
|
41
|
+
"data" bytea NOT NULL,
|
|
42
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
43
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
44
|
+
)`,
|
|
45
|
+
`CREATE TABLE IF NOT EXISTS "doc_updates" (
|
|
46
|
+
"doc_id" text NOT NULL,
|
|
47
|
+
"seq" integer NOT NULL,
|
|
48
|
+
"data" bytea NOT NULL,
|
|
49
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
50
|
+
PRIMARY KEY ("doc_id", "seq")
|
|
51
|
+
)`,
|
|
52
|
+
`ALTER TABLE "doc_updates" ALTER COLUMN "seq" DROP IDENTITY IF EXISTS`,
|
|
53
|
+
`DO $$
|
|
54
|
+
DECLARE
|
|
55
|
+
current_pk name;
|
|
56
|
+
current_pk_columns text[];
|
|
57
|
+
BEGIN
|
|
58
|
+
SELECT c.conname, array_agg(a.attname ORDER BY pk_key.ordinality)
|
|
59
|
+
INTO current_pk, current_pk_columns
|
|
60
|
+
FROM pg_constraint c
|
|
61
|
+
JOIN unnest(c.conkey) WITH ORDINALITY AS pk_key(attnum, ordinality) ON true
|
|
62
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = pk_key.attnum
|
|
63
|
+
WHERE c.conrelid = 'doc_updates'::regclass AND c.contype = 'p'
|
|
64
|
+
GROUP BY c.conname;
|
|
65
|
+
|
|
66
|
+
IF current_pk_columns IS DISTINCT FROM ARRAY['doc_id', 'seq']::text[] THEN
|
|
67
|
+
IF current_pk IS NOT NULL THEN
|
|
68
|
+
EXECUTE format('ALTER TABLE %I DROP CONSTRAINT %I', 'doc_updates', current_pk);
|
|
69
|
+
END IF;
|
|
70
|
+
|
|
71
|
+
ALTER TABLE "doc_updates" ADD PRIMARY KEY ("doc_id", "seq");
|
|
72
|
+
END IF;
|
|
73
|
+
END $$`
|
|
74
|
+
]];
|
|
75
|
+
/**
|
|
76
|
+
* Run all pending DDL inside a single transaction.
|
|
77
|
+
*
|
|
78
|
+
* The package owns the doc tables, so it creates them on connect rather than relying on a
|
|
79
|
+
* consumer-side migration runner.
|
|
80
|
+
*/
|
|
81
|
+
const migrate = async (pool) => {
|
|
82
|
+
const client = await pool.connect();
|
|
83
|
+
try {
|
|
84
|
+
await client.query("begin");
|
|
85
|
+
for (const step of migrations) for (const statement of step) await client.query(statement);
|
|
86
|
+
await client.query("commit");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
await client.query("rollback");
|
|
89
|
+
throw error;
|
|
90
|
+
} finally {
|
|
91
|
+
client.release();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const migrator = {
|
|
95
|
+
version: migrations.length,
|
|
96
|
+
migrate
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/impls/pg/connection.ts
|
|
100
|
+
var PgConnection = class extends AutoReconnectConnection {
|
|
101
|
+
pgOptions;
|
|
102
|
+
ownsPool = false;
|
|
103
|
+
constructor(pgOptions) {
|
|
104
|
+
super();
|
|
105
|
+
this.pgOptions = pgOptions;
|
|
106
|
+
}
|
|
107
|
+
async doConnect() {
|
|
108
|
+
let pool = this.pgOptions.pool;
|
|
109
|
+
if (!pool) {
|
|
110
|
+
if (!this.pgOptions.connectionString) throw new Error("PgConnection requires either a pool or a connectionString.");
|
|
111
|
+
pool = new pg.Pool({ connectionString: this.pgOptions.connectionString });
|
|
112
|
+
this.ownsPool = true;
|
|
113
|
+
}
|
|
114
|
+
await migrator.migrate(pool);
|
|
115
|
+
return {
|
|
116
|
+
pool,
|
|
117
|
+
db: drizzle(pool, { schema })
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
doDisconnect(conn) {
|
|
121
|
+
if (this.ownsPool) conn.pool.end().catch((error) => {
|
|
122
|
+
console.error("failed to close pg pool", error);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/impls/pg/index.ts
|
|
128
|
+
const toSnapshotRecord = (row) => {
|
|
129
|
+
if (!row) return null;
|
|
130
|
+
return {
|
|
131
|
+
docId: row.docId,
|
|
132
|
+
data: row.data,
|
|
133
|
+
createdAt: row.createdAt,
|
|
134
|
+
updatedAt: row.updatedAt
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const toUpdateRecord = (row) => ({
|
|
138
|
+
docId: row.docId,
|
|
139
|
+
seq: row.seq,
|
|
140
|
+
data: row.data,
|
|
141
|
+
createdAt: row.createdAt
|
|
142
|
+
});
|
|
143
|
+
var PgDocStorage = class extends BaseDocStorage {
|
|
144
|
+
static identifier = "PgDocStorage";
|
|
145
|
+
connection = new PgConnection(this.options);
|
|
146
|
+
get db() {
|
|
147
|
+
return this.connection.inner.db;
|
|
148
|
+
}
|
|
149
|
+
async getDoc(docId) {
|
|
150
|
+
if (this.isReadonly) {
|
|
151
|
+
const [snapshotRow] = await this.db.select().from(docSnapshots).where(eq(docSnapshots.docId, docId)).limit(1);
|
|
152
|
+
const updateRows = await this.db.select().from(docUpdates).where(eq(docUpdates.docId, docId)).orderBy(asc(docUpdates.seq));
|
|
153
|
+
const snapshot = toSnapshotRecord(snapshotRow);
|
|
154
|
+
const updates = updateRows.map(toUpdateRecord);
|
|
155
|
+
return updates.length ? this.mergeUpdates(snapshot, updates) : snapshot;
|
|
156
|
+
}
|
|
157
|
+
return await this.db.transaction(async (tx) => {
|
|
158
|
+
await tx.execute(sql`select doc_id from ${docSnapshots} where doc_id = ${docId} for update`);
|
|
159
|
+
const [snapshotRow] = await tx.select().from(docSnapshots).where(eq(docSnapshots.docId, docId)).limit(1);
|
|
160
|
+
const updateRows = await tx.select().from(docUpdates).where(eq(docUpdates.docId, docId)).orderBy(asc(docUpdates.seq));
|
|
161
|
+
const snapshot = toSnapshotRecord(snapshotRow);
|
|
162
|
+
const updates = updateRows.map(toUpdateRecord);
|
|
163
|
+
if (!updates.length) return snapshot;
|
|
164
|
+
const nextSnapshot = this.mergeUpdates(snapshot, updates);
|
|
165
|
+
await tx.insert(docSnapshots).values({
|
|
166
|
+
docId: nextSnapshot.docId,
|
|
167
|
+
data: nextSnapshot.data,
|
|
168
|
+
createdAt: nextSnapshot.createdAt,
|
|
169
|
+
updatedAt: nextSnapshot.updatedAt
|
|
170
|
+
}).onConflictDoUpdate({
|
|
171
|
+
target: docSnapshots.docId,
|
|
172
|
+
set: {
|
|
173
|
+
data: nextSnapshot.data,
|
|
174
|
+
updatedAt: nextSnapshot.updatedAt
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
await tx.delete(docUpdates).where(and(eq(docUpdates.docId, docId), inArray(docUpdates.seq, updates.map((update) => update.seq))));
|
|
178
|
+
return nextSnapshot;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async pushDocUpdate(update, _origin) {
|
|
182
|
+
const now = /* @__PURE__ */ new Date();
|
|
183
|
+
await this.db.transaction(async (tx) => {
|
|
184
|
+
const [snapshot] = await tx.select({ docId: docSnapshots.docId }).from(docSnapshots).where(eq(docSnapshots.docId, update.docId)).for("update");
|
|
185
|
+
if (!snapshot) {
|
|
186
|
+
if ((await tx.insert(docSnapshots).values({
|
|
187
|
+
docId: update.docId,
|
|
188
|
+
data: update.data,
|
|
189
|
+
createdAt: now,
|
|
190
|
+
updatedAt: now
|
|
191
|
+
}).onConflictDoNothing().returning({ docId: docSnapshots.docId })).length) return;
|
|
192
|
+
await tx.select({ docId: docSnapshots.docId }).from(docSnapshots).where(eq(docSnapshots.docId, update.docId)).for("update");
|
|
193
|
+
}
|
|
194
|
+
const [{ seq }] = await tx.select({ seq: sql`coalesce(max(${docUpdates.seq}), 0) + 1` }).from(docUpdates).where(eq(docUpdates.docId, update.docId));
|
|
195
|
+
await tx.insert(docUpdates).values({
|
|
196
|
+
docId: update.docId,
|
|
197
|
+
seq,
|
|
198
|
+
data: update.data,
|
|
199
|
+
createdAt: now
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
async deleteDoc(docId) {
|
|
204
|
+
await this.db.transaction(async (tx) => {
|
|
205
|
+
await tx.delete(docUpdates).where(eq(docUpdates.docId, docId));
|
|
206
|
+
await tx.delete(docSnapshots).where(eq(docSnapshots.docId, docId));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async getDocSnapshot(docId) {
|
|
210
|
+
const [snapshot] = await this.db.select().from(docSnapshots).where(eq(docSnapshots.docId, docId)).limit(1);
|
|
211
|
+
return toSnapshotRecord(snapshot);
|
|
212
|
+
}
|
|
213
|
+
async setDocSnapshot(snapshot) {
|
|
214
|
+
await this.db.insert(docSnapshots).values({
|
|
215
|
+
docId: snapshot.docId,
|
|
216
|
+
data: snapshot.data,
|
|
217
|
+
createdAt: snapshot.createdAt,
|
|
218
|
+
updatedAt: snapshot.updatedAt
|
|
219
|
+
}).onConflictDoUpdate({
|
|
220
|
+
target: docSnapshots.docId,
|
|
221
|
+
set: {
|
|
222
|
+
data: snapshot.data,
|
|
223
|
+
updatedAt: snapshot.updatedAt
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
async getDocUpdates(docId) {
|
|
229
|
+
return (await this.db.select().from(docUpdates).where(eq(docUpdates.docId, docId)).orderBy(asc(docUpdates.seq))).map(toUpdateRecord);
|
|
230
|
+
}
|
|
231
|
+
async markUpdatesMerged(docId, updates) {
|
|
232
|
+
if (!updates.length) return 0;
|
|
233
|
+
await this.db.delete(docUpdates).where(and(eq(docUpdates.docId, docId), inArray(docUpdates.seq, updates.map((update) => update.seq))));
|
|
234
|
+
return updates.length;
|
|
235
|
+
}
|
|
236
|
+
subscribeDocUpdate(_callback) {
|
|
237
|
+
return () => {};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
export { PgConnection, PgDocStorage, docSnapshots, docUpdates, schema };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mengine/storage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "UNLICENSED",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/one2x-ai/medeo-engine.git",
|
|
8
|
+
"directory": "packages/storage"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./src/index.ts",
|
|
16
|
+
"./idb": "./src/impls/idb/index.ts",
|
|
17
|
+
"./pg": "./src/impls/pg/index.ts",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js",
|
|
23
|
+
"./idb": "./dist/idb.js",
|
|
24
|
+
"./pg": "./dist/pg.js",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"access": "public",
|
|
28
|
+
"registry": "https://registry.npmjs.org/"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "vp pack",
|
|
32
|
+
"dev": "vp pack --watch",
|
|
33
|
+
"test": "vp test",
|
|
34
|
+
"prepublishOnly": "vp pack"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mengine/utils": "workspace:*",
|
|
38
|
+
"drizzle-orm": "catalog:",
|
|
39
|
+
"lodash-es": "4.18.1",
|
|
40
|
+
"loro-crdt": "catalog:"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@testcontainers/postgresql": "catalog:",
|
|
44
|
+
"@types/lodash-es": "catalog:",
|
|
45
|
+
"@types/node": "catalog:",
|
|
46
|
+
"@types/pg": "catalog:",
|
|
47
|
+
"@typescript/native-preview": "catalog:",
|
|
48
|
+
"idb": "catalog:",
|
|
49
|
+
"pg": "catalog:",
|
|
50
|
+
"typescript": "catalog:",
|
|
51
|
+
"vite-plugin-wasm": "catalog:",
|
|
52
|
+
"vite-plus": "catalog:"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"idb": "catalog:",
|
|
56
|
+
"pg": "catalog:"
|
|
57
|
+
}
|
|
58
|
+
}
|