@korajs/server 1.0.0-beta.0 → 1.0.0-beta.2
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/index.cjs +586 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +509 -179
- package/dist/index.d.ts +509 -179
- package/dist/index.js +568 -49
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
package/dist/index.cjs
CHANGED
|
@@ -176,6 +176,7 @@ __export(index_exports, {
|
|
|
176
176
|
MixedAuthProvider: () => MixedAuthProvider,
|
|
177
177
|
NoAuthProvider: () => NoAuthProvider,
|
|
178
178
|
PostgresServerStore: () => PostgresServerStore,
|
|
179
|
+
RETRIABLE_REJECTION_CODES: () => RETRIABLE_REJECTION_CODES,
|
|
179
180
|
SqliteServerStore: () => SqliteServerStore,
|
|
180
181
|
TokenAuthProvider: () => TokenAuthProvider,
|
|
181
182
|
WsServerTransport: () => WsServerTransport,
|
|
@@ -186,7 +187,8 @@ __export(index_exports, {
|
|
|
186
187
|
createPrettyLogger: () => createPrettyLogger,
|
|
187
188
|
createProductionServer: () => createProductionServer,
|
|
188
189
|
createSilentLogger: () => createSilentLogger,
|
|
189
|
-
createSqliteServerStore: () => createSqliteServerStore
|
|
190
|
+
createSqliteServerStore: () => createSqliteServerStore,
|
|
191
|
+
isRetriableRejection: () => isRetriableRejection
|
|
190
192
|
});
|
|
191
193
|
module.exports = __toCommonJS(index_exports);
|
|
192
194
|
init_cjs_shims();
|
|
@@ -248,6 +250,13 @@ function createDefaultLogger() {
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
|
|
253
|
+
// src/apply/rejection-taxonomy.ts
|
|
254
|
+
init_cjs_shims();
|
|
255
|
+
var RETRIABLE_REJECTION_CODES = /* @__PURE__ */ new Set(["RATE_LIMIT"]);
|
|
256
|
+
function isRetriableRejection(code) {
|
|
257
|
+
return RETRIABLE_REJECTION_CODES.has(code);
|
|
258
|
+
}
|
|
259
|
+
|
|
251
260
|
// src/store/memory-server-store.ts
|
|
252
261
|
init_cjs_shims();
|
|
253
262
|
var import_core2 = require("@korajs/core");
|
|
@@ -269,6 +278,14 @@ function fieldTypeToSql(descriptor, dialect) {
|
|
|
269
278
|
return dialect === "postgres" ? "BIGINT" : "INTEGER";
|
|
270
279
|
case "array":
|
|
271
280
|
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
281
|
+
case "object":
|
|
282
|
+
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
283
|
+
case "json":
|
|
284
|
+
return dialect === "postgres" ? "JSONB" : "TEXT";
|
|
285
|
+
case "blob":
|
|
286
|
+
return "TEXT";
|
|
287
|
+
case "secret":
|
|
288
|
+
return "TEXT";
|
|
272
289
|
case "richtext":
|
|
273
290
|
return dialect === "postgres" ? "BYTEA" : "BLOB";
|
|
274
291
|
}
|
|
@@ -285,13 +302,13 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
285
302
|
const columns = ["id TEXT PRIMARY KEY NOT NULL"];
|
|
286
303
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
287
304
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
288
|
-
let colDef = `${fieldName} ${sqlType}`;
|
|
305
|
+
let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
|
|
289
306
|
if (descriptor.defaultValue !== void 0) {
|
|
290
307
|
colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
|
|
291
308
|
}
|
|
292
309
|
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
293
310
|
const values = descriptor.enumValues.map((v) => `'${v}'`).join(", ");
|
|
294
|
-
colDef += ` CHECK (${fieldName} IN (${values}))`;
|
|
311
|
+
colDef += ` CHECK (${(0, import_core.quoteIdent)(fieldName)} IN (${values}))`;
|
|
295
312
|
}
|
|
296
313
|
columns.push(colDef);
|
|
297
314
|
}
|
|
@@ -299,24 +316,26 @@ function generateCollectionDDL(name, collection, dialect) {
|
|
|
299
316
|
columns.push(`_created_at ${tsType} NOT NULL DEFAULT 0`);
|
|
300
317
|
columns.push(`_updated_at ${tsType} NOT NULL DEFAULT 0`);
|
|
301
318
|
columns.push("_deleted INTEGER NOT NULL DEFAULT 0");
|
|
302
|
-
statements.push(`CREATE TABLE IF NOT EXISTS ${name} (
|
|
319
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${(0, import_core.quoteIdent)(name)} (
|
|
303
320
|
${columns.join(",\n ")}
|
|
304
321
|
)`);
|
|
305
322
|
for (const [fieldName, descriptor] of Object.entries(collection.fields)) {
|
|
306
323
|
const sqlType = fieldTypeToSql(descriptor, dialect);
|
|
307
|
-
let colDef = `${fieldName} ${sqlType}`;
|
|
324
|
+
let colDef = `${(0, import_core.quoteIdent)(fieldName)} ${sqlType}`;
|
|
308
325
|
if (descriptor.defaultValue !== void 0) {
|
|
309
326
|
colDef += ` DEFAULT ${sqlDefaultLiteral(descriptor.defaultValue)}`;
|
|
310
327
|
}
|
|
311
328
|
statements.push(`--kora:safe-alter
|
|
312
|
-
ALTER TABLE ${name} ADD COLUMN ${colDef}`);
|
|
329
|
+
ALTER TABLE ${(0, import_core.quoteIdent)(name)} ADD COLUMN ${colDef}`);
|
|
313
330
|
}
|
|
314
331
|
for (const indexField of collection.indexes) {
|
|
315
332
|
statements.push(
|
|
316
|
-
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${name} (${indexField})`
|
|
333
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}_${indexField} ON ${(0, import_core.quoteIdent)(name)} (${(0, import_core.quoteIdent)(indexField)})`
|
|
317
334
|
);
|
|
318
335
|
}
|
|
319
|
-
statements.push(
|
|
336
|
+
statements.push(
|
|
337
|
+
`CREATE INDEX IF NOT EXISTS idx_${name}__deleted ON ${(0, import_core.quoteIdent)(name)} (_deleted)`
|
|
338
|
+
);
|
|
320
339
|
return statements;
|
|
321
340
|
}
|
|
322
341
|
function generateAllCollectionDDL(schema, dialect) {
|
|
@@ -952,7 +971,7 @@ var PostgresServerStore = class {
|
|
|
952
971
|
this.assertCollection(collection);
|
|
953
972
|
const schema = this.schema;
|
|
954
973
|
const collectionDef = schema.collections[collection];
|
|
955
|
-
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
974
|
+
const query = import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
956
975
|
const rows = await this.db.execute(query);
|
|
957
976
|
if (rows.length === 0) return null;
|
|
958
977
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -969,7 +988,7 @@ var PostgresServerStore = class {
|
|
|
969
988
|
}
|
|
970
989
|
}
|
|
971
990
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
972
|
-
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw(collection)} WHERE ${whereClause}`;
|
|
991
|
+
const query = import_drizzle_orm.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
973
992
|
const rows = await this.db.execute(query);
|
|
974
993
|
const cnt = rows[0]?.cnt;
|
|
975
994
|
return typeof cnt === "string" ? Number.parseInt(cnt, 10) : cnt ?? 0;
|
|
@@ -1056,7 +1075,7 @@ var PostgresServerStore = class {
|
|
|
1056
1075
|
);
|
|
1057
1076
|
} else {
|
|
1058
1077
|
await txOrDb.execute(
|
|
1059
|
-
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1078
|
+
import_drizzle_orm.sql`UPDATE ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1060
1079
|
);
|
|
1061
1080
|
}
|
|
1062
1081
|
}
|
|
@@ -1075,16 +1094,16 @@ var PostgresServerStore = class {
|
|
|
1075
1094
|
updatedAt,
|
|
1076
1095
|
0
|
|
1077
1096
|
];
|
|
1078
|
-
const columnsSql = import_drizzle_orm.sql.raw(allColumns.join(", "));
|
|
1097
|
+
const columnsSql = import_drizzle_orm.sql.raw(allColumns.map((c) => (0, import_core3.quoteIdent)(c)).join(", "));
|
|
1079
1098
|
const valuesSql = import_drizzle_orm.sql.join(
|
|
1080
1099
|
values.map((v) => import_drizzle_orm.sql`${v}`),
|
|
1081
1100
|
import_drizzle_orm.sql.raw(", ")
|
|
1082
1101
|
);
|
|
1083
1102
|
const updateSet = import_drizzle_orm.sql.raw(
|
|
1084
|
-
allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
|
|
1103
|
+
allColumns.slice(1).map((c) => `${(0, import_core3.quoteIdent)(c)} = excluded.${(0, import_core3.quoteIdent)(c)}`).join(", ")
|
|
1085
1104
|
);
|
|
1086
1105
|
await txOrDb.execute(
|
|
1087
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1106
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1088
1107
|
);
|
|
1089
1108
|
}
|
|
1090
1109
|
/**
|
|
@@ -1144,7 +1163,7 @@ var PostgresServerStore = class {
|
|
|
1144
1163
|
);
|
|
1145
1164
|
} else {
|
|
1146
1165
|
await this.db.execute(
|
|
1147
|
-
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1166
|
+
import_drizzle_orm.sql`INSERT INTO ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1148
1167
|
);
|
|
1149
1168
|
}
|
|
1150
1169
|
}
|
|
@@ -1157,10 +1176,12 @@ var PostgresServerStore = class {
|
|
|
1157
1176
|
options?.where ?? {},
|
|
1158
1177
|
options?.includeDeleted ?? false
|
|
1159
1178
|
);
|
|
1160
|
-
const parts = [
|
|
1179
|
+
const parts = [
|
|
1180
|
+
import_drizzle_orm.sql`SELECT * FROM ${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
1181
|
+
];
|
|
1161
1182
|
if (options?.orderBy) {
|
|
1162
1183
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1163
|
-
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
1184
|
+
parts.push(import_drizzle_orm.sql.raw(` ORDER BY ${(0, import_core3.quoteIdent)(options.orderBy)} ${dir}`));
|
|
1164
1185
|
}
|
|
1165
1186
|
if (options?.limit !== void 0) {
|
|
1166
1187
|
parts.push(import_drizzle_orm.sql` LIMIT ${options.limit}`);
|
|
@@ -1176,7 +1197,7 @@ var PostgresServerStore = class {
|
|
|
1176
1197
|
conditions.push(import_drizzle_orm.sql.raw("_deleted = 0"));
|
|
1177
1198
|
}
|
|
1178
1199
|
for (const [key, value] of Object.entries(where)) {
|
|
1179
|
-
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw(key)} = ${value}`);
|
|
1200
|
+
conditions.push(import_drizzle_orm.sql`${import_drizzle_orm.sql.raw((0, import_core3.quoteIdent)(key))} = ${value}`);
|
|
1180
1201
|
}
|
|
1181
1202
|
if (conditions.length === 0) {
|
|
1182
1203
|
return import_drizzle_orm.sql.raw("1 = 1");
|
|
@@ -1528,7 +1549,7 @@ var SqliteServerStore = class {
|
|
|
1528
1549
|
this.assertCollection(collection);
|
|
1529
1550
|
const schema = this.schema;
|
|
1530
1551
|
const collectionDef = schema.collections[collection];
|
|
1531
|
-
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE id = ${id} AND _deleted = 0`;
|
|
1552
|
+
const query = import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE id = ${id} AND _deleted = 0`;
|
|
1532
1553
|
const rows = this.db.all(query);
|
|
1533
1554
|
if (rows.length === 0) return null;
|
|
1534
1555
|
return this.deserializeRow(rows[0], collectionDef);
|
|
@@ -1544,7 +1565,7 @@ var SqliteServerStore = class {
|
|
|
1544
1565
|
}
|
|
1545
1566
|
}
|
|
1546
1567
|
const whereClause = this.buildWhereClause(where ?? {}, false);
|
|
1547
|
-
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw(collection)} WHERE ${whereClause}`;
|
|
1568
|
+
const query = import_drizzle_orm2.sql`SELECT COUNT(*) as cnt FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`;
|
|
1548
1569
|
const rows = this.db.all(query);
|
|
1549
1570
|
return rows[0]?.cnt ?? 0;
|
|
1550
1571
|
}
|
|
@@ -1621,7 +1642,7 @@ var SqliteServerStore = class {
|
|
|
1621
1642
|
);
|
|
1622
1643
|
} else {
|
|
1623
1644
|
txOrDb.run(
|
|
1624
|
-
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw(collection)} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1645
|
+
import_drizzle_orm2.sql`UPDATE ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} SET _deleted = 1, _updated_at = ${Date.now()} WHERE id = ${recordId}`
|
|
1625
1646
|
);
|
|
1626
1647
|
}
|
|
1627
1648
|
}
|
|
@@ -1642,16 +1663,16 @@ var SqliteServerStore = class {
|
|
|
1642
1663
|
0
|
|
1643
1664
|
// _deleted = false
|
|
1644
1665
|
];
|
|
1645
|
-
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.join(", "));
|
|
1666
|
+
const columnsSql = import_drizzle_orm2.sql.raw(allColumns.map((c) => (0, import_core4.quoteIdent)(c)).join(", "));
|
|
1646
1667
|
const valuesSql = import_drizzle_orm2.sql.join(
|
|
1647
1668
|
values.map((v) => import_drizzle_orm2.sql`${v}`),
|
|
1648
1669
|
import_drizzle_orm2.sql.raw(", ")
|
|
1649
1670
|
);
|
|
1650
1671
|
const updateSet = import_drizzle_orm2.sql.raw(
|
|
1651
|
-
allColumns.slice(1).map((c) => `${c} = excluded.${c}`).join(", ")
|
|
1672
|
+
allColumns.slice(1).map((c) => `${(0, import_core4.quoteIdent)(c)} = excluded.${(0, import_core4.quoteIdent)(c)}`).join(", ")
|
|
1652
1673
|
);
|
|
1653
1674
|
txOrDb.run(
|
|
1654
|
-
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(tableName)} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1675
|
+
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(tableName))} (${columnsSql}) VALUES (${valuesSql}) ON CONFLICT (id) DO UPDATE SET ${updateSet}`
|
|
1655
1676
|
);
|
|
1656
1677
|
}
|
|
1657
1678
|
/**
|
|
@@ -1709,7 +1730,7 @@ var SqliteServerStore = class {
|
|
|
1709
1730
|
);
|
|
1710
1731
|
} else {
|
|
1711
1732
|
tx.run(
|
|
1712
|
-
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw(collectionName)} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1733
|
+
import_drizzle_orm2.sql`INSERT INTO ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collectionName))} (id, _deleted, _created_at, _updated_at) VALUES (${recordId}, 1, ${Date.now()}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET _deleted = 1, _updated_at = ${Date.now()}`
|
|
1713
1734
|
);
|
|
1714
1735
|
}
|
|
1715
1736
|
}
|
|
@@ -1723,10 +1744,12 @@ var SqliteServerStore = class {
|
|
|
1723
1744
|
options?.where ?? {},
|
|
1724
1745
|
options?.includeDeleted ?? false
|
|
1725
1746
|
);
|
|
1726
|
-
const parts = [
|
|
1747
|
+
const parts = [
|
|
1748
|
+
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(collection))} WHERE ${whereClause}`
|
|
1749
|
+
];
|
|
1727
1750
|
if (options?.orderBy) {
|
|
1728
1751
|
const dir = options.orderDirection === "desc" ? "DESC" : "ASC";
|
|
1729
|
-
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${options.orderBy} ${dir}`));
|
|
1752
|
+
parts.push(import_drizzle_orm2.sql.raw(` ORDER BY ${(0, import_core4.quoteIdent)(options.orderBy)} ${dir}`));
|
|
1730
1753
|
}
|
|
1731
1754
|
if (options?.limit !== void 0) {
|
|
1732
1755
|
parts.push(import_drizzle_orm2.sql` LIMIT ${options.limit}`);
|
|
@@ -1742,7 +1765,7 @@ var SqliteServerStore = class {
|
|
|
1742
1765
|
conditions.push(import_drizzle_orm2.sql.raw("_deleted = 0"));
|
|
1743
1766
|
}
|
|
1744
1767
|
for (const [key, value] of Object.entries(where)) {
|
|
1745
|
-
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw(key)} = ${value}`);
|
|
1768
|
+
conditions.push(import_drizzle_orm2.sql`${import_drizzle_orm2.sql.raw((0, import_core4.quoteIdent)(key))} = ${value}`);
|
|
1746
1769
|
}
|
|
1747
1770
|
if (conditions.length === 0) {
|
|
1748
1771
|
return import_drizzle_orm2.sql.raw("1 = 1");
|
|
@@ -2067,6 +2090,7 @@ init_cjs_shims();
|
|
|
2067
2090
|
var import_core10 = require("@korajs/core");
|
|
2068
2091
|
var import_internal = require("@korajs/core/internal");
|
|
2069
2092
|
var import_sync2 = require("@korajs/sync");
|
|
2093
|
+
var import_sync3 = require("@korajs/sync");
|
|
2070
2094
|
|
|
2071
2095
|
// src/apply/apply-server-operation.ts
|
|
2072
2096
|
init_cjs_shims();
|
|
@@ -2188,12 +2212,14 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2188
2212
|
const lookup = relationLookup ?? (schema ? (0, import_merge2.buildMergeRelationLookup)(schema) : /* @__PURE__ */ new Map());
|
|
2189
2213
|
const constraintCheck = await validateIncomingOperationConstraints(store, op, schema);
|
|
2190
2214
|
if (!constraintCheck.valid) {
|
|
2215
|
+
const code = constraintCheck.code ?? "CONSTRAINT_VIOLATION";
|
|
2191
2216
|
return {
|
|
2192
2217
|
result: "skipped",
|
|
2193
2218
|
appliedOperations: [],
|
|
2194
2219
|
rejection: {
|
|
2195
|
-
code
|
|
2196
|
-
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint
|
|
2220
|
+
code,
|
|
2221
|
+
message: constraintCheck.message ?? `Operation "${op.id}" violates a schema constraint`,
|
|
2222
|
+
retriable: isRetriableRejection(code)
|
|
2197
2223
|
}
|
|
2198
2224
|
};
|
|
2199
2225
|
}
|
|
@@ -2206,7 +2232,8 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2206
2232
|
appliedOperations: [],
|
|
2207
2233
|
rejection: {
|
|
2208
2234
|
code: "REFERENTIAL_INTEGRITY",
|
|
2209
|
-
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"
|
|
2235
|
+
message: `Operation "${op.id}" violates referential integrity on "${op.collection}"`,
|
|
2236
|
+
retriable: isRetriableRejection("REFERENTIAL_INTEGRITY")
|
|
2210
2237
|
}
|
|
2211
2238
|
};
|
|
2212
2239
|
}
|
|
@@ -2239,6 +2266,14 @@ async function applyServerOperation(store, op, relationLookup) {
|
|
|
2239
2266
|
};
|
|
2240
2267
|
}
|
|
2241
2268
|
|
|
2269
|
+
// src/auth/no-auth.ts
|
|
2270
|
+
init_cjs_shims();
|
|
2271
|
+
var NoAuthProvider = class {
|
|
2272
|
+
async authenticate(_token) {
|
|
2273
|
+
return { userId: "anonymous" };
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2242
2277
|
// src/scopes/resolve-session-scopes.ts
|
|
2243
2278
|
init_cjs_shims();
|
|
2244
2279
|
var import_core8 = require("@korajs/core");
|
|
@@ -2368,6 +2403,25 @@ var SessionRateLimiter = class {
|
|
|
2368
2403
|
// src/session/client-session.ts
|
|
2369
2404
|
var DEFAULT_BATCH_SIZE = 100;
|
|
2370
2405
|
var DEFAULT_SCHEMA_VERSION = 1;
|
|
2406
|
+
var warnedUnscopedProviders = /* @__PURE__ */ new WeakSet();
|
|
2407
|
+
function warnIfMultiTenantWithoutScopes(auth, resolvedScopes, schema) {
|
|
2408
|
+
if (!auth || auth instanceof NoAuthProvider) {
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (resolvedScopes) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (!schema || Object.keys(schema.collections).length === 0) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (warnedUnscopedProviders.has(auth)) {
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
warnedUnscopedProviders.add(auth);
|
|
2421
|
+
console.warn(
|
|
2422
|
+
"[kora] An authenticated session resolved to no sync scopes, so every user will sync every other user's data. Return per-user sync scopes from your auth provider (for example KoraAuthProvider's resolveScopes) to isolate tenants. Note: declaring sync rules in your schema is not enough on its own \u2014 the per-user values come from the auth provider. (This warning is expected for single-tenant apps where all authenticated users are meant to share the same data.)"
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2371
2425
|
var ClientSession = class {
|
|
2372
2426
|
state = "connected";
|
|
2373
2427
|
clientNodeId = null;
|
|
@@ -2386,16 +2440,21 @@ var ClientSession = class {
|
|
|
2386
2440
|
onRelay;
|
|
2387
2441
|
onAwarenessUpdate;
|
|
2388
2442
|
onYjsDocUpdate;
|
|
2443
|
+
onBlobChunkRequest;
|
|
2444
|
+
onBlobChunkResponse;
|
|
2445
|
+
persistBlobChunk;
|
|
2389
2446
|
onClose;
|
|
2390
2447
|
maxOperationBytes;
|
|
2391
2448
|
maxOpsPerMinute;
|
|
2392
2449
|
rateLimiter;
|
|
2450
|
+
validateOperation;
|
|
2451
|
+
koraContext;
|
|
2393
2452
|
constructor(options) {
|
|
2394
2453
|
this.sessionId = options.sessionId;
|
|
2395
2454
|
this.transport = options.transport;
|
|
2396
2455
|
this.store = options.store;
|
|
2397
2456
|
this.auth = options.auth ?? null;
|
|
2398
|
-
this.serializer = options.serializer ?? new
|
|
2457
|
+
this.serializer = options.serializer ?? new import_sync3.NegotiatedMessageSerializer("json");
|
|
2399
2458
|
this.emitter = options.emitter ?? null;
|
|
2400
2459
|
this.batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
2401
2460
|
this.schemaVersion = options.schemaVersion ?? DEFAULT_SCHEMA_VERSION;
|
|
@@ -2407,10 +2466,15 @@ var ClientSession = class {
|
|
|
2407
2466
|
this.onRelay = options.onRelay ?? null;
|
|
2408
2467
|
this.onAwarenessUpdate = options.onAwarenessUpdate ?? null;
|
|
2409
2468
|
this.onYjsDocUpdate = options.onYjsDocUpdate ?? null;
|
|
2469
|
+
this.onBlobChunkRequest = options.onBlobChunkRequest ?? null;
|
|
2470
|
+
this.onBlobChunkResponse = options.onBlobChunkResponse ?? null;
|
|
2471
|
+
this.persistBlobChunk = options.persistBlobChunk ?? null;
|
|
2410
2472
|
this.onClose = options.onClose ?? null;
|
|
2411
2473
|
this.maxOperationBytes = options.maxOperationBytes ?? DEFAULT_MAX_OPERATION_BYTES;
|
|
2412
2474
|
this.maxOpsPerMinute = options.maxOpsPerMinute ?? DEFAULT_MAX_OPS_PER_MINUTE;
|
|
2413
2475
|
this.rateLimiter = new SessionRateLimiter(this.maxOpsPerMinute);
|
|
2476
|
+
this.validateOperation = options.validateOperation ?? null;
|
|
2477
|
+
this.koraContext = options.koraContext ?? null;
|
|
2414
2478
|
}
|
|
2415
2479
|
/**
|
|
2416
2480
|
* Start handling messages from the client transport.
|
|
@@ -2512,8 +2576,33 @@ var ClientSession = class {
|
|
|
2512
2576
|
case "yjs-doc-update":
|
|
2513
2577
|
this.handleYjsDocUpdate(message);
|
|
2514
2578
|
break;
|
|
2579
|
+
case "blob-chunk-request":
|
|
2580
|
+
this.onBlobChunkRequest?.(this.sessionId, message);
|
|
2581
|
+
break;
|
|
2582
|
+
case "blob-chunk-response":
|
|
2583
|
+
this.onBlobChunkResponse?.(this.sessionId, message);
|
|
2584
|
+
break;
|
|
2585
|
+
case "blob-chunk-push":
|
|
2586
|
+
await this.handleBlobChunkPush(message);
|
|
2587
|
+
break;
|
|
2515
2588
|
}
|
|
2516
2589
|
}
|
|
2590
|
+
/**
|
|
2591
|
+
* Persist a client-uploaded blob chunk (or manifest) centrally. The bytes are
|
|
2592
|
+
* verified to hash to the declared hash before storing, so a corrupt or
|
|
2593
|
+
* mislabeled upload is rejected rather than served later as trusted content.
|
|
2594
|
+
*/
|
|
2595
|
+
async handleBlobChunkPush(message) {
|
|
2596
|
+
if (!this.persistBlobChunk) {
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const bytes = (0, import_sync2.decodeBlobChunkBytes)(message.bytes);
|
|
2600
|
+
const actual = await (0, import_core10.hashBlob)(bytes);
|
|
2601
|
+
if (actual !== message.hash) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
await this.persistBlobChunk(message.hash, bytes);
|
|
2605
|
+
}
|
|
2517
2606
|
handleMessageFailure(error) {
|
|
2518
2607
|
const reason = error instanceof Error ? error.message : "Message handling failed";
|
|
2519
2608
|
this.sendError("SYNC_ERROR", reason, true);
|
|
@@ -2547,25 +2636,26 @@ var ClientSession = class {
|
|
|
2547
2636
|
this.authContext = { userId: msg.nodeId, scopes: resolvedScopes };
|
|
2548
2637
|
}
|
|
2549
2638
|
}
|
|
2639
|
+
warnIfMultiTenantWithoutScopes(this.auth, resolvedScopes, this.store.getSchema());
|
|
2550
2640
|
if (msg.syncQueries && msg.syncQueries.length > 0) {
|
|
2551
|
-
this.syncQuerySubsets = (0,
|
|
2641
|
+
this.syncQuerySubsets = (0, import_sync3.dedupeQuerySubsets)(msg.syncQueries);
|
|
2552
2642
|
} else {
|
|
2553
2643
|
this.syncQuerySubsets = [];
|
|
2554
2644
|
}
|
|
2555
|
-
this.resumeDeltaCursor = msg.deltaCursor ? (0,
|
|
2645
|
+
this.resumeDeltaCursor = msg.deltaCursor ? (0, import_sync3.decodeDeltaCursor)(msg.deltaCursor) : null;
|
|
2556
2646
|
const serverVector = this.store.getVersionVector();
|
|
2557
2647
|
const selectedWireFormat = selectWireFormat(msg.supportedWireFormats);
|
|
2558
2648
|
this.setSerializerWireFormat(selectedWireFormat);
|
|
2559
|
-
if (!(0,
|
|
2649
|
+
if (!(0, import_sync3.isClientSchemaVersionSupported)(msg.schemaVersion, this.supportedSchemaVersions)) {
|
|
2560
2650
|
const { min, max } = this.supportedSchemaVersions;
|
|
2561
2651
|
const response2 = {
|
|
2562
2652
|
type: "handshake-response",
|
|
2563
2653
|
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2564
2654
|
nodeId: this.store.getNodeId(),
|
|
2565
|
-
versionVector: (0,
|
|
2655
|
+
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
2566
2656
|
schemaVersion: this.schemaVersion,
|
|
2567
2657
|
accepted: false,
|
|
2568
|
-
rejectReason: `${
|
|
2658
|
+
rejectReason: `${import_sync3.SCHEMA_MISMATCH_PREFIX}: client schema version ${msg.schemaVersion} not in supported range [${min}, ${max}]`,
|
|
2569
2659
|
supportedSchemaMin: min,
|
|
2570
2660
|
supportedSchemaMax: max,
|
|
2571
2661
|
serverTime: Date.now()
|
|
@@ -2578,11 +2668,14 @@ var ClientSession = class {
|
|
|
2578
2668
|
type: "handshake-response",
|
|
2579
2669
|
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2580
2670
|
nodeId: this.store.getNodeId(),
|
|
2581
|
-
versionVector: (0,
|
|
2671
|
+
versionVector: (0, import_sync3.versionVectorToWire)(serverVector),
|
|
2582
2672
|
schemaVersion: this.schemaVersion,
|
|
2583
2673
|
accepted: true,
|
|
2584
2674
|
selectedWireFormat,
|
|
2585
2675
|
serverTime: Date.now(),
|
|
2676
|
+
// Advertise central blob storage so the client uploads the bytes behind
|
|
2677
|
+
// its blob fields, keeping them available after the author goes offline.
|
|
2678
|
+
...this.persistBlobChunk ? { blobStorageEnabled: true } : {},
|
|
2586
2679
|
// Confirm the accepted scope so the client knows what data will be synced.
|
|
2587
2680
|
// This may differ from what the client requested if auth scopes are narrower.
|
|
2588
2681
|
...this.authContext?.scopes ? { acceptedScope: this.authContext.scopes } : {}
|
|
@@ -2590,7 +2683,7 @@ var ClientSession = class {
|
|
|
2590
2683
|
this.sendToClient(response);
|
|
2591
2684
|
this.emitter?.emit({ type: "sync:connected", nodeId: msg.nodeId });
|
|
2592
2685
|
this.state = "syncing";
|
|
2593
|
-
const clientVector = (0,
|
|
2686
|
+
const clientVector = (0, import_sync3.wireToVersionVector)(msg.versionVector);
|
|
2594
2687
|
await this.sendDelta(clientVector);
|
|
2595
2688
|
this.state = "streaming";
|
|
2596
2689
|
}
|
|
@@ -2628,10 +2721,39 @@ var ClientSession = class {
|
|
|
2628
2721
|
);
|
|
2629
2722
|
continue;
|
|
2630
2723
|
}
|
|
2724
|
+
if (this.validateOperation && this.koraContext) {
|
|
2725
|
+
let decision;
|
|
2726
|
+
try {
|
|
2727
|
+
decision = await this.validateOperation(op, {
|
|
2728
|
+
auth: this.authContext,
|
|
2729
|
+
kora: this.koraContext
|
|
2730
|
+
});
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
const message = error instanceof Error ? error.message : "validator error";
|
|
2733
|
+
this.sendOperationRejected(op, "VALIDATION_ERROR", `Validator threw: ${message}`, true);
|
|
2734
|
+
continue;
|
|
2735
|
+
}
|
|
2736
|
+
if (decision.action === "reject") {
|
|
2737
|
+
this.sendOperationRejected(
|
|
2738
|
+
op,
|
|
2739
|
+
decision.code,
|
|
2740
|
+
decision.message,
|
|
2741
|
+
decision.retriable ?? isRetriableRejection(decision.code)
|
|
2742
|
+
);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
if (decision.action === "ignore") {
|
|
2746
|
+
continue;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2631
2749
|
try {
|
|
2632
2750
|
const applyResult = await applyServerOperation(this.store, op);
|
|
2633
2751
|
if (applyResult.rejection) {
|
|
2634
|
-
this.sendError(
|
|
2752
|
+
this.sendError(
|
|
2753
|
+
applyResult.rejection.code,
|
|
2754
|
+
applyResult.rejection.message,
|
|
2755
|
+
applyResult.rejection.retriable
|
|
2756
|
+
);
|
|
2635
2757
|
continue;
|
|
2636
2758
|
}
|
|
2637
2759
|
if (applyResult.result === "applied") {
|
|
@@ -2692,7 +2814,7 @@ var ClientSession = class {
|
|
|
2692
2814
|
return;
|
|
2693
2815
|
}
|
|
2694
2816
|
const sorted = (0, import_internal.topologicalSort)(missing);
|
|
2695
|
-
const afterCursor = (0,
|
|
2817
|
+
const afterCursor = (0, import_sync3.sliceOperationsAfterCursor)(sorted, this.resumeDeltaCursor);
|
|
2696
2818
|
const totalBatches = Math.ceil(afterCursor.length / this.batchSize);
|
|
2697
2819
|
if (afterCursor.length === 0) {
|
|
2698
2820
|
const emptyBatch = {
|
|
@@ -2710,7 +2832,7 @@ var ClientSession = class {
|
|
|
2710
2832
|
const start = i * this.batchSize;
|
|
2711
2833
|
const batchOps = afterCursor.slice(start, start + this.batchSize);
|
|
2712
2834
|
const serializedOps = batchOps.map((op) => this.serializer.encodeOperation(op));
|
|
2713
|
-
const batchCursor = (0,
|
|
2835
|
+
const batchCursor = (0, import_sync3.createDeltaCursorFromBatch)(batchOps, i);
|
|
2714
2836
|
const batchMsg = {
|
|
2715
2837
|
type: "operation-batch",
|
|
2716
2838
|
messageId: (0, import_core10.generateUUIDv7)(),
|
|
@@ -2718,7 +2840,7 @@ var ClientSession = class {
|
|
|
2718
2840
|
isFinal: i === totalBatches - 1,
|
|
2719
2841
|
batchIndex: i,
|
|
2720
2842
|
totalBatches,
|
|
2721
|
-
...batchCursor ? { cursor: (0,
|
|
2843
|
+
...batchCursor ? { cursor: (0, import_sync3.encodeDeltaCursor)(batchCursor) } : {}
|
|
2722
2844
|
};
|
|
2723
2845
|
this.sendToClient(batchMsg);
|
|
2724
2846
|
this.emitter?.emit({
|
|
@@ -2732,7 +2854,7 @@ var ClientSession = class {
|
|
|
2732
2854
|
if (!operationMatchesScopes(op, this.authContext?.scopes)) {
|
|
2733
2855
|
return false;
|
|
2734
2856
|
}
|
|
2735
|
-
return (0,
|
|
2857
|
+
return (0, import_sync3.operationMatchesQuerySubsets)(op, this.syncQuerySubsets);
|
|
2736
2858
|
}
|
|
2737
2859
|
handleAwarenessUpdate(msg) {
|
|
2738
2860
|
this.onAwarenessUpdate?.(this.sessionId, msg);
|
|
@@ -2750,6 +2872,25 @@ var ClientSession = class {
|
|
|
2750
2872
|
};
|
|
2751
2873
|
this.sendToClient(errorMsg);
|
|
2752
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Reject one specific client operation, tied to its id, so the submitter can
|
|
2877
|
+
* divert it out of its pending queue into a durable rejected store rather than
|
|
2878
|
+
* losing it or retrying forever. The op is NOT materialized, so no other
|
|
2879
|
+
* replica ever sees it.
|
|
2880
|
+
*/
|
|
2881
|
+
sendOperationRejected(op, code, message, retriable) {
|
|
2882
|
+
const rejectedMsg = {
|
|
2883
|
+
type: "operation-rejected",
|
|
2884
|
+
messageId: (0, import_core10.generateUUIDv7)(),
|
|
2885
|
+
operationId: op.id,
|
|
2886
|
+
collection: op.collection,
|
|
2887
|
+
recordId: op.recordId,
|
|
2888
|
+
code,
|
|
2889
|
+
message,
|
|
2890
|
+
retriable
|
|
2891
|
+
};
|
|
2892
|
+
this.sendToClient(rejectedMsg);
|
|
2893
|
+
}
|
|
2753
2894
|
setSerializerWireFormat(format) {
|
|
2754
2895
|
if (typeof this.serializer.setWireFormat === "function") {
|
|
2755
2896
|
this.serializer.setWireFormat(format);
|
|
@@ -2771,9 +2912,9 @@ function selectWireFormat(supportedWireFormats) {
|
|
|
2771
2912
|
|
|
2772
2913
|
// src/server/kora-sync-server.ts
|
|
2773
2914
|
init_cjs_shims();
|
|
2774
|
-
var
|
|
2915
|
+
var import_core13 = require("@korajs/core");
|
|
2775
2916
|
var import_internal2 = require("@korajs/core/internal");
|
|
2776
|
-
var
|
|
2917
|
+
var import_sync5 = require("@korajs/sync");
|
|
2777
2918
|
|
|
2778
2919
|
// src/awareness/awareness-relay.ts
|
|
2779
2920
|
init_cjs_shims();
|
|
@@ -2990,6 +3131,114 @@ var ServerMetricsCollector = class {
|
|
|
2990
3131
|
}
|
|
2991
3132
|
};
|
|
2992
3133
|
|
|
3134
|
+
// src/richtext/blob-chunk-relay.ts
|
|
3135
|
+
init_cjs_shims();
|
|
3136
|
+
var import_sync4 = require("@korajs/sync");
|
|
3137
|
+
var BlobChunkRelay = class {
|
|
3138
|
+
clients = /* @__PURE__ */ new Map();
|
|
3139
|
+
/** requestId -> the session that originated the request (to route the answer back). */
|
|
3140
|
+
pending = /* @__PURE__ */ new Map();
|
|
3141
|
+
resolveBlobChunk;
|
|
3142
|
+
constructor(resolveBlobChunk) {
|
|
3143
|
+
this.resolveBlobChunk = resolveBlobChunk ?? null;
|
|
3144
|
+
}
|
|
3145
|
+
addClient(sessionId, transport) {
|
|
3146
|
+
this.clients.set(sessionId, { sessionId, transport });
|
|
3147
|
+
}
|
|
3148
|
+
removeClient(sessionId) {
|
|
3149
|
+
this.clients.delete(sessionId);
|
|
3150
|
+
for (const [requestId, originSessionId] of this.pending) {
|
|
3151
|
+
if (originSessionId === sessionId) {
|
|
3152
|
+
this.pending.delete(requestId);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
getClientCount() {
|
|
3157
|
+
return this.clients.size;
|
|
3158
|
+
}
|
|
3159
|
+
getPendingCount() {
|
|
3160
|
+
return this.pending.size;
|
|
3161
|
+
}
|
|
3162
|
+
clear() {
|
|
3163
|
+
this.clients.clear();
|
|
3164
|
+
this.pending.clear();
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Handle an inbound chunk request from a session. Tries the server's own
|
|
3168
|
+
* store first (if configured), otherwise forwards the request to peer
|
|
3169
|
+
* sessions and remembers who to route the answer back to.
|
|
3170
|
+
*/
|
|
3171
|
+
handleRequest(sourceSessionId, message) {
|
|
3172
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
if (this.resolveBlobChunk) {
|
|
3176
|
+
void this.resolveBlobChunk(message.hash).then(
|
|
3177
|
+
(bytes) => {
|
|
3178
|
+
if (bytes !== null) {
|
|
3179
|
+
this.sendResponseTo(sourceSessionId, message.requestId, (0, import_sync4.encodeBlobChunkBytes)(bytes));
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3183
|
+
},
|
|
3184
|
+
() => {
|
|
3185
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3186
|
+
}
|
|
3187
|
+
);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
this.forwardRequestToPeers(sourceSessionId, message);
|
|
3191
|
+
}
|
|
3192
|
+
/**
|
|
3193
|
+
* Handle an inbound chunk response from a peer. Routes it back to the session
|
|
3194
|
+
* that originally requested it. A "not held" answer (bytes === null) is
|
|
3195
|
+
* ignored so a peer without the chunk does not preempt one that has it; the
|
|
3196
|
+
* requester's own per-request timeout bounds the wait.
|
|
3197
|
+
*/
|
|
3198
|
+
handleResponse(sourceSessionId, message) {
|
|
3199
|
+
if (!this.clients.has(sourceSessionId)) {
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
if (message.bytes === null) {
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
const originSessionId = this.pending.get(message.requestId);
|
|
3206
|
+
if (!originSessionId) {
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
this.pending.delete(message.requestId);
|
|
3210
|
+
this.sendResponseTo(originSessionId, message.requestId, message.bytes);
|
|
3211
|
+
}
|
|
3212
|
+
forwardRequestToPeers(sourceSessionId, message) {
|
|
3213
|
+
this.pending.set(message.requestId, sourceSessionId);
|
|
3214
|
+
for (const [, client] of this.clients) {
|
|
3215
|
+
if (client.sessionId === sourceSessionId) {
|
|
3216
|
+
continue;
|
|
3217
|
+
}
|
|
3218
|
+
if (!client.transport.isConnected()) {
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
client.transport.send(message);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
sendResponseTo(sessionId, requestId, bytes) {
|
|
3225
|
+
const client = this.clients.get(sessionId);
|
|
3226
|
+
if (!client || !client.transport.isConnected()) {
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
const response = {
|
|
3230
|
+
type: "blob-chunk-response",
|
|
3231
|
+
messageId: generateResponseId(requestId),
|
|
3232
|
+
requestId,
|
|
3233
|
+
bytes
|
|
3234
|
+
};
|
|
3235
|
+
client.transport.send(response);
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
function generateResponseId(requestId) {
|
|
3239
|
+
return `blob-resp-${requestId}`;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
2993
3242
|
// src/richtext/yjs-doc-relay.ts
|
|
2994
3243
|
init_cjs_shims();
|
|
2995
3244
|
var YjsDocRelay = class {
|
|
@@ -3025,6 +3274,170 @@ var YjsDocRelay = class {
|
|
|
3025
3274
|
}
|
|
3026
3275
|
};
|
|
3027
3276
|
|
|
3277
|
+
// src/server/route-context.ts
|
|
3278
|
+
init_cjs_shims();
|
|
3279
|
+
var import_core12 = require("@korajs/core");
|
|
3280
|
+
function recordMatchesScope(collection, record, scope) {
|
|
3281
|
+
const collectionScope = scope[collection];
|
|
3282
|
+
if (!collectionScope) {
|
|
3283
|
+
return false;
|
|
3284
|
+
}
|
|
3285
|
+
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
3286
|
+
if (record[field] !== expected) {
|
|
3287
|
+
return false;
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
return true;
|
|
3291
|
+
}
|
|
3292
|
+
function pickFields(record, keys) {
|
|
3293
|
+
const picked = {};
|
|
3294
|
+
for (const key of keys) {
|
|
3295
|
+
if (key in record) {
|
|
3296
|
+
picked[key] = record[key];
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
return picked;
|
|
3300
|
+
}
|
|
3301
|
+
async function buildRouteOperation(store, mutation) {
|
|
3302
|
+
const nodeId = store.getNodeId();
|
|
3303
|
+
const clock = new import_core12.HybridLogicalClock(nodeId);
|
|
3304
|
+
const schemaVersion = store.getSchema()?.version ?? 1;
|
|
3305
|
+
const sequenceNumber = nextServerSequenceNumber(store);
|
|
3306
|
+
if (mutation.type === "insert") {
|
|
3307
|
+
return (0, import_core12.createOperation)(
|
|
3308
|
+
{
|
|
3309
|
+
nodeId,
|
|
3310
|
+
type: "insert",
|
|
3311
|
+
collection: mutation.collection,
|
|
3312
|
+
recordId: mutation.recordId ?? (0, import_core12.generateUUIDv7)(),
|
|
3313
|
+
data: mutation.data ?? {},
|
|
3314
|
+
previousData: null,
|
|
3315
|
+
sequenceNumber,
|
|
3316
|
+
causalDeps: [],
|
|
3317
|
+
schemaVersion
|
|
3318
|
+
},
|
|
3319
|
+
clock
|
|
3320
|
+
);
|
|
3321
|
+
}
|
|
3322
|
+
if (!mutation.recordId) {
|
|
3323
|
+
throw new RouteMutationError(
|
|
3324
|
+
"MISSING_RECORD_ID",
|
|
3325
|
+
`A ${mutation.type} mutation on "${mutation.collection}" requires a recordId.`
|
|
3326
|
+
);
|
|
3327
|
+
}
|
|
3328
|
+
const current = await store.findRecord(mutation.collection, mutation.recordId);
|
|
3329
|
+
if (mutation.type === "update") {
|
|
3330
|
+
const data = mutation.data ?? {};
|
|
3331
|
+
return (0, import_core12.createOperation)(
|
|
3332
|
+
{
|
|
3333
|
+
nodeId,
|
|
3334
|
+
type: "update",
|
|
3335
|
+
collection: mutation.collection,
|
|
3336
|
+
recordId: mutation.recordId,
|
|
3337
|
+
data,
|
|
3338
|
+
previousData: current ? pickFields(current, Object.keys(data)) : {},
|
|
3339
|
+
sequenceNumber,
|
|
3340
|
+
causalDeps: [],
|
|
3341
|
+
schemaVersion
|
|
3342
|
+
},
|
|
3343
|
+
clock
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
return (0, import_core12.createOperation)(
|
|
3347
|
+
{
|
|
3348
|
+
nodeId,
|
|
3349
|
+
type: "delete",
|
|
3350
|
+
collection: mutation.collection,
|
|
3351
|
+
recordId: mutation.recordId,
|
|
3352
|
+
data: null,
|
|
3353
|
+
previousData: current ? { ...current } : null,
|
|
3354
|
+
sequenceNumber,
|
|
3355
|
+
causalDeps: [],
|
|
3356
|
+
schemaVersion
|
|
3357
|
+
},
|
|
3358
|
+
clock
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
var RouteMutationError = class extends Error {
|
|
3362
|
+
constructor(code, message) {
|
|
3363
|
+
super(message);
|
|
3364
|
+
this.code = code;
|
|
3365
|
+
this.name = "RouteMutationError";
|
|
3366
|
+
}
|
|
3367
|
+
code;
|
|
3368
|
+
};
|
|
3369
|
+
function createRouteContext(server, store) {
|
|
3370
|
+
let mutationTail = Promise.resolve();
|
|
3371
|
+
function serialize(work) {
|
|
3372
|
+
const run = mutationTail.then(work, work);
|
|
3373
|
+
mutationTail = run.then(
|
|
3374
|
+
() => void 0,
|
|
3375
|
+
() => void 0
|
|
3376
|
+
);
|
|
3377
|
+
return run;
|
|
3378
|
+
}
|
|
3379
|
+
return {
|
|
3380
|
+
apply(mutation, options) {
|
|
3381
|
+
return serialize(async () => {
|
|
3382
|
+
let op;
|
|
3383
|
+
try {
|
|
3384
|
+
op = await buildRouteOperation(store, mutation);
|
|
3385
|
+
} catch (error) {
|
|
3386
|
+
if (error instanceof RouteMutationError) {
|
|
3387
|
+
return {
|
|
3388
|
+
ok: false,
|
|
3389
|
+
code: error.code,
|
|
3390
|
+
message: error.message,
|
|
3391
|
+
retriable: isRetriableRejection(error.code)
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
throw error;
|
|
3395
|
+
}
|
|
3396
|
+
if (options?.scope && !operationMatchesScopes(op, options.scope)) {
|
|
3397
|
+
return {
|
|
3398
|
+
ok: false,
|
|
3399
|
+
code: "SCOPE_VIOLATION",
|
|
3400
|
+
message: `Mutation on "${mutation.collection}" is outside the provided scope.`,
|
|
3401
|
+
retriable: isRetriableRejection("SCOPE_VIOLATION")
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
const result = await server.applyLocalOperation(op);
|
|
3405
|
+
if (result.result !== "applied") {
|
|
3406
|
+
const code = result.rejection?.code ?? "NOT_APPLIED";
|
|
3407
|
+
return {
|
|
3408
|
+
ok: false,
|
|
3409
|
+
code,
|
|
3410
|
+
message: result.rejection?.message ?? `Operation on "${mutation.collection}" was not applied (${result.result}).`,
|
|
3411
|
+
// Prefer the pipeline's own classification when present; fall back
|
|
3412
|
+
// to classifying the code so this stays correct if new codes appear.
|
|
3413
|
+
retriable: result.rejection?.retriable ?? isRetriableRejection(code)
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
const record = await store.findRecord(mutation.collection, op.recordId);
|
|
3417
|
+
return { ok: true, operation: op, record };
|
|
3418
|
+
});
|
|
3419
|
+
},
|
|
3420
|
+
async query(collection, options) {
|
|
3421
|
+
const { scope, ...queryOptions } = options ?? {};
|
|
3422
|
+
const records = await store.queryCollection(collection, queryOptions);
|
|
3423
|
+
if (!scope) {
|
|
3424
|
+
return records;
|
|
3425
|
+
}
|
|
3426
|
+
return records.filter((record) => recordMatchesScope(collection, record, scope));
|
|
3427
|
+
},
|
|
3428
|
+
async findById(collection, id, options) {
|
|
3429
|
+
const record = await store.findRecord(collection, id);
|
|
3430
|
+
if (!record) {
|
|
3431
|
+
return null;
|
|
3432
|
+
}
|
|
3433
|
+
if (options?.scope && !recordMatchesScope(collection, record, options.scope)) {
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
return record;
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3028
3441
|
// src/server/kora-sync-server.ts
|
|
3029
3442
|
var DEFAULT_MAX_CONNECTIONS = 0;
|
|
3030
3443
|
var DEFAULT_BATCH_SIZE2 = 100;
|
|
@@ -3047,16 +3460,24 @@ var KoraSyncServer = class {
|
|
|
3047
3460
|
metrics;
|
|
3048
3461
|
awarenessRelay = new AwarenessRelay();
|
|
3049
3462
|
yjsDocRelay = new YjsDocRelay();
|
|
3463
|
+
blobChunkRelay;
|
|
3464
|
+
persistBlobChunk;
|
|
3465
|
+
maxOperationBytes;
|
|
3466
|
+
maxOpsPerMinute;
|
|
3467
|
+
validateOperation;
|
|
3468
|
+
koraContext;
|
|
3050
3469
|
sessions = /* @__PURE__ */ new Map();
|
|
3051
3470
|
httpClients = /* @__PURE__ */ new Map();
|
|
3052
3471
|
httpSessionToClient = /* @__PURE__ */ new Map();
|
|
3053
|
-
|
|
3472
|
+
// Informational value reported by getStatus(). Keep in sync with the
|
|
3473
|
+
// @korajs/server package version on release; it is not used for protocol negotiation.
|
|
3474
|
+
serverVersion = "1.0.0-beta.0";
|
|
3054
3475
|
wsServer = null;
|
|
3055
3476
|
running = false;
|
|
3056
3477
|
constructor(config) {
|
|
3057
3478
|
this.store = config.store;
|
|
3058
3479
|
this.auth = config.auth ?? null;
|
|
3059
|
-
this.serializer = config.serializer ?? new
|
|
3480
|
+
this.serializer = config.serializer ?? new import_sync5.JsonMessageSerializer();
|
|
3060
3481
|
this.emitter = config.emitter ?? null;
|
|
3061
3482
|
this.maxConnections = config.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
|
|
3062
3483
|
this.batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE2;
|
|
@@ -3071,10 +3492,24 @@ var KoraSyncServer = class {
|
|
|
3071
3492
|
this.logger = config.logger ?? createDefaultLogger();
|
|
3072
3493
|
this.metrics = config.metricsCollector ?? new ServerMetricsCollector();
|
|
3073
3494
|
this.metrics.setSchemaVersion(this.schemaVersion);
|
|
3495
|
+
this.blobChunkRelay = new BlobChunkRelay(config.resolveBlobChunk);
|
|
3496
|
+
this.persistBlobChunk = config.persistBlobChunk ?? null;
|
|
3497
|
+
this.maxOperationBytes = config.maxOperationBytes;
|
|
3498
|
+
this.maxOpsPerMinute = config.maxOpsPerMinute;
|
|
3499
|
+
this.validateOperation = config.validateOperation;
|
|
3500
|
+
this.koraContext = createRouteContext(this, this.store);
|
|
3074
3501
|
if (!this.emitter) {
|
|
3075
3502
|
this.emitter = new import_internal2.SimpleEventEmitter();
|
|
3076
3503
|
}
|
|
3077
3504
|
}
|
|
3505
|
+
/**
|
|
3506
|
+
* The trusted, scoped data-plane context (`apply` / `query` / `findById`).
|
|
3507
|
+
* Shared with the production server so HTTP routes, the operation validator,
|
|
3508
|
+
* and `server.kora` are all the same object over the same pipeline.
|
|
3509
|
+
*/
|
|
3510
|
+
getKoraContext() {
|
|
3511
|
+
return this.koraContext;
|
|
3512
|
+
}
|
|
3078
3513
|
/**
|
|
3079
3514
|
* Subscribe to session-level events for metrics collection and logging.
|
|
3080
3515
|
* Called when a new session is created.
|
|
@@ -3143,10 +3578,10 @@ var KoraSyncServer = class {
|
|
|
3143
3578
|
*/
|
|
3144
3579
|
async start(wsServerImpl) {
|
|
3145
3580
|
if (this.running) {
|
|
3146
|
-
throw new
|
|
3581
|
+
throw new import_core13.SyncError("Server is already running", { port: this.port });
|
|
3147
3582
|
}
|
|
3148
3583
|
if (!wsServerImpl && this.port === void 0) {
|
|
3149
|
-
throw new
|
|
3584
|
+
throw new import_core13.SyncError(
|
|
3150
3585
|
"Port is required for standalone mode. Provide port in config or use handleConnection() for attach mode.",
|
|
3151
3586
|
{}
|
|
3152
3587
|
);
|
|
@@ -3194,6 +3629,7 @@ var KoraSyncServer = class {
|
|
|
3194
3629
|
});
|
|
3195
3630
|
this.awarenessRelay.clear();
|
|
3196
3631
|
this.yjsDocRelay.clear();
|
|
3632
|
+
this.blobChunkRelay.clear();
|
|
3197
3633
|
for (const session of this.sessions.values()) {
|
|
3198
3634
|
session.close("server shutting down");
|
|
3199
3635
|
}
|
|
@@ -3255,7 +3691,7 @@ var KoraSyncServer = class {
|
|
|
3255
3691
|
if (this.maxConnections > 0 && this.sessions.size >= this.maxConnections) {
|
|
3256
3692
|
transport.send({
|
|
3257
3693
|
type: "error",
|
|
3258
|
-
messageId: (0,
|
|
3694
|
+
messageId: (0, import_core13.generateUUIDv7)(),
|
|
3259
3695
|
code: "MAX_CONNECTIONS",
|
|
3260
3696
|
message: `Server has reached maximum connections (${this.maxConnections})`,
|
|
3261
3697
|
retriable: true
|
|
@@ -3268,12 +3704,12 @@ var KoraSyncServer = class {
|
|
|
3268
3704
|
event: "connection.rejected",
|
|
3269
3705
|
details: { reason: "max_connections", max: this.maxConnections }
|
|
3270
3706
|
});
|
|
3271
|
-
throw new
|
|
3707
|
+
throw new import_core13.SyncError("Maximum connections reached", {
|
|
3272
3708
|
current: this.sessions.size,
|
|
3273
3709
|
max: this.maxConnections
|
|
3274
3710
|
});
|
|
3275
3711
|
}
|
|
3276
|
-
const sessionId = (0,
|
|
3712
|
+
const sessionId = (0, import_core13.generateUUIDv7)();
|
|
3277
3713
|
this.metrics.recordConnection(sessionId);
|
|
3278
3714
|
const sessionEmitter = new import_internal2.SimpleEventEmitter();
|
|
3279
3715
|
sessionEmitter.on("sync:connected", (event) => {
|
|
@@ -3338,12 +3774,25 @@ var KoraSyncServer = class {
|
|
|
3338
3774
|
onYjsDocUpdate: (sourceSessionId, message) => {
|
|
3339
3775
|
this.handleYjsDocRelay(sourceSessionId, message);
|
|
3340
3776
|
},
|
|
3777
|
+
onBlobChunkRequest: (sourceSessionId, message) => {
|
|
3778
|
+
this.blobChunkRelay.handleRequest(sourceSessionId, message);
|
|
3779
|
+
},
|
|
3780
|
+
onBlobChunkResponse: (sourceSessionId, message) => {
|
|
3781
|
+
this.blobChunkRelay.handleResponse(sourceSessionId, message);
|
|
3782
|
+
},
|
|
3783
|
+
...this.persistBlobChunk ? { persistBlobChunk: this.persistBlobChunk } : {},
|
|
3784
|
+
// Only forward when configured, so an unset server value leaves the
|
|
3785
|
+
// session on its own documented default rather than `undefined`.
|
|
3786
|
+
...this.maxOperationBytes !== void 0 ? { maxOperationBytes: this.maxOperationBytes } : {},
|
|
3787
|
+
...this.maxOpsPerMinute !== void 0 ? { maxOpsPerMinute: this.maxOpsPerMinute } : {},
|
|
3788
|
+
...this.validateOperation ? { validateOperation: this.validateOperation, koraContext: this.koraContext } : {},
|
|
3341
3789
|
onClose: (sid) => {
|
|
3342
3790
|
this.handleSessionClose(sid);
|
|
3343
3791
|
}
|
|
3344
3792
|
});
|
|
3345
3793
|
this.sessions.set(sessionId, session);
|
|
3346
3794
|
this.yjsDocRelay.addClient(sessionId, transport);
|
|
3795
|
+
this.blobChunkRelay.addClient(sessionId, transport);
|
|
3347
3796
|
session.start();
|
|
3348
3797
|
this.logger.log({
|
|
3349
3798
|
timestamp: Date.now(),
|
|
@@ -3376,6 +3825,61 @@ var KoraSyncServer = class {
|
|
|
3376
3825
|
errorCount: snapshot.errorCount
|
|
3377
3826
|
};
|
|
3378
3827
|
}
|
|
3828
|
+
/**
|
|
3829
|
+
* Apply a server-originated operation (for example one created by a custom
|
|
3830
|
+
* HTTP route) through the same validated pipeline that incoming client
|
|
3831
|
+
* operations use — Tier 2 constraints, referential integrity, and cascade
|
|
3832
|
+
* side effects — then relay every applied operation to connected clients.
|
|
3833
|
+
*
|
|
3834
|
+
* Because the operation did not come from a client session, it is relayed to
|
|
3835
|
+
* ALL sessions (there is no source session to exclude). Each session still
|
|
3836
|
+
* applies its own per-scope visibility filter in `relayOperations`, so a
|
|
3837
|
+
* client only receives the operation if it falls within that client's scope.
|
|
3838
|
+
*
|
|
3839
|
+
* @param op - A fully-formed, server-originated operation to apply
|
|
3840
|
+
* @returns The apply result, including any server-generated side-effect ops
|
|
3841
|
+
*/
|
|
3842
|
+
async applyLocalOperation(op) {
|
|
3843
|
+
const result = await applyServerOperation(this.store, op);
|
|
3844
|
+
if (result.result === "applied" && result.appliedOperations.length > 0) {
|
|
3845
|
+
for (const session of this.sessions.values()) {
|
|
3846
|
+
session.relayOperations(result.appliedOperations);
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
return result;
|
|
3850
|
+
}
|
|
3851
|
+
/**
|
|
3852
|
+
* Collect every blob reference still reachable from live records on the server.
|
|
3853
|
+
*
|
|
3854
|
+
* This is the live set for garbage-collecting the server's central blob store:
|
|
3855
|
+
* pass the result to `collectBlobGarbage(blobStore, liveRefs)` from
|
|
3856
|
+
* `@korajs/store` to reclaim bytes no record references any more. Only
|
|
3857
|
+
* collections that declare a `blob` field are scanned.
|
|
3858
|
+
*
|
|
3859
|
+
* @returns Every live blob reference across all collections
|
|
3860
|
+
*/
|
|
3861
|
+
async getLiveBlobRefs() {
|
|
3862
|
+
const schema = this.store.getSchema();
|
|
3863
|
+
if (!schema) {
|
|
3864
|
+
return [];
|
|
3865
|
+
}
|
|
3866
|
+
const refs = [];
|
|
3867
|
+
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
3868
|
+
const hasBlobField = Object.values(collection.fields).some((field) => field.kind === "blob");
|
|
3869
|
+
if (!hasBlobField) {
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
const records = await this.store.materializeCollection(name);
|
|
3873
|
+
for (const record of records) {
|
|
3874
|
+
for (const value of Object.values(record)) {
|
|
3875
|
+
if ((0, import_core13.isBlobRef)(value)) {
|
|
3876
|
+
refs.push(value);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return refs;
|
|
3882
|
+
}
|
|
3379
3883
|
/**
|
|
3380
3884
|
* Get the number of currently connected clients.
|
|
3381
3885
|
*/
|
|
@@ -3409,6 +3913,7 @@ var KoraSyncServer = class {
|
|
|
3409
3913
|
this.metrics.recordDisconnection(sessionId);
|
|
3410
3914
|
this.awarenessRelay.removeClient(sessionId);
|
|
3411
3915
|
this.yjsDocRelay.removeClient(sessionId);
|
|
3916
|
+
this.blobChunkRelay.removeClient(sessionId);
|
|
3412
3917
|
this.sessions.delete(sessionId);
|
|
3413
3918
|
const clientId = this.httpSessionToClient.get(sessionId);
|
|
3414
3919
|
if (clientId) {
|
|
@@ -3461,14 +3966,6 @@ function normalizeHttpBody(body, contentType) {
|
|
|
3461
3966
|
return body;
|
|
3462
3967
|
}
|
|
3463
3968
|
|
|
3464
|
-
// src/auth/no-auth.ts
|
|
3465
|
-
init_cjs_shims();
|
|
3466
|
-
var NoAuthProvider = class {
|
|
3467
|
-
async authenticate(_token) {
|
|
3468
|
-
return { userId: "anonymous" };
|
|
3469
|
-
}
|
|
3470
|
-
};
|
|
3471
|
-
|
|
3472
3969
|
// src/auth/token-auth.ts
|
|
3473
3970
|
init_cjs_shims();
|
|
3474
3971
|
var TokenAuthProvider = class {
|
|
@@ -3578,6 +4075,7 @@ function createProductionServer(config) {
|
|
|
3578
4075
|
enableDashboard: true,
|
|
3579
4076
|
...config.syncOptions
|
|
3580
4077
|
});
|
|
4078
|
+
const routeContext = syncServer.getKoraContext();
|
|
3581
4079
|
let httpServer = null;
|
|
3582
4080
|
function getOperationalToken(kind) {
|
|
3583
4081
|
const auth = config.operationalAuth;
|
|
@@ -3664,6 +4162,10 @@ function createProductionServer(config) {
|
|
|
3664
4162
|
const chunks = [];
|
|
3665
4163
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
3666
4164
|
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
4165
|
+
if (!req.readableFlowing) {
|
|
4166
|
+
req.resume();
|
|
4167
|
+
}
|
|
4168
|
+
req.on("error", () => resolve(Buffer.concat(chunks)));
|
|
3667
4169
|
});
|
|
3668
4170
|
}
|
|
3669
4171
|
async function readJsonBody(req) {
|
|
@@ -3713,6 +4215,10 @@ function createProductionServer(config) {
|
|
|
3713
4215
|
res.end(JSON.stringify(result.body ?? null));
|
|
3714
4216
|
}
|
|
3715
4217
|
return {
|
|
4218
|
+
kora: routeContext,
|
|
4219
|
+
getLiveBlobRefs() {
|
|
4220
|
+
return syncServer.getLiveBlobRefs();
|
|
4221
|
+
},
|
|
3716
4222
|
async start() {
|
|
3717
4223
|
const { createServer } = await import("http");
|
|
3718
4224
|
const { createReadStream, existsSync, statSync } = await import("fs");
|
|
@@ -3720,6 +4226,19 @@ function createProductionServer(config) {
|
|
|
3720
4226
|
const { WebSocketServer } = await import("ws");
|
|
3721
4227
|
const distDir = resolve(staticDir);
|
|
3722
4228
|
httpServer = createServer(async (req, res) => {
|
|
4229
|
+
try {
|
|
4230
|
+
await handleRequest(req, res);
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
console.error("[kora] Unhandled error in HTTP request handler:", error);
|
|
4233
|
+
if (!res.headersSent) {
|
|
4234
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
4235
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
4236
|
+
} else if (!res.writableEnded) {
|
|
4237
|
+
res.end();
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
});
|
|
4241
|
+
async function handleRequest(req, res) {
|
|
3723
4242
|
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
|
|
3724
4243
|
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
|
|
3725
4244
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
@@ -3847,7 +4366,8 @@ data: ${JSON.stringify(s)}
|
|
|
3847
4366
|
body: await readJsonBody(req),
|
|
3848
4367
|
headers: req.headers,
|
|
3849
4368
|
query: getQuery(url),
|
|
3850
|
-
ip: getClientIp(req)
|
|
4369
|
+
ip: getClientIp(req),
|
|
4370
|
+
kora: routeContext
|
|
3851
4371
|
});
|
|
3852
4372
|
writeJsonResponse(res, result);
|
|
3853
4373
|
return;
|
|
@@ -3881,7 +4401,7 @@ data: ${JSON.stringify(s)}
|
|
|
3881
4401
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
3882
4402
|
res.writeHead(200, { "Content-Type": contentType });
|
|
3883
4403
|
createReadStream(filePath).pipe(res);
|
|
3884
|
-
}
|
|
4404
|
+
}
|
|
3885
4405
|
const wss = new WebSocketServer({ noServer: true });
|
|
3886
4406
|
httpServer.on("upgrade", (req, socket, head) => {
|
|
3887
4407
|
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
@@ -3998,6 +4518,7 @@ function formatUptime(ms) {
|
|
|
3998
4518
|
MixedAuthProvider,
|
|
3999
4519
|
NoAuthProvider,
|
|
4000
4520
|
PostgresServerStore,
|
|
4521
|
+
RETRIABLE_REJECTION_CODES,
|
|
4001
4522
|
SqliteServerStore,
|
|
4002
4523
|
TokenAuthProvider,
|
|
4003
4524
|
WsServerTransport,
|
|
@@ -4008,6 +4529,7 @@ function formatUptime(ms) {
|
|
|
4008
4529
|
createPrettyLogger,
|
|
4009
4530
|
createProductionServer,
|
|
4010
4531
|
createSilentLogger,
|
|
4011
|
-
createSqliteServerStore
|
|
4532
|
+
createSqliteServerStore,
|
|
4533
|
+
isRetriableRejection
|
|
4012
4534
|
});
|
|
4013
4535
|
//# sourceMappingURL=index.cjs.map
|