@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/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
+ }