@crvouga/mockingbird-service-postgres 3.4.0 → 3.5.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 +18 -0
- package/COMPATIBILITY.md +13 -1
- package/dist/admin.js +1 -1
- package/dist/admin.js.map +2 -2
- package/dist/index.js +7 -5
- package/dist/index.js.map +2 -2
- package/dist/server.js +479 -28
- package/dist/server.js.map +4 -4
- package/dist/storage/columnar-slab.d.ts +2 -0
- package/dist/wire/cli.js +4274 -236
- package/dist/wire/cli.js.map +4 -4
- package/dist/wire/fleet.d.ts +38 -2
- package/dist/wire/index.js +7 -5
- package/dist/wire/index.js.map +2 -2
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -26967,7 +26967,7 @@ function readSlabCell(column, rowIndex, intern, rowCount) {
|
|
|
26967
26967
|
}
|
|
26968
26968
|
if (pack === PACK_INT) {
|
|
26969
26969
|
const integer2 = view.getBigInt64(nonNullIndex * 8, true);
|
|
26970
|
-
return integer2 <= BigInt(Number.MAX_SAFE_INTEGER) && integer2 >= BigInt(Number.MIN_SAFE_INTEGER) ? Number(integer2) : integer2;
|
|
26970
|
+
return !column.integerBigInt && integer2 <= BigInt(Number.MAX_SAFE_INTEGER) && integer2 >= BigInt(Number.MIN_SAFE_INTEGER) ? Number(integer2) : integer2;
|
|
26971
26971
|
}
|
|
26972
26972
|
if (pack === PACK_TEXT_INTERN) {
|
|
26973
26973
|
const id = view.getUint32(nonNullIndex * 4, true);
|
|
@@ -27528,7 +27528,8 @@ function decodeInner(snapshot, prng, clock) {
|
|
|
27528
27528
|
if (meta.stats) state.tableStats.set(table.oid, meta.stats);
|
|
27529
27529
|
const rowCount = readVarintU32(r);
|
|
27530
27530
|
const slabColumns = [];
|
|
27531
|
-
for (let c = 0; c < meta.columns.length; c++)
|
|
27531
|
+
for (let c = 0; c < meta.columns.length; c++)
|
|
27532
|
+
slabColumns.push(readPackedColumn(r, rowCount, intern, meta.columns[c].type.id));
|
|
27532
27533
|
table.attachSlab(new ColumnarSlab(snapshot, rowCount, slabColumns, intern));
|
|
27533
27534
|
schema.tables.set(table.name, table);
|
|
27534
27535
|
}
|
|
@@ -27541,7 +27542,7 @@ function decodeInner(snapshot, prng, clock) {
|
|
|
27541
27542
|
const matRowCount = readVarintU32(r);
|
|
27542
27543
|
const width = readVarintU32(r);
|
|
27543
27544
|
const cols = [];
|
|
27544
|
-
for (let c = 0; c < width; c++) cols.push(readPackedColumn(r, matRowCount, intern));
|
|
27545
|
+
for (let c = 0; c < width; c++) cols.push(readPackedColumn(r, matRowCount, intern, meta.matColumns?.[c]?.type));
|
|
27545
27546
|
const slab = new ColumnarSlab(snapshot, matRowCount, cols, intern);
|
|
27546
27547
|
matRows = slab.materialize();
|
|
27547
27548
|
}
|
|
@@ -27813,7 +27814,7 @@ function writePackedColumn(w, rows, col, internId, forceId) {
|
|
|
27813
27814
|
writeTaggedDatum(w, rows[i][col], internId, forceId);
|
|
27814
27815
|
}
|
|
27815
27816
|
}
|
|
27816
|
-
function readPackedColumn(r, n, intern) {
|
|
27817
|
+
function readPackedColumn(r, n, intern, type) {
|
|
27817
27818
|
const pack = r.u8();
|
|
27818
27819
|
if (pack === PACK_NULL) return { pack, nullBitmap: null, payload: new Uint8Array(0) };
|
|
27819
27820
|
const bits = r.raw(n + 7 >> 3);
|
|
@@ -27826,7 +27827,8 @@ function readPackedColumn(r, n, intern) {
|
|
|
27826
27827
|
r.skipAlign4();
|
|
27827
27828
|
const width = 8;
|
|
27828
27829
|
const payload = r.raw(nonNull * width);
|
|
27829
|
-
|
|
27830
|
+
const integerBigInt = type !== void 0 && ["int8", "money", "time", "timestamp", "timestamptz"].includes(type);
|
|
27831
|
+
return { pack, nullBitmap: bits, payload, integerBigInt };
|
|
27830
27832
|
}
|
|
27831
27833
|
if (pack === PACK_TEXT_INTERN) {
|
|
27832
27834
|
r.skipAlign4();
|
|
@@ -30531,6 +30533,9 @@ var start = async (options) => {
|
|
|
30531
30533
|
});
|
|
30532
30534
|
};
|
|
30533
30535
|
|
|
30536
|
+
// src/wire/fleet.ts
|
|
30537
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
30538
|
+
|
|
30534
30539
|
// ../core/dist/clock.js
|
|
30535
30540
|
var createClock = (source = Date.now) => {
|
|
30536
30541
|
let offsetMs = 0;
|
|
@@ -30572,6 +30577,310 @@ var createClock = (source = Date.now) => {
|
|
|
30572
30577
|
};
|
|
30573
30578
|
};
|
|
30574
30579
|
|
|
30580
|
+
// ../core/dist/controls.js
|
|
30581
|
+
var ControlError = class extends RangeError {
|
|
30582
|
+
constructor(message2) {
|
|
30583
|
+
super(message2);
|
|
30584
|
+
this.name = "ControlError";
|
|
30585
|
+
}
|
|
30586
|
+
};
|
|
30587
|
+
var UNITS = {
|
|
30588
|
+
ms: 1,
|
|
30589
|
+
s: 1e3,
|
|
30590
|
+
m: 6e4,
|
|
30591
|
+
h: 36e5,
|
|
30592
|
+
d: 864e5
|
|
30593
|
+
};
|
|
30594
|
+
var parseDuration = (value) => {
|
|
30595
|
+
if (typeof value === "number")
|
|
30596
|
+
return Number.isFinite(value) ? value : void 0;
|
|
30597
|
+
if (typeof value !== "string")
|
|
30598
|
+
return void 0;
|
|
30599
|
+
const match = /^(-?\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/.exec(value.trim());
|
|
30600
|
+
if (!match)
|
|
30601
|
+
return void 0;
|
|
30602
|
+
return Number(match[1]) * UNITS[match[2] ?? "ms"];
|
|
30603
|
+
};
|
|
30604
|
+
var parseInstant = (value) => {
|
|
30605
|
+
if (typeof value === "number")
|
|
30606
|
+
return Number.isFinite(value) ? value : void 0;
|
|
30607
|
+
if (typeof value !== "string")
|
|
30608
|
+
return void 0;
|
|
30609
|
+
const text = value.trim();
|
|
30610
|
+
if (/^-?\d+$/.test(text))
|
|
30611
|
+
return Number(text);
|
|
30612
|
+
const parsed = Date.parse(text);
|
|
30613
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
30614
|
+
};
|
|
30615
|
+
var durationMs = (value, label, negative = false) => {
|
|
30616
|
+
const parsed = parseDuration(value);
|
|
30617
|
+
if (parsed === void 0)
|
|
30618
|
+
throw new ControlError(`${label}: expected milliseconds or a duration like "250ms" or "15m"`);
|
|
30619
|
+
if (!negative && parsed < 0)
|
|
30620
|
+
throw new ControlError(`${label}: must not be negative`);
|
|
30621
|
+
return parsed;
|
|
30622
|
+
};
|
|
30623
|
+
var instantMs = (value, label) => {
|
|
30624
|
+
const parsed = parseInstant(value);
|
|
30625
|
+
if (parsed === void 0)
|
|
30626
|
+
throw new ControlError(`${label}: expected epoch ms or ISO-8601`);
|
|
30627
|
+
return parsed;
|
|
30628
|
+
};
|
|
30629
|
+
var normalizeSeed = (seed, label = "seed") => {
|
|
30630
|
+
if (typeof seed === "number" && Number.isFinite(seed))
|
|
30631
|
+
return seed;
|
|
30632
|
+
if (typeof seed === "string" && seed !== "")
|
|
30633
|
+
return /^\d+$/.test(seed) ? Number(seed) : seed;
|
|
30634
|
+
throw new ControlError(`${label}: expected a number or a non-empty string`);
|
|
30635
|
+
};
|
|
30636
|
+
var SNAPSHOT_FORMAT = "mockingbird.snapshot/v1";
|
|
30637
|
+
var NAMESPACE_KEYS = /* @__PURE__ */ new Set(["seed", "clock", "virtualTime", "latency", "faults", "snapshot"]);
|
|
30638
|
+
var MOCK_KEYS = /* @__PURE__ */ new Set([...NAMESPACE_KEYS, "credentials", "namespaces"]);
|
|
30639
|
+
var REPORTED_KEYS = /* @__PURE__ */ new Set(["namespace", "snapshots"]);
|
|
30640
|
+
var REPORTED_CLOCK_KEYS = /* @__PURE__ */ new Set(["iso", "offsetMs"]);
|
|
30641
|
+
var KEY_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
30642
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
30643
|
+
var unknownKeys = (value, known, label, reported) => {
|
|
30644
|
+
for (const key of Object.keys(value)) {
|
|
30645
|
+
if (!known.has(key) && !reported?.has(key))
|
|
30646
|
+
throw new ControlError(`${label}: unknown control ${JSON.stringify(key)}; expected ${[...known].join(", ")}`);
|
|
30647
|
+
}
|
|
30648
|
+
};
|
|
30649
|
+
var latencyRule = (value, label) => {
|
|
30650
|
+
if (!isRecord2(value))
|
|
30651
|
+
return { delayMs: durationMs(value, label), jitterMs: 0 };
|
|
30652
|
+
unknownKeys(value, /* @__PURE__ */ new Set(["delay", "jitter", "delayMs", "jitterMs", "operations"]), label);
|
|
30653
|
+
return {
|
|
30654
|
+
delayMs: durationMs(value.delay ?? value.delayMs ?? 0, `${label}.delay`),
|
|
30655
|
+
jitterMs: durationMs(value.jitter ?? value.jitterMs ?? 0, `${label}.jitter`)
|
|
30656
|
+
};
|
|
30657
|
+
};
|
|
30658
|
+
var normalizeLatency = (value, label = "latency") => {
|
|
30659
|
+
const base = latencyRule(value, label);
|
|
30660
|
+
const operations = {};
|
|
30661
|
+
if (isRecord2(value) && value.operations !== void 0) {
|
|
30662
|
+
if (!isRecord2(value.operations))
|
|
30663
|
+
throw new ControlError(`${label}.operations: expected an object keyed by operationId`);
|
|
30664
|
+
for (const [operationId, rule] of Object.entries(value.operations)) {
|
|
30665
|
+
if (isRecord2(rule) && rule.operations !== void 0)
|
|
30666
|
+
throw new ControlError(`${label}.operations.${operationId}: cannot nest operations`);
|
|
30667
|
+
operations[operationId] = latencyRule(rule, `${label}.operations.${operationId}`);
|
|
30668
|
+
}
|
|
30669
|
+
}
|
|
30670
|
+
return { ...base, operations };
|
|
30671
|
+
};
|
|
30672
|
+
var normalizeClock = (value, label = "clock") => {
|
|
30673
|
+
if (!isRecord2(value))
|
|
30674
|
+
throw new ControlError(`${label}: expected an object`);
|
|
30675
|
+
unknownKeys(value, /* @__PURE__ */ new Set(["now", "set", "frozen", "freeze", "autoAdvance", "autoAdvanceMs"]), label, REPORTED_CLOCK_KEYS);
|
|
30676
|
+
const plan = {};
|
|
30677
|
+
const now = value.now ?? value.set;
|
|
30678
|
+
if (now !== void 0)
|
|
30679
|
+
plan.now = instantMs(now, `${label}.now`);
|
|
30680
|
+
const frozen = value.frozen ?? value.freeze;
|
|
30681
|
+
if (frozen !== void 0) {
|
|
30682
|
+
if (typeof frozen !== "boolean")
|
|
30683
|
+
throw new ControlError(`${label}.frozen: expected a boolean`);
|
|
30684
|
+
plan.frozen = frozen;
|
|
30685
|
+
}
|
|
30686
|
+
const auto = value.autoAdvance ?? value.autoAdvanceMs;
|
|
30687
|
+
if (auto !== void 0)
|
|
30688
|
+
plan.autoAdvanceMs = durationMs(auto, `${label}.autoAdvance`);
|
|
30689
|
+
return plan;
|
|
30690
|
+
};
|
|
30691
|
+
var assertSnapshotDocument = (value, label = "snapshot") => {
|
|
30692
|
+
if (!isRecord2(value))
|
|
30693
|
+
throw new ControlError(`${label}: expected a snapshot document`);
|
|
30694
|
+
if (value.format !== SNAPSHOT_FORMAT)
|
|
30695
|
+
throw new ControlError(`${label}.format: expected ${JSON.stringify(SNAPSHOT_FORMAT)}`);
|
|
30696
|
+
if (!Array.isArray(value.records))
|
|
30697
|
+
throw new ControlError(`${label}.records: expected a list`);
|
|
30698
|
+
value.records.forEach((row, index) => {
|
|
30699
|
+
if (!isRecord2(row) || typeof row.collection !== "string" || typeof row.id !== "string" || row.value === void 0 && typeof row.raw !== "string" || row.seq !== void 0 && !Number.isSafeInteger(row.seq))
|
|
30700
|
+
throw new ControlError(`${label}.records[${index}]: expected {collection, id, value, seq?}`);
|
|
30701
|
+
});
|
|
30702
|
+
if (value.sequences !== void 0) {
|
|
30703
|
+
if (!Array.isArray(value.sequences))
|
|
30704
|
+
throw new ControlError(`${label}.sequences: expected a list`);
|
|
30705
|
+
value.sequences.forEach((row, index) => {
|
|
30706
|
+
if (!isRecord2(row) || typeof row.name !== "string" || typeof row.kind !== "string" || !Number.isSafeInteger(row.value))
|
|
30707
|
+
throw new ControlError(`${label}.sequences[${index}]: expected {name, kind, value}`);
|
|
30708
|
+
});
|
|
30709
|
+
}
|
|
30710
|
+
if (value.clock !== void 0) {
|
|
30711
|
+
if (!isRecord2(value.clock) || typeof value.clock.now !== "number" || !Number.isFinite(value.clock.now) || typeof value.clock.frozen !== "boolean")
|
|
30712
|
+
throw new ControlError(`${label}.clock: expected {now, frozen}`);
|
|
30713
|
+
}
|
|
30714
|
+
return value;
|
|
30715
|
+
};
|
|
30716
|
+
var normalizeControls = (value, label = "controls") => {
|
|
30717
|
+
if (!isRecord2(value))
|
|
30718
|
+
throw new ControlError(`${label}: expected an object`);
|
|
30719
|
+
unknownKeys(value, NAMESPACE_KEYS, label, REPORTED_KEYS);
|
|
30720
|
+
const plan = {};
|
|
30721
|
+
if (value.seed !== void 0)
|
|
30722
|
+
plan.seed = normalizeSeed(value.seed, `${label}.seed`);
|
|
30723
|
+
if (value.clock !== void 0)
|
|
30724
|
+
plan.clock = normalizeClock(value.clock, `${label}.clock`);
|
|
30725
|
+
if (value.virtualTime !== void 0) {
|
|
30726
|
+
if (typeof value.virtualTime !== "boolean")
|
|
30727
|
+
throw new ControlError(`${label}.virtualTime: expected a boolean`);
|
|
30728
|
+
plan.virtualTime = value.virtualTime;
|
|
30729
|
+
}
|
|
30730
|
+
if (value.latency !== void 0)
|
|
30731
|
+
plan.latency = normalizeLatency(value.latency, `${label}.latency`);
|
|
30732
|
+
if (value.faults !== void 0) {
|
|
30733
|
+
if (!Array.isArray(value.faults) || !value.faults.every(isRecord2))
|
|
30734
|
+
throw new ControlError(`${label}.faults: expected a list of fault rules`);
|
|
30735
|
+
plan.faults = value.faults;
|
|
30736
|
+
}
|
|
30737
|
+
if (value.snapshot !== void 0)
|
|
30738
|
+
plan.snapshot = assertSnapshotDocument(value.snapshot, `${label}.snapshot`);
|
|
30739
|
+
return plan;
|
|
30740
|
+
};
|
|
30741
|
+
var normalizeBootControls = (value, label = "controls") => {
|
|
30742
|
+
if (value === void 0)
|
|
30743
|
+
return { shared: {}, namespaces: {}, credentials: {} };
|
|
30744
|
+
if (!isRecord2(value))
|
|
30745
|
+
throw new ControlError(`${label}: expected an object`);
|
|
30746
|
+
unknownKeys(value, MOCK_KEYS, label);
|
|
30747
|
+
const { credentials, namespaces, ...shared } = value;
|
|
30748
|
+
const plan = {
|
|
30749
|
+
shared: normalizeControls(shared, label),
|
|
30750
|
+
namespaces: {},
|
|
30751
|
+
credentials: {}
|
|
30752
|
+
};
|
|
30753
|
+
if (namespaces !== void 0) {
|
|
30754
|
+
if (!isRecord2(namespaces))
|
|
30755
|
+
throw new ControlError(`${label}.namespaces: expected an object`);
|
|
30756
|
+
for (const [name, each] of Object.entries(namespaces)) {
|
|
30757
|
+
if (!KEY_PATTERN.test(name))
|
|
30758
|
+
throw new ControlError(`${label}.namespaces: bad namespace ${JSON.stringify(name)}`);
|
|
30759
|
+
plan.namespaces[name] = normalizeControls(each, `${label}.namespaces.${name}`);
|
|
30760
|
+
}
|
|
30761
|
+
}
|
|
30762
|
+
if (credentials !== void 0) {
|
|
30763
|
+
if (!isRecord2(credentials))
|
|
30764
|
+
throw new ControlError(`${label}.credentials: expected an object`);
|
|
30765
|
+
for (const [credential, namespace] of Object.entries(credentials)) {
|
|
30766
|
+
if (typeof namespace !== "string" || !KEY_PATTERN.test(namespace))
|
|
30767
|
+
throw new ControlError(`${label}.credentials: bad namespace for a credential`);
|
|
30768
|
+
plan.credentials[credential] = namespace;
|
|
30769
|
+
}
|
|
30770
|
+
}
|
|
30771
|
+
return plan;
|
|
30772
|
+
};
|
|
30773
|
+
var CONTROL_HEADERS = {
|
|
30774
|
+
namespace: "x-mockingbird-namespace",
|
|
30775
|
+
adminKey: "x-mockingbird-admin-key",
|
|
30776
|
+
branch: "x-mockingbird-branch",
|
|
30777
|
+
at: "x-mockingbird-at",
|
|
30778
|
+
reset: "x-mockingbird-reset",
|
|
30779
|
+
restore: "x-mockingbird-restore",
|
|
30780
|
+
seed: "x-mockingbird-seed",
|
|
30781
|
+
time: "x-mockingbird-time",
|
|
30782
|
+
advance: "x-mockingbird-advance",
|
|
30783
|
+
virtualTime: "x-mockingbird-virtual-time",
|
|
30784
|
+
fault: "x-mockingbird-fault",
|
|
30785
|
+
delay: "x-mockingbird-delay",
|
|
30786
|
+
timeout: "x-mockingbird-timeout",
|
|
30787
|
+
snapshot: "x-mockingbird-snapshot"
|
|
30788
|
+
};
|
|
30789
|
+
var CONTROL_HEADER_REFERENCE = [
|
|
30790
|
+
{
|
|
30791
|
+
header: CONTROL_HEADERS.namespace,
|
|
30792
|
+
value: "<key>",
|
|
30793
|
+
scope: "select",
|
|
30794
|
+
guarded: false,
|
|
30795
|
+
summary: "Isolation key. Each key has its own records, clock, random streams, faults, latency, journal and snapshots."
|
|
30796
|
+
},
|
|
30797
|
+
{
|
|
30798
|
+
header: CONTROL_HEADERS.branch,
|
|
30799
|
+
value: "<name>",
|
|
30800
|
+
scope: "select",
|
|
30801
|
+
guarded: false,
|
|
30802
|
+
summary: "Read and write a named copy-on-write branch of the namespace's history."
|
|
30803
|
+
},
|
|
30804
|
+
{
|
|
30805
|
+
header: CONTROL_HEADERS.at,
|
|
30806
|
+
value: "<checkpoint>",
|
|
30807
|
+
scope: "select",
|
|
30808
|
+
guarded: false,
|
|
30809
|
+
summary: "Read the namespace as it was at a checkpoint. With a branch, start the branch there."
|
|
30810
|
+
},
|
|
30811
|
+
{
|
|
30812
|
+
header: CONTROL_HEADERS.reset,
|
|
30813
|
+
value: "true",
|
|
30814
|
+
scope: "namespace",
|
|
30815
|
+
guarded: true,
|
|
30816
|
+
summary: "Reset the namespace to its boot state before the request is handled."
|
|
30817
|
+
},
|
|
30818
|
+
{
|
|
30819
|
+
header: CONTROL_HEADERS.restore,
|
|
30820
|
+
value: "<snapshot>",
|
|
30821
|
+
scope: "namespace",
|
|
30822
|
+
guarded: true,
|
|
30823
|
+
summary: "Restore a snapshot before the request is handled."
|
|
30824
|
+
},
|
|
30825
|
+
{
|
|
30826
|
+
header: CONTROL_HEADERS.seed,
|
|
30827
|
+
value: "<seed>",
|
|
30828
|
+
scope: "namespace",
|
|
30829
|
+
guarded: true,
|
|
30830
|
+
summary: "Restart the namespace's random streams from this seed."
|
|
30831
|
+
},
|
|
30832
|
+
{
|
|
30833
|
+
header: CONTROL_HEADERS.time,
|
|
30834
|
+
value: "<epoch ms | ISO-8601>",
|
|
30835
|
+
scope: "namespace",
|
|
30836
|
+
guarded: true,
|
|
30837
|
+
summary: "Freeze the namespace clock at this instant."
|
|
30838
|
+
},
|
|
30839
|
+
{
|
|
30840
|
+
header: CONTROL_HEADERS.advance,
|
|
30841
|
+
value: "<duration>",
|
|
30842
|
+
scope: "namespace",
|
|
30843
|
+
guarded: true,
|
|
30844
|
+
summary: "Move the namespace clock by this much, forward or (negative) back."
|
|
30845
|
+
},
|
|
30846
|
+
{
|
|
30847
|
+
header: CONTROL_HEADERS.virtualTime,
|
|
30848
|
+
value: "true | false",
|
|
30849
|
+
scope: "namespace",
|
|
30850
|
+
guarded: true,
|
|
30851
|
+
summary: "Spend injected waits on the mock clock (true) or the wall clock (false)."
|
|
30852
|
+
},
|
|
30853
|
+
{
|
|
30854
|
+
header: CONTROL_HEADERS.delay,
|
|
30855
|
+
value: "<duration>",
|
|
30856
|
+
scope: "request",
|
|
30857
|
+
guarded: true,
|
|
30858
|
+
summary: "Hold this response back this long, on top of the namespace's latency."
|
|
30859
|
+
},
|
|
30860
|
+
{
|
|
30861
|
+
header: CONTROL_HEADERS.timeout,
|
|
30862
|
+
value: "<duration>",
|
|
30863
|
+
scope: "request",
|
|
30864
|
+
guarded: true,
|
|
30865
|
+
summary: "Never answer this request: hold it this long, then drop the connection."
|
|
30866
|
+
},
|
|
30867
|
+
{
|
|
30868
|
+
header: CONTROL_HEADERS.fault,
|
|
30869
|
+
value: '<status> | drop | <preset> | key=value; \u2026 | {"status": 503, \u2026}',
|
|
30870
|
+
scope: "request",
|
|
30871
|
+
guarded: true,
|
|
30872
|
+
summary: "Fault this request only. Stored fault rules are not consulted when it answers."
|
|
30873
|
+
},
|
|
30874
|
+
{
|
|
30875
|
+
header: CONTROL_HEADERS.snapshot,
|
|
30876
|
+
value: "<name>",
|
|
30877
|
+
scope: "namespace",
|
|
30878
|
+
guarded: true,
|
|
30879
|
+
summary: "Snapshot the namespace after the request is handled. Echoed on the response."
|
|
30880
|
+
}
|
|
30881
|
+
];
|
|
30882
|
+
var GUARDED = CONTROL_HEADER_REFERENCE.filter((each) => each.guarded).map((each) => each.header);
|
|
30883
|
+
|
|
30575
30884
|
// src/wire/probe.ts
|
|
30576
30885
|
import { createHash as createHash3, createHmac as createHmac2, pbkdf2Sync as pbkdf2Sync2, randomBytes as randomBytes2 } from "node:crypto";
|
|
30577
30886
|
import { createConnection } from "node:net";
|
|
@@ -30676,26 +30985,38 @@ function probe(connectionString) {
|
|
|
30676
30985
|
async function startProtocol(entry) {
|
|
30677
30986
|
const namespaces = /* @__PURE__ */ new Map();
|
|
30678
30987
|
const selectors = { default: entry.database ?? "postgres", ...entry.namespaces };
|
|
30988
|
+
if (entry.seed !== void 0 && (!String(entry.seed).trim() || !Number.isFinite(Number(entry.seed))))
|
|
30989
|
+
throw new Error("Postgres seed must be a finite number");
|
|
30990
|
+
let admission;
|
|
30991
|
+
const boot = normalizeBootControls(entry.adminControls);
|
|
30992
|
+
const create = async (name, databaseName, port = 0) => {
|
|
30993
|
+
if (!/^[A-Za-z0-9_.-]{1,64}$/.test(name) || typeof databaseName !== "string" || !databaseName.length)
|
|
30994
|
+
throw new Error("invalid Postgres namespace/database name");
|
|
30995
|
+
const clock = createClock();
|
|
30996
|
+
const control = { ...boot.shared.clock, ...boot.namespaces[name]?.clock };
|
|
30997
|
+
if (control.frozen) clock.freeze();
|
|
30998
|
+
if (control.now !== void 0) clock.set(control.now);
|
|
30999
|
+
const database = new Database({
|
|
31000
|
+
now: () => new Date(clock.now()),
|
|
31001
|
+
...entry.seed !== void 0 ? { seed: Number(entry.seed) } : {}
|
|
31002
|
+
});
|
|
31003
|
+
const server = await serve({
|
|
31004
|
+
database,
|
|
31005
|
+
databaseName,
|
|
31006
|
+
allowDatabaseSelection: true,
|
|
31007
|
+
host: entry.host ?? "127.0.0.1",
|
|
31008
|
+
port,
|
|
31009
|
+
...entry.password !== void 0 ? { password: entry.password } : {},
|
|
31010
|
+
...entry.user !== void 0 ? { user: entry.user } : {}
|
|
31011
|
+
});
|
|
31012
|
+
server.server.prependListener("connection", (socket) => {
|
|
31013
|
+
if (admission?.(name) === false) socket.destroy();
|
|
31014
|
+
});
|
|
31015
|
+
namespaces.set(name, { server, clock });
|
|
31016
|
+
};
|
|
30679
31017
|
try {
|
|
30680
|
-
for (const [name, databaseName] of Object.entries(selectors))
|
|
30681
|
-
|
|
30682
|
-
throw new Error("invalid Postgres namespace/database name");
|
|
30683
|
-
const clock = createClock();
|
|
30684
|
-
const database = new Database({
|
|
30685
|
-
now: () => new Date(clock.now()),
|
|
30686
|
-
...entry.seed !== void 0 ? { seed: Number(entry.seed) } : {}
|
|
30687
|
-
});
|
|
30688
|
-
const server = await serve({
|
|
30689
|
-
database,
|
|
30690
|
-
databaseName,
|
|
30691
|
-
allowDatabaseSelection: true,
|
|
30692
|
-
host: entry.host ?? "127.0.0.1",
|
|
30693
|
-
port: name === "default" ? entry.port ?? 0 : 0,
|
|
30694
|
-
...entry.password !== void 0 ? { password: entry.password } : {},
|
|
30695
|
-
...entry.user !== void 0 ? { user: entry.user } : {}
|
|
30696
|
-
});
|
|
30697
|
-
namespaces.set(name, { server, clock });
|
|
30698
|
-
}
|
|
31018
|
+
for (const [name, databaseName] of Object.entries(selectors))
|
|
31019
|
+
await create(name, databaseName, name === "default" ? entry.port ?? 0 : 0);
|
|
30699
31020
|
} catch (error) {
|
|
30700
31021
|
await Promise.allSettled([...namespaces.values()].map(({ server }) => server.close()));
|
|
30701
31022
|
throw error;
|
|
@@ -30717,17 +31038,143 @@ async function startProtocol(entry) {
|
|
|
30717
31038
|
protocol: "postgres",
|
|
30718
31039
|
url: publicUrl("default"),
|
|
30719
31040
|
connection: get("default").server.connectionString,
|
|
30720
|
-
connections
|
|
30721
|
-
|
|
30722
|
-
|
|
30723
|
-
|
|
31041
|
+
get connections() {
|
|
31042
|
+
return Object.fromEntries([...namespaces].map(([name, { server }]) => [name, server.connectionString]));
|
|
31043
|
+
},
|
|
31044
|
+
get namespaces() {
|
|
31045
|
+
return {
|
|
31046
|
+
mechanism: "endpoint",
|
|
31047
|
+
endpoints: Object.fromEntries([...namespaces.keys()].map((name) => [name, publicUrl(name)]))
|
|
31048
|
+
};
|
|
31049
|
+
},
|
|
31050
|
+
async createNamespace(name) {
|
|
31051
|
+
if (namespaces.has(name)) throw new Error("namespace already exists");
|
|
31052
|
+
await create(name, entry.database ?? "postgres");
|
|
31053
|
+
origins.set(name, get(name).server.snapshotAll());
|
|
31054
|
+
},
|
|
31055
|
+
async deleteNamespace(name) {
|
|
31056
|
+
const { server } = get(name);
|
|
31057
|
+
if (server.connections) throw new Error("Postgres namespace has open sessions");
|
|
31058
|
+
await server.close();
|
|
31059
|
+
for (const database of server.databaseNames()) server.getDatabase(database).close();
|
|
31060
|
+
namespaces.delete(name);
|
|
31061
|
+
origins.delete(name);
|
|
31062
|
+
},
|
|
31063
|
+
async exportCheckpoint(namespace, handle) {
|
|
31064
|
+
const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
|
|
31065
|
+
if (!point || point.namespace !== namespace) throw new Error("invalid Postgres checkpoint");
|
|
31066
|
+
return {
|
|
31067
|
+
format: "mockingbird.postgres/v1",
|
|
31068
|
+
inventory: Object.fromEntries(
|
|
31069
|
+
[...point.databases].map(([name, snapshot]) => {
|
|
31070
|
+
const database = snapshot.open();
|
|
31071
|
+
try {
|
|
31072
|
+
const tables = database.prepare("SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'public'").all();
|
|
31073
|
+
return [
|
|
31074
|
+
name,
|
|
31075
|
+
Object.fromEntries(
|
|
31076
|
+
tables.map((table) => {
|
|
31077
|
+
const identifier2 = table.table_name.replaceAll('"', '""');
|
|
31078
|
+
const rows = database.prepare(`SELECT * FROM public."${identifier2}"`).all();
|
|
31079
|
+
return [
|
|
31080
|
+
table.table_name,
|
|
31081
|
+
{
|
|
31082
|
+
count: rows.length,
|
|
31083
|
+
records: Object.fromEntries(
|
|
31084
|
+
rows.map((row, index) => [
|
|
31085
|
+
String(row.id ?? index),
|
|
31086
|
+
createHash4("sha256").update(
|
|
31087
|
+
JSON.stringify(
|
|
31088
|
+
row,
|
|
31089
|
+
(_key, value) => typeof value === "bigint" ? String(value) : value
|
|
31090
|
+
)
|
|
31091
|
+
).digest("hex")
|
|
31092
|
+
])
|
|
31093
|
+
)
|
|
31094
|
+
}
|
|
31095
|
+
];
|
|
31096
|
+
})
|
|
31097
|
+
)
|
|
31098
|
+
];
|
|
31099
|
+
} finally {
|
|
31100
|
+
database.close();
|
|
31101
|
+
}
|
|
31102
|
+
})
|
|
31103
|
+
),
|
|
31104
|
+
databases: [...point.databases].map(([name, snapshot]) => ({
|
|
31105
|
+
name,
|
|
31106
|
+
bytes: Buffer.from(snapshot.encode()).toString("base64")
|
|
31107
|
+
})),
|
|
31108
|
+
clock: point.clock,
|
|
31109
|
+
faults: point.faults
|
|
31110
|
+
};
|
|
31111
|
+
},
|
|
31112
|
+
async importCheckpoint(namespace, document) {
|
|
31113
|
+
const doc = document;
|
|
31114
|
+
if (doc?.format !== "mockingbird.postgres/v1" || !Array.isArray(doc.databases) || !doc.databases.length || !Number.isFinite(doc.clock?.now) || typeof doc.clock?.frozen !== "boolean")
|
|
31115
|
+
throw new Error("invalid Postgres archive");
|
|
31116
|
+
const databases = /* @__PURE__ */ new Map();
|
|
31117
|
+
for (const row of doc.databases) {
|
|
31118
|
+
if (typeof row.name !== "string" || databases.has(row.name) || typeof row.bytes !== "string" || !/^[A-Za-z0-9+/]*={0,2}$/.test(row.bytes))
|
|
31119
|
+
throw new Error("invalid Postgres archive database");
|
|
31120
|
+
databases.set(row.name, Snapshot.decode(Buffer.from(row.bytes, "base64")));
|
|
31121
|
+
}
|
|
31122
|
+
const handle = {};
|
|
31123
|
+
snapshots.set(handle, { namespace, databases, clock: doc.clock, faults: doc.faults ?? {} });
|
|
31124
|
+
return handle;
|
|
31125
|
+
},
|
|
31126
|
+
async inspect(namespace, { offset, limit, collection }) {
|
|
31127
|
+
const { server } = get(namespace);
|
|
31128
|
+
const database = server.getDatabase(server.databaseNames()[0]);
|
|
31129
|
+
const tables = database.prepare("SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema = 'public'").all();
|
|
31130
|
+
if (!collection) return { tables };
|
|
31131
|
+
const table = collection.replaceAll('"', '""');
|
|
31132
|
+
return {
|
|
31133
|
+
records: database.prepare(`SELECT * FROM public."${table}" LIMIT ${limit} OFFSET ${offset}`).all(),
|
|
31134
|
+
offset
|
|
31135
|
+
};
|
|
31136
|
+
},
|
|
31137
|
+
async initializationAccess() {
|
|
31138
|
+
const privateServers = [];
|
|
31139
|
+
const connections = {};
|
|
31140
|
+
try {
|
|
31141
|
+
for (const [name, { server }] of namespaces) {
|
|
31142
|
+
const databaseName = server.databaseNames()[0];
|
|
31143
|
+
const seedServer = await serve({
|
|
31144
|
+
database: server.getDatabase(databaseName),
|
|
31145
|
+
databaseName,
|
|
31146
|
+
port: 0,
|
|
31147
|
+
host: "127.0.0.1",
|
|
31148
|
+
...entry.user ? { user: entry.user } : {},
|
|
31149
|
+
...entry.password ? { password: entry.password } : {}
|
|
31150
|
+
});
|
|
31151
|
+
privateServers.push(seedServer);
|
|
31152
|
+
connections[name] = seedServer.connectionString;
|
|
31153
|
+
}
|
|
31154
|
+
} catch (error) {
|
|
31155
|
+
await Promise.allSettled(privateServers.map((s) => s.close()));
|
|
31156
|
+
throw error;
|
|
31157
|
+
}
|
|
31158
|
+
return {
|
|
31159
|
+
connections,
|
|
31160
|
+
close: async () => {
|
|
31161
|
+
await Promise.all(privateServers.map((s) => s.close()));
|
|
31162
|
+
}
|
|
31163
|
+
};
|
|
31164
|
+
},
|
|
31165
|
+
setAdmission(allow) {
|
|
31166
|
+
admission = allow;
|
|
30724
31167
|
},
|
|
30725
31168
|
async ready() {
|
|
31169
|
+
if (admission) return [...namespaces.values()].every(({ server }) => server.server.listening);
|
|
30726
31170
|
const results = await Promise.all(
|
|
30727
31171
|
[...namespaces.values()].map(
|
|
30728
31172
|
({ server }) => server.server.listening ? probe(server.connectionString) : false
|
|
30729
31173
|
)
|
|
30730
31174
|
);
|
|
31175
|
+
const deadline = Date.now() + 2e3;
|
|
31176
|
+
while ([...namespaces.values()].some(({ server }) => server.connections > 0) && Date.now() < deadline)
|
|
31177
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
30731
31178
|
return results.every(Boolean);
|
|
30732
31179
|
},
|
|
30733
31180
|
async close() {
|
|
@@ -30789,7 +31236,11 @@ async function startProtocol(entry) {
|
|
|
30789
31236
|
for (const name of server.databaseNames()) server.getDatabase(name).now = () => new Date(clock.now());
|
|
30790
31237
|
server.clearFaults();
|
|
30791
31238
|
clock.reset();
|
|
31239
|
+
const control = { ...boot.shared.clock, ...boot.namespaces[namespace]?.clock };
|
|
31240
|
+
if (control.frozen) clock.freeze();
|
|
31241
|
+
if (control.now !== void 0) clock.set(control.now);
|
|
30792
31242
|
},
|
|
31243
|
+
nextEvent: () => void 0,
|
|
30793
31244
|
clock: (namespace) => get(namespace).clock
|
|
30794
31245
|
};
|
|
30795
31246
|
}
|