@cloudbitmaps/core 0.9.0 → 0.10.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/NOTICE +4 -4
- package/README.md +28 -7
- package/dist/chunk-BUPDGZRR.js +461 -0
- package/dist/chunk-BUPDGZRR.js.map +7 -0
- package/dist/core/audit.d.ts +127 -0
- package/dist/core/bit-route.d.ts +21 -0
- package/dist/core/blob.d.ts +34 -0
- package/dist/core/budget.d.ts +66 -0
- package/dist/core/codec.d.ts +114 -0
- package/dist/core/concurrency.d.ts +15 -0
- package/dist/core/consistency.d.ts +73 -0
- package/dist/core/cooperative.d.ts +78 -0
- package/dist/core/cost.d.ts +102 -0
- package/dist/core/crbm/crc32c.d.ts +10 -0
- package/dist/core/crbm/format.d.ts +116 -0
- package/dist/core/crbm/reader.d.ts +69 -0
- package/dist/core/crbm/varint.d.ts +13 -0
- package/dist/core/crbm/writer.d.ts +39 -0
- package/dist/core/crbm-storage-source.d.ts +432 -0
- package/dist/core/crypto.d.ts +80 -0
- package/dist/core/determinism.d.ts +42 -0
- package/dist/core/discover.d.ts +75 -0
- package/dist/core/due-index.d.ts +55 -0
- package/dist/core/engine.d.ts +204 -0
- package/dist/core/erase-id.d.ts +141 -0
- package/dist/core/erasure.d.ts +179 -0
- package/dist/core/errors.d.ts +120 -0
- package/dist/core/generation-gc.d.ts +52 -0
- package/dist/core/keys.d.ts +23 -0
- package/dist/core/load.d.ts +102 -0
- package/dist/core/lru.d.ts +69 -0
- package/dist/core/metrics.d.ts +132 -0
- package/dist/core/name-codec.d.ts +79 -0
- package/dist/core/pinned-storage-source.d.ts +69 -0
- package/dist/core/ports.d.ts +340 -0
- package/dist/core/registry-scan.d.ts +38 -0
- package/dist/core/retention-sweep.d.ts +190 -0
- package/dist/core/retention.d.ts +67 -0
- package/dist/core/retry.d.ts +65 -0
- package/dist/core/rollback.d.ts +71 -0
- package/dist/core/validate.d.ts +20 -0
- package/dist/driver-kit.d.ts +10 -0
- package/dist/driver-kit.js +314 -0
- package/dist/driver-kit.js.map +7 -0
- package/dist/drivers/_shared/aws-errors.d.ts +18 -0
- package/dist/drivers/_shared/keys.d.ts +27 -0
- package/dist/drivers/_shared/object-registry-keys.d.ts +28 -0
- package/dist/drivers/_shared/object-registry.d.ts +84 -0
- package/dist/drivers/_shared/registry.d.ts +62 -0
- package/dist/drivers/backends.d.ts +49 -0
- package/dist/drivers/crypto.d.ts +38 -0
- package/dist/drivers/localfs/fs-util.d.ts +20 -0
- package/dist/drivers/localfs/paths.d.ts +33 -0
- package/dist/drivers/localfs/registry.d.ts +28 -0
- package/dist/drivers/localfs/storage.d.ts +19 -0
- package/dist/drivers/memory.d.ts +57 -0
- package/dist/drivers/retry/retrying-drivers.d.ts +90 -0
- package/dist/export/index.d.ts +92 -0
- package/dist/index.d.ts +85 -2760
- package/dist/index.js +2815 -3062
- package/dist/index.js.map +7 -1
- package/dist/testing/fuzz-core.d.ts +14 -0
- package/package.json +19 -115
- package/dist/azure/index.cjs +0 -368
- package/dist/azure/index.cjs.map +0 -1
- package/dist/azure/index.d.cts +0 -48
- package/dist/azure/index.d.ts +0 -48
- package/dist/azure/index.js +0 -304
- package/dist/azure/index.js.map +0 -1
- package/dist/cassandra/index.cjs +0 -265
- package/dist/cassandra/index.cjs.map +0 -1
- package/dist/cassandra/index.d.cts +0 -52
- package/dist/cassandra/index.d.ts +0 -52
- package/dist/cassandra/index.js +0 -204
- package/dist/cassandra/index.js.map +0 -1
- package/dist/chunk-7LMLYSVJ.js +0 -43
- package/dist/chunk-7LMLYSVJ.js.map +0 -1
- package/dist/chunk-BD22WDAN.js +0 -230
- package/dist/chunk-BD22WDAN.js.map +0 -1
- package/dist/chunk-NUIDEEFZ.js +0 -91
- package/dist/chunk-NUIDEEFZ.js.map +0 -1
- package/dist/chunk-OZ6LAFV4.js +0 -6
- package/dist/chunk-OZ6LAFV4.js.map +0 -1
- package/dist/chunk-SNJVZ227.js +0 -35
- package/dist/chunk-SNJVZ227.js.map +0 -1
- package/dist/dynamodb/index.cjs +0 -767
- package/dist/dynamodb/index.cjs.map +0 -1
- package/dist/dynamodb/index.d.cts +0 -106
- package/dist/dynamodb/index.d.ts +0 -106
- package/dist/dynamodb/index.js +0 -469
- package/dist/dynamodb/index.js.map +0 -1
- package/dist/gcs/index.cjs +0 -343
- package/dist/gcs/index.cjs.map +0 -1
- package/dist/gcs/index.d.cts +0 -46
- package/dist/gcs/index.d.ts +0 -46
- package/dist/gcs/index.js +0 -279
- package/dist/gcs/index.js.map +0 -1
- package/dist/index.cjs +0 -4990
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2761
- package/dist/mongodb/index.cjs +0 -260
- package/dist/mongodb/index.cjs.map +0 -1
- package/dist/mongodb/index.d.cts +0 -45
- package/dist/mongodb/index.d.ts +0 -45
- package/dist/mongodb/index.js +0 -199
- package/dist/mongodb/index.js.map +0 -1
- package/dist/mysql/index.cjs +0 -281
- package/dist/mysql/index.cjs.map +0 -1
- package/dist/mysql/index.d.cts +0 -56
- package/dist/mysql/index.d.ts +0 -56
- package/dist/mysql/index.js +0 -211
- package/dist/mysql/index.js.map +0 -1
- package/dist/ports-CvcvJWf6.d.cts +0 -395
- package/dist/ports-CvcvJWf6.d.ts +0 -395
- package/dist/postgres/index.cjs +0 -273
- package/dist/postgres/index.cjs.map +0 -1
- package/dist/postgres/index.d.cts +0 -54
- package/dist/postgres/index.d.ts +0 -54
- package/dist/postgres/index.js +0 -203
- package/dist/postgres/index.js.map +0 -1
- package/dist/redis/index.cjs +0 -257
- package/dist/redis/index.cjs.map +0 -1
- package/dist/redis/index.d.cts +0 -42
- package/dist/redis/index.d.ts +0 -42
- package/dist/redis/index.js +0 -197
- package/dist/redis/index.js.map +0 -1
- package/dist/s3/index.cjs +0 -918
- package/dist/s3/index.cjs.map +0 -1
- package/dist/s3/index.d.cts +0 -112
- package/dist/s3/index.d.ts +0 -112
- package/dist/s3/index.js +0 -555
- package/dist/s3/index.js.map +0 -1
package/dist/mongodb/index.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { NO_ROW } from '../chunk-OZ6LAFV4.js';
|
|
2
|
-
import { ValidationError, WriteConflictError, IntegrityError, TransientError, validateChunkRef, namespacePart, validateSegmentRef } from '../chunk-NUIDEEFZ.js';
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
4
|
-
|
|
5
|
-
// src/drivers/mongodb/keys.ts
|
|
6
|
-
function normalizeKeyPrefix(prefix) {
|
|
7
|
-
if (prefix === void 0 || prefix === "") return "";
|
|
8
|
-
for (const ch of prefix) {
|
|
9
|
-
if (ch.charCodeAt(0) < 32 || ch === "|") {
|
|
10
|
-
throw new ValidationError('keyPrefix must not contain "|" or control characters');
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return prefix;
|
|
14
|
-
}
|
|
15
|
-
function segmentScope(prefix, ref) {
|
|
16
|
-
validateSegmentRef(ref);
|
|
17
|
-
return { kp: prefix, ns: namespacePart(ref.namespace), seg: ref.segment };
|
|
18
|
-
}
|
|
19
|
-
function chunkDocId(prefix, ref) {
|
|
20
|
-
validateChunkRef(ref);
|
|
21
|
-
return `${prefix}|${namespacePart(ref.namespace)}|${ref.segment}|${ref.chunkKey}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// src/drivers/mongodb/mongodb-errors.ts
|
|
25
|
-
function serverCode(err) {
|
|
26
|
-
const code = err?.code;
|
|
27
|
-
return typeof code === "number" ? code : void 0;
|
|
28
|
-
}
|
|
29
|
-
function networkCode(err) {
|
|
30
|
-
const code = err?.code;
|
|
31
|
-
return typeof code === "string" && /^E[A-Z_]+$/.test(code) ? code : void 0;
|
|
32
|
-
}
|
|
33
|
-
function isDuplicateKey(err) {
|
|
34
|
-
const code = serverCode(err);
|
|
35
|
-
return code === 11e3 || code === 11001;
|
|
36
|
-
}
|
|
37
|
-
var RETRYABLE_CODES = /* @__PURE__ */ new Set([
|
|
38
|
-
6,
|
|
39
|
-
// HostUnreachable
|
|
40
|
-
7,
|
|
41
|
-
// HostNotFound
|
|
42
|
-
89,
|
|
43
|
-
// NetworkTimeout
|
|
44
|
-
91,
|
|
45
|
-
// ShutdownInProgress
|
|
46
|
-
189,
|
|
47
|
-
// PrimarySteppedDown
|
|
48
|
-
262,
|
|
49
|
-
// ExceededTimeLimit
|
|
50
|
-
9001,
|
|
51
|
-
// SocketException
|
|
52
|
-
10107,
|
|
53
|
-
// NotWritablePrimary
|
|
54
|
-
11600,
|
|
55
|
-
// InterruptedAtShutdown
|
|
56
|
-
11602,
|
|
57
|
-
// InterruptedDueToReplStateChange
|
|
58
|
-
13435,
|
|
59
|
-
// NotPrimaryNoSecondaryOk
|
|
60
|
-
13436,
|
|
61
|
-
// NotPrimaryOrSecondary
|
|
62
|
-
63,
|
|
63
|
-
// StaleShardVersion
|
|
64
|
-
150
|
|
65
|
-
// StaleEpoch
|
|
66
|
-
]);
|
|
67
|
-
function isTransient(err) {
|
|
68
|
-
if (isDuplicateKey(err)) return false;
|
|
69
|
-
const name = err?.name;
|
|
70
|
-
if (name === "MongoNetworkError" || name === "MongoServerSelectionError") return true;
|
|
71
|
-
const hasLabel = err?.hasErrorLabel;
|
|
72
|
-
if (typeof hasLabel === "function") {
|
|
73
|
-
if (hasLabel.call(err, "RetryableWriteError") || hasLabel.call(err, "TransientTransactionError")) {
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const code = serverCode(err);
|
|
78
|
-
if (code !== void 0 && RETRYABLE_CODES.has(code)) return true;
|
|
79
|
-
const net = networkCode(err);
|
|
80
|
-
return net === "ECONNRESET" || net === "ECONNABORTED" || net === "ETIMEDOUT" || net === "ESOCKETTIMEDOUT" || net === "ECONNREFUSED" || net === "EHOSTUNREACH" || net === "ENETUNREACH" || net === "EPIPE" || net === "EAI_AGAIN" || net === "ENOTFOUND";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// src/drivers/mongodb/warm.ts
|
|
84
|
-
var DEFAULT_COLLECTION = "cloud_roaring_warm";
|
|
85
|
-
var INDEX_NAME = "cr_warm_scope_ck";
|
|
86
|
-
var DEFAULT_BATCH = 500;
|
|
87
|
-
var SIMPLE_COLLATION = { locale: "simple" };
|
|
88
|
-
async function ensureMongoWarmIndexes(db, collection = DEFAULT_COLLECTION) {
|
|
89
|
-
await db.collection(collection).createIndex(
|
|
90
|
-
{ kp: 1, ns: 1, seg: 1, ck: 1 },
|
|
91
|
-
{ name: INDEX_NAME, collation: SIMPLE_COLLATION }
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
var MongoWarmDriver = class {
|
|
95
|
-
col;
|
|
96
|
-
keyPrefix;
|
|
97
|
-
batch;
|
|
98
|
-
constructor(options) {
|
|
99
|
-
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);
|
|
100
|
-
const batch = options.listPageSize ?? DEFAULT_BATCH;
|
|
101
|
-
if (!Number.isSafeInteger(batch) || batch < 1) {
|
|
102
|
-
throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);
|
|
103
|
-
}
|
|
104
|
-
this.batch = batch;
|
|
105
|
-
this.col = options.db.collection(options.collection ?? DEFAULT_COLLECTION);
|
|
106
|
-
}
|
|
107
|
-
// Reads are strongly consistent when the Db targets the primary (see the `db` option), so the optional
|
|
108
|
-
// `WarmReadOptions` hint would be a no-op — the structurally-optional param is simply omitted, as in the
|
|
109
|
-
// LocalFs / Postgres / Redis drivers.
|
|
110
|
-
async get(ref) {
|
|
111
|
-
const _id = chunkDocId(this.keyPrefix, ref);
|
|
112
|
-
let doc;
|
|
113
|
-
try {
|
|
114
|
-
doc = await this.col.findOne({ _id }, { collation: SIMPLE_COLLATION });
|
|
115
|
-
} catch (err) {
|
|
116
|
-
throw this.mapError(err);
|
|
117
|
-
}
|
|
118
|
-
return doc === null ? null : this.rowFrom(doc, ref.chunkKey);
|
|
119
|
-
}
|
|
120
|
-
async putConditional(ref, bytes, expected) {
|
|
121
|
-
const _id = chunkDocId(this.keyPrefix, ref);
|
|
122
|
-
const token = randomUUID();
|
|
123
|
-
const payload = Buffer.from(bytes);
|
|
124
|
-
try {
|
|
125
|
-
if (expected === NO_ROW) {
|
|
126
|
-
const { kp, ns, seg } = segmentScope(this.keyPrefix, ref);
|
|
127
|
-
await this.col.insertOne({ _id, kp, ns, seg, ck: ref.chunkKey, token, payload });
|
|
128
|
-
} else {
|
|
129
|
-
const res = await this.col.updateOne(
|
|
130
|
-
{ _id, token: expected },
|
|
131
|
-
{ $set: { token, payload } },
|
|
132
|
-
{ collation: SIMPLE_COLLATION }
|
|
133
|
-
);
|
|
134
|
-
if (res.matchedCount !== 1) {
|
|
135
|
-
throw new WriteConflictError(`OCC conflict on chunk ${ref.chunkKey}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return { token };
|
|
139
|
-
} catch (err) {
|
|
140
|
-
if (isDuplicateKey(err)) {
|
|
141
|
-
throw new WriteConflictError(`chunk ${ref.chunkKey} already exists (create-if-absent)`);
|
|
142
|
-
}
|
|
143
|
-
throw this.mapError(err);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async deleteConditional(ref, expected) {
|
|
147
|
-
const _id = chunkDocId(this.keyPrefix, ref);
|
|
148
|
-
let deletedCount;
|
|
149
|
-
try {
|
|
150
|
-
deletedCount = (await this.col.deleteOne({ _id, token: expected }, { collation: SIMPLE_COLLATION })).deletedCount;
|
|
151
|
-
} catch (err) {
|
|
152
|
-
throw this.mapError(err);
|
|
153
|
-
}
|
|
154
|
-
if (deletedCount !== 1) {
|
|
155
|
-
throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async *listChunks(ref) {
|
|
159
|
-
const scope = segmentScope(this.keyPrefix, ref);
|
|
160
|
-
const cursor = this.col.find(scope, { collation: SIMPLE_COLLATION }).sort({ ck: 1 }).batchSize(this.batch);
|
|
161
|
-
try {
|
|
162
|
-
for await (const doc of cursor) {
|
|
163
|
-
if (typeof doc.ck !== "number") {
|
|
164
|
-
throw new IntegrityError("warm doc is missing its numeric chunk key (ck)");
|
|
165
|
-
}
|
|
166
|
-
yield { chunkKey: doc.ck, ...this.rowFrom(doc, doc.ck) };
|
|
167
|
-
}
|
|
168
|
-
} catch (err) {
|
|
169
|
-
throw this.mapError(err);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
/** Build a {@link WarmRow} from a doc; reject one missing/!typed fields (corrupt/foreign). */
|
|
173
|
-
rowFrom(doc, chunkKey) {
|
|
174
|
-
if (typeof doc.token !== "string" || doc.token === "") {
|
|
175
|
-
throw new IntegrityError(`warm doc for chunk ${chunkKey} is missing its token`);
|
|
176
|
-
}
|
|
177
|
-
return { token: doc.token, bytes: decodePayload(doc.payload, chunkKey) };
|
|
178
|
-
}
|
|
179
|
-
/** Reclassify a transient Mongo fault as a retryable {@link TransientError}; else propagate unchanged. */
|
|
180
|
-
mapError(err) {
|
|
181
|
-
if (isTransient(err)) {
|
|
182
|
-
return new TransientError(
|
|
183
|
-
`transient MongoDB fault: ${err?.message ?? "unknown"}`,
|
|
184
|
-
{ cause: err }
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
return err;
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
function decodePayload(p, chunkKey) {
|
|
191
|
-
if (p instanceof Uint8Array) return new Uint8Array(p);
|
|
192
|
-
const inner = p?.buffer;
|
|
193
|
-
if (inner instanceof Uint8Array) return new Uint8Array(inner);
|
|
194
|
-
throw new IntegrityError(`warm doc for chunk ${chunkKey} has no readable payload`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export { MongoWarmDriver, ensureMongoWarmIndexes };
|
|
198
|
-
//# sourceMappingURL=index.js.map
|
|
199
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/drivers/mongodb/keys.ts","../../src/drivers/mongodb/mongodb-errors.ts","../../src/drivers/mongodb/warm.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Logical-ref → MongoDB document-id mapping for {@link MongoWarmDriver} (Phase 7).\n *\n * Pure, SDK-free string logic — unit-testable without a live MongoDB. Each chunk is one document with a\n * **deterministic composite `_id`** `<prefix>|<ns>|<seg>|<chunkKey>`, so create-if-absent is a plain\n * `insertOne` (a duplicate `_id` → the unique-index conflict, no extra index needed) and `get` is a direct\n * `_id` lookup. The ref fields are ALSO stored as separate typed fields (`kp`/`ns`/`seg`/`ck`) so `listChunks`\n * can query + **numerically** sort by `ck` (an `_id`-string sort would order `10` before `9`). The name grammar\n * (validateSegmentRef) forbids `|` in ns/seg and {@link normalizeKeyPrefix} forbids it in the prefix, so the\n * `|`-joined id is unambiguous.\n */\nimport { ValidationError } from '@/core/errors';\nimport { validateChunkRef, validateSegmentRef } from '@/core/validate';\nimport type { ChunkRef, SegmentRef } from '@/core/ports';\nimport { namespacePart } from '../_shared/keys';\n\n/** The scope fields shared by every doc of a segment — the `listChunks` filter. */\nexport interface SegmentScope {\n kp: string;\n ns: string;\n seg: string;\n}\n\n/**\n * Validate a caller-supplied key prefix: no control characters and no `|` (the composite-`_id` delimiter), so\n * it can neither corrupt an id nor alias another prefix. Empty/undefined ⇒ no prefix (stored as `''`).\n */\nexport function normalizeKeyPrefix(prefix: string | undefined): string {\n if (prefix === undefined || prefix === '') return '';\n for (const ch of prefix) {\n if (ch.charCodeAt(0) < 0x20 || ch === '|') {\n throw new ValidationError('keyPrefix must not contain \"|\" or control characters');\n }\n }\n return prefix;\n}\n\n/** The scope filter for all of a segment's chunk docs. Validates the ref. */\nexport function segmentScope(prefix: string, ref: SegmentRef): SegmentScope {\n validateSegmentRef(ref);\n return { kp: prefix, ns: namespacePart(ref.namespace), seg: ref.segment };\n}\n\n/** The deterministic `_id` of one chunk's doc: `<prefix>|<ns>|<seg>|<chunkKey>`. Validates the full ref. */\nexport function chunkDocId(prefix: string, ref: ChunkRef): string {\n validateChunkRef(ref);\n return `${prefix}|${namespacePart(ref.namespace)}|${ref.segment}|${ref.chunkKey}`;\n}\n","/**\n * Pure helpers for classifying MongoDB (`mongodb`) errors (Phase 7; transient class mirrors the other drivers).\n *\n * SDK-free + side-effect-free — they read only structural shapes off the thrown value (`err.code`, `err.name`,\n * the driver's `hasErrorLabel`, or a Node socket code). The driver signals a create-if-absent conflict itself\n * (a duplicate-key error, {@link isDuplicateKey}) and a token-fenced conflict via a 0 matched/deleted count,\n * so there's no general OCC classifier here — only duplicate-key + transient-vs-not.\n */\n\n/** A MongoDB numeric server error code, if present (`err.code`). */\nfunction serverCode(err: unknown): number | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'number' ? code : undefined;\n}\n\n/** A Node system/socket error `code` string (starts with `E`, uppercase letters + underscore, no digits). */\nfunction networkCode(err: unknown): string | undefined {\n const code = (err as { code?: unknown } | null)?.code;\n return typeof code === 'string' && /^E[A-Z_]+$/.test(code) ? code : undefined;\n}\n\n/** A create-if-absent lost the race — the `_id` already existed (duplicate key: 11000 / 11001). */\nexport function isDuplicateKey(err: unknown): boolean {\n const code = serverCode(err);\n return code === 11000 || code === 11001;\n}\n\n/**\n * Retryable MongoDB server error codes: failover / primary-stepdown / node-unreachable / shutdown / network\n * timeouts. (Names from the server error catalogue.) These are safe to retry; deterministic errors\n * (BadValue, duplicate key, etc.) are not listed, so they surface.\n */\nconst RETRYABLE_CODES = new Set<number>([\n 6, // HostUnreachable\n 7, // HostNotFound\n 89, // NetworkTimeout\n 91, // ShutdownInProgress\n 189, // PrimarySteppedDown\n 262, // ExceededTimeLimit\n 9001, // SocketException\n 10107, // NotWritablePrimary\n 11600, // InterruptedAtShutdown\n 11602, // InterruptedDueToReplStateChange\n 13435, // NotPrimaryNoSecondaryOk\n 13436, // NotPrimaryOrSecondary\n 63, // StaleShardVersion\n 150, // StaleEpoch\n]);\n\n/**\n * A transient MongoDB fault safe to retry: a network error, a server-selection timeout, a driver retryable\n * label (`RetryableWriteError` / `TransientTransactionError`), a retryable server code, or a dropped socket.\n * A duplicate-key error is NOT transient (it's the deterministic create-if-absent conflict).\n */\nexport function isTransient(err: unknown): boolean {\n if (isDuplicateKey(err)) return false;\n const name = (err as { name?: unknown } | null)?.name;\n if (name === 'MongoNetworkError' || name === 'MongoServerSelectionError') return true;\n const hasLabel = (err as { hasErrorLabel?: (l: string) => boolean } | null)?.hasErrorLabel;\n if (typeof hasLabel === 'function') {\n if (\n hasLabel.call(err, 'RetryableWriteError') ||\n hasLabel.call(err, 'TransientTransactionError')\n ) {\n return true;\n }\n }\n const code = serverCode(err);\n if (code !== undefined && RETRYABLE_CODES.has(code)) return true;\n const net = networkCode(err);\n return (\n net === 'ECONNRESET' ||\n net === 'ECONNABORTED' ||\n net === 'ETIMEDOUT' ||\n net === 'ESOCKETTIMEDOUT' ||\n net === 'ECONNREFUSED' ||\n net === 'EHOSTUNREACH' ||\n net === 'ENETUNREACH' ||\n net === 'EPIPE' ||\n net === 'EAI_AGAIN' ||\n net === 'ENOTFOUND'\n );\n}\n","/**\n * `MongoWarmDriver` — an {@link IWarmDriver} over MongoDB / DocumentDB (Phase 7).\n *\n * Uses the official `mongodb` driver, an **optional peer dependency** — only consumers of\n * `cloud-roaring/mongodb` install it. A `Db` is **injected** (dependency injection): the driver owns no\n * connection/credential logic, so it's thin and reuses the caller's client.\n *\n * Each chunk is one document keyed by a deterministic composite `_id` (`<prefix>|<ns>|<seg>|<chunkKey>`) with\n * an opaque OCC **token** (a random UUID per write) and the delta `payload` (BSON binary). Optimistic\n * concurrency is per-document and server-side:\n * - **create-if-absent** = `insertOne` with that `_id`; a duplicate-key error ⇒ {@link WriteConflictError}.\n * - **token-fenced update / delete** = `updateOne` / `deleteOne` filtered on `{ _id, token: expected }`; a 0\n * matched/deleted count ⇒ {@link WriteConflictError}.\n *\n * Each op is a single-document atomic operation, so no transaction is needed. Tokens are not reused across\n * delete→recreate (ABA-safe, D3 — probabilistic random UUID, like the Postgres/Redis drivers). Reads are\n * strongly consistent when the `Db` targets the primary (majority/primary read preference), which the OCC\n * read-modify-write requires — see the `db` option note. Drivers may use `node:crypto`.\n */\nimport { randomUUID } from 'node:crypto';\nimport type { Collection, Db } from 'mongodb';\nimport { IntegrityError, TransientError, ValidationError, WriteConflictError } from '@/core/errors';\nimport { NO_ROW } from '@/core/ports';\nimport type { ChunkRef, IWarmDriver, NoRow, SegmentRef, Token, WarmRow } from '@/core/ports';\nimport { chunkDocId, normalizeKeyPrefix, segmentScope } from './keys';\nimport { isDuplicateKey, isTransient } from './mongodb-errors';\n\nconst DEFAULT_COLLECTION = 'cloud_roaring_warm';\nconst INDEX_NAME = 'cr_warm_scope_ck';\n/** find() cursor batch — bounds resident docs on a very wide segment. */\nconst DEFAULT_BATCH = 500;\n/**\n * Force **binary (byte-exact) comparison** on every op + the index, overriding any case-/diacritic-insensitive\n * *default* collation the collection may have been created with. Grammar-legal segment/namespace names differ\n * only by case (`\"A\"` vs `\"a\"`) are distinct stores; under a case-insensitive default collation an unpinned\n * `findOne({_id})` / `updateOne` / `deleteOne` would match the *wrong* document → cross-segment read/leak and\n * wrong-doc OCC updates. This is the Mongo analogue of the MySQL `utf8mb4_bin` requirement. Operation-level\n * collation is binary in the common (no-default-collation) case, so this is a no-op there and a fix otherwise.\n * NOTE: `insertOne` uniqueness is governed by the `_id` index's collation, which cannot be overridden per-op —\n * so a collection created with a case-insensitive **default** collation can still collapse `\"a\"`/`\"A\"` on\n * *insert* (a spurious create-if-absent conflict, not a data leak). The operator guide states the warm\n * collection must use the simple default collation; this pin closes the get/update/delete/list matching hole.\n */\nconst SIMPLE_COLLATION = { locale: 'simple' } as const;\n\n/** One warm chunk document. */\ninterface WarmDoc {\n _id: string;\n kp: string;\n ns: string;\n seg: string;\n ck: number;\n token: string;\n payload: unknown; // written as a Node Buffer; read back as a BSON Binary (or Buffer if promoteBuffers)\n}\n\nexport interface MongoWarmDriverOptions {\n /**\n * A `mongodb` `Db` (the driver calls `db.collection(...)`). The driver never connects/closes the client —\n * the caller owns its lifecycle. Reads must be strongly consistent for OCC (read-your-writes): use a\n * primary read preference (the default), not `secondary`/`secondaryPreferred`.\n */\n readonly db: Db;\n /** Warm collection name (default `cloud_roaring_warm`). */\n readonly collection?: string;\n /** Optional key-prefix component of the composite `_id` so several logical stores can share one collection. */\n readonly keyPrefix?: string;\n /** Advanced: `listChunks` cursor batch size (default 500). Tunes peak memory on wide segments. */\n readonly listPageSize?: number;\n}\n\n/**\n * Create the index `listChunks` needs — `{ kp, ns, seg, ck }` (scope filter + numeric ascending sort) —\n * idempotently. **Run this once at deploy time — it is effectively required, not merely an optimization:**\n * without it a `listChunks` over a segment whose dirty backlog exceeds MongoDB's 32 MB in-memory-sort ceiling\n * fails (server error 292, `QueryExceededMemoryLimitNoDiskUseAllowedException`) rather than merely running\n * slow. (create-if-absent needs no extra index — the composite `_id` is inherently unique.)\n */\nexport async function ensureMongoWarmIndexes(\n db: Db,\n collection: string = DEFAULT_COLLECTION,\n): Promise<void> {\n await db\n .collection(collection)\n .createIndex(\n { kp: 1, ns: 1, seg: 1, ck: 1 },\n { name: INDEX_NAME, collation: SIMPLE_COLLATION },\n );\n}\n\nexport class MongoWarmDriver implements IWarmDriver {\n private readonly col: Collection<WarmDoc>;\n private readonly keyPrefix: string;\n private readonly batch: number;\n\n constructor(options: MongoWarmDriverOptions) {\n this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);\n const batch = options.listPageSize ?? DEFAULT_BATCH;\n if (!Number.isSafeInteger(batch) || batch < 1) {\n throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);\n }\n this.batch = batch;\n this.col = options.db.collection<WarmDoc>(options.collection ?? DEFAULT_COLLECTION);\n }\n\n // Reads are strongly consistent when the Db targets the primary (see the `db` option), so the optional\n // `WarmReadOptions` hint would be a no-op — the structurally-optional param is simply omitted, as in the\n // LocalFs / Postgres / Redis drivers.\n async get(ref: ChunkRef): Promise<WarmRow | null> {\n const _id = chunkDocId(this.keyPrefix, ref); // validates ref\n let doc: WarmDoc | null;\n try {\n doc = await this.col.findOne({ _id }, { collation: SIMPLE_COLLATION });\n } catch (err) {\n throw this.mapError(err);\n }\n return doc === null ? null : this.rowFrom(doc, ref.chunkKey);\n }\n\n async putConditional(\n ref: ChunkRef,\n bytes: Uint8Array,\n expected: Token | NoRow,\n ): Promise<{ token: Token }> {\n const _id = chunkDocId(this.keyPrefix, ref); // validates ref\n const token = randomUUID();\n const payload = Buffer.from(bytes); // copy: severs any caller-owned/reused input buffer; stored as Binary\n try {\n if (expected === NO_ROW) {\n const { kp, ns, seg } = segmentScope(this.keyPrefix, ref);\n await this.col.insertOne({ _id, kp, ns, seg, ck: ref.chunkKey, token, payload });\n } else {\n const res = await this.col.updateOne(\n { _id, token: expected },\n { $set: { token, payload } },\n { collation: SIMPLE_COLLATION },\n );\n if (res.matchedCount !== 1) {\n throw new WriteConflictError(`OCC conflict on chunk ${ref.chunkKey}`);\n }\n }\n return { token };\n } catch (err) {\n // A duplicate `_id` is the deterministic create-if-absent conflict — never a transient.\n if (isDuplicateKey(err)) {\n throw new WriteConflictError(`chunk ${ref.chunkKey} already exists (create-if-absent)`);\n }\n throw this.mapError(err); // WriteConflictError (from the update branch) passes straight through\n }\n }\n\n async deleteConditional(ref: ChunkRef, expected: Token): Promise<void> {\n const _id = chunkDocId(this.keyPrefix, ref); // validates ref\n let deletedCount: number;\n try {\n deletedCount = (\n await this.col.deleteOne({ _id, token: expected }, { collation: SIMPLE_COLLATION })\n ).deletedCount;\n } catch (err) {\n throw this.mapError(err);\n }\n if (deletedCount !== 1) {\n throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);\n }\n }\n\n async *listChunks(ref: SegmentRef): AsyncIterable<{ chunkKey: number } & WarmRow> {\n const scope = segmentScope(this.keyPrefix, ref); // validates ref\n // The driver's cursor streams in `batchSize` batches, so peak memory is bounded on a wide segment; the\n // `{ kp, ns, seg, ck }` index (ensureMongoWarmIndexes) serves the filter + numeric ascending sort — and is\n // effectively required: without it a wide segment's sort spills past MongoDB's 32 MB in-memory-sort limit\n // and errors (292), not just runs slow.\n const cursor = this.col\n .find(scope, { collation: SIMPLE_COLLATION })\n .sort({ ck: 1 })\n .batchSize(this.batch);\n try {\n for await (const doc of cursor) {\n // Validate the yielded chunkKey (untrusted-data posture, like the sibling drivers) — a foreign/corrupt\n // doc matching the scope but lacking a numeric `ck` must not surface as `{ chunkKey: undefined }`.\n if (typeof doc.ck !== 'number') {\n throw new IntegrityError('warm doc is missing its numeric chunk key (ck)');\n }\n yield { chunkKey: doc.ck, ...this.rowFrom(doc, doc.ck) };\n }\n } catch (err) {\n throw this.mapError(err);\n }\n }\n\n /** Build a {@link WarmRow} from a doc; reject one missing/!typed fields (corrupt/foreign). */\n private rowFrom(doc: WarmDoc, chunkKey: number): WarmRow {\n if (typeof doc.token !== 'string' || doc.token === '') {\n throw new IntegrityError(`warm doc for chunk ${chunkKey} is missing its token`);\n }\n return { token: doc.token, bytes: decodePayload(doc.payload, chunkKey) };\n }\n\n /** Reclassify a transient Mongo fault as a retryable {@link TransientError}; else propagate unchanged. */\n private mapError(err: unknown): unknown {\n if (isTransient(err)) {\n return new TransientError(\n `transient MongoDB fault: ${(err as { message?: unknown } | null)?.message ?? 'unknown'}`,\n { cause: err },\n );\n }\n return err;\n }\n}\n\n/**\n * Decode a stored payload to owned bytes, duck-typed so the driver needs no runtime `mongodb`/`bson` import\n * (kept `import type`, so `cloud-roaring/mongodb`'s built bundle has no runtime SDK dependency). A Node\n * `Buffer` is a `Uint8Array` (the `promoteBuffers` case); a BSON `Binary` exposes its bytes on `.buffer`.\n */\nfunction decodePayload(p: unknown, chunkKey: number): Uint8Array {\n if (p instanceof Uint8Array) return new Uint8Array(p); // Buffer / Uint8Array — copy\n const inner = (p as { buffer?: unknown } | null)?.buffer; // BSON Binary → .buffer is a Buffer\n if (inner instanceof Uint8Array) return new Uint8Array(inner);\n throw new IntegrityError(`warm doc for chunk ${chunkKey} has no readable payload`);\n}\n"]}
|
package/dist/mysql/index.cjs
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var crypto = require('crypto');
|
|
4
|
-
|
|
5
|
-
// src/drivers/mysql/warm.ts
|
|
6
|
-
|
|
7
|
-
// src/core/errors.ts
|
|
8
|
-
var ERROR_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error");
|
|
9
|
-
var TRANSIENT_BRAND = /* @__PURE__ */ Symbol.for("cloud-roaring.error.transient");
|
|
10
|
-
var CloudRoaringError = class extends Error {
|
|
11
|
-
/** Cross-bundle brand — see the predicates ({@link isCloudRoaringError}, …). Non-enumerable-ish (symbol key ⇒ not in JSON). */
|
|
12
|
-
[ERROR_BRAND] = true;
|
|
13
|
-
constructor(message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = new.target.name;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
var ValidationError = class extends CloudRoaringError {
|
|
19
|
-
};
|
|
20
|
-
var WriteConflictError = class extends CloudRoaringError {
|
|
21
|
-
};
|
|
22
|
-
var IntegrityError = class extends CloudRoaringError {
|
|
23
|
-
};
|
|
24
|
-
var TransientError = class extends CloudRoaringError {
|
|
25
|
-
/** A second brand so the whole transient subtree (incl. {@link TimeoutError}) is classifiable cross-bundle. */
|
|
26
|
-
[TRANSIENT_BRAND] = true;
|
|
27
|
-
constructor(message, options) {
|
|
28
|
-
super(message);
|
|
29
|
-
if (options && "cause" in options) this.cause = options.cause;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
function hasBrand(err, brand) {
|
|
33
|
-
return typeof err === "object" && err !== null && err[brand] === true;
|
|
34
|
-
}
|
|
35
|
-
function isCloudRoaringError(err) {
|
|
36
|
-
return hasBrand(err, ERROR_BRAND);
|
|
37
|
-
}
|
|
38
|
-
function isWriteConflictError(err) {
|
|
39
|
-
return isCloudRoaringError(err) && err.name === "WriteConflictError";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/core/ports.ts
|
|
43
|
-
var NO_ROW = /* @__PURE__ */ Symbol.for("cloud-roaring.no-row");
|
|
44
|
-
|
|
45
|
-
// src/core/validate.ts
|
|
46
|
-
var CHUNK_KEY_MAX = 65535;
|
|
47
|
-
var NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/;
|
|
48
|
-
function validatePart(value, field) {
|
|
49
|
-
if (typeof value !== "string" || !NAME.test(value) || value.includes("..")) {
|
|
50
|
-
throw new ValidationError(
|
|
51
|
-
`${field} must match ${String(NAME)} and contain no "..": got ${JSON.stringify(value)}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function validateSegmentRef(ref) {
|
|
56
|
-
validatePart(ref.segment, "segment");
|
|
57
|
-
if (ref.namespace !== void 0) validatePart(ref.namespace, "namespace");
|
|
58
|
-
}
|
|
59
|
-
function validateChunkRef(ref) {
|
|
60
|
-
validateSegmentRef(ref);
|
|
61
|
-
if (!Number.isInteger(ref.chunkKey) || ref.chunkKey < 0 || ref.chunkKey > CHUNK_KEY_MAX) {
|
|
62
|
-
throw new ValidationError(
|
|
63
|
-
`chunkKey must be an integer in [0, ${CHUNK_KEY_MAX}]; got ${ref.chunkKey}`
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// src/drivers/_shared/keys.ts
|
|
69
|
-
var DEFAULT_NAMESPACE = "_default";
|
|
70
|
-
function namespacePart(namespace) {
|
|
71
|
-
return namespace ?? DEFAULT_NAMESPACE;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/drivers/mysql/keys.ts
|
|
75
|
-
var IDENT = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
|
|
76
|
-
function validateAndQuoteTable(table) {
|
|
77
|
-
const parts = table.split(".");
|
|
78
|
-
if (parts.length > 2 || parts.some((p) => !IDENT.test(p))) {
|
|
79
|
-
throw new ValidationError(
|
|
80
|
-
`invalid table name ${JSON.stringify(table)} \u2014 expected an identifier or "db.table" (letters, digits, underscore; \u226464 chars; leading letter/underscore)`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
return parts.map((p) => `\`${p}\``).join(".");
|
|
84
|
-
}
|
|
85
|
-
var MAX_KEY_PREFIX_CHARS = 191;
|
|
86
|
-
function normalizeKeyPrefix(prefix) {
|
|
87
|
-
if (prefix === void 0 || prefix === "") return "";
|
|
88
|
-
let chars = 0;
|
|
89
|
-
for (const ch of prefix) {
|
|
90
|
-
chars++;
|
|
91
|
-
if (ch.charCodeAt(0) < 32) {
|
|
92
|
-
throw new ValidationError("keyPrefix must not contain control characters");
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (chars > MAX_KEY_PREFIX_CHARS) {
|
|
96
|
-
throw new ValidationError(
|
|
97
|
-
`keyPrefix must be at most ${MAX_KEY_PREFIX_CHARS} characters (the key_prefix column width); got ${chars}`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return prefix;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// src/drivers/mysql/mysql-errors.ts
|
|
104
|
-
function errno(err) {
|
|
105
|
-
const n = err?.errno;
|
|
106
|
-
return typeof n === "number" ? n : void 0;
|
|
107
|
-
}
|
|
108
|
-
function code(err) {
|
|
109
|
-
const c = err?.code;
|
|
110
|
-
return typeof c === "string" ? c : void 0;
|
|
111
|
-
}
|
|
112
|
-
function isDuplicateKey(err) {
|
|
113
|
-
return errno(err) === 1062 || code(err) === "ER_DUP_ENTRY";
|
|
114
|
-
}
|
|
115
|
-
function isTransient(err) {
|
|
116
|
-
const n = errno(err);
|
|
117
|
-
if (n === 1205 || // ER_LOCK_WAIT_TIMEOUT
|
|
118
|
-
n === 1213 || // ER_LOCK_DEADLOCK
|
|
119
|
-
n === 1040 || // ER_CON_COUNT_ERROR (too many connections)
|
|
120
|
-
n === 1203 || // ER_TOO_MANY_USER_CONNECTIONS
|
|
121
|
-
n === 1053 || // ER_SERVER_SHUTDOWN
|
|
122
|
-
n === 1927 || // ER_CONNECTION_KILLED
|
|
123
|
-
n === 2006 || // CR_SERVER_GONE_ERROR
|
|
124
|
-
n === 2013) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
const c = code(err);
|
|
128
|
-
return c === "PROTOCOL_CONNECTION_LOST" || c === "PROTOCOL_SEQUENCE_TIMEOUT" || c === "CR_SERVER_GONE_ERROR" || c === "CR_SERVER_LOST" || c === "ECONNRESET" || c === "ECONNABORTED" || c === "ETIMEDOUT" || c === "ESOCKETTIMEDOUT" || c === "ECONNREFUSED" || c === "EHOSTUNREACH" || c === "ENETUNREACH" || c === "EPIPE" || c === "EAI_AGAIN" || c === "ENOTFOUND";
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// src/drivers/mysql/warm.ts
|
|
132
|
-
var DEFAULT_TABLE = "cloud_roaring_warm";
|
|
133
|
-
var DEFAULT_LIST_BATCH = 1e3;
|
|
134
|
-
function mysqlWarmTableDDL(table = DEFAULT_TABLE) {
|
|
135
|
-
const t = validateAndQuoteTable(table);
|
|
136
|
-
return `CREATE TABLE IF NOT EXISTS ${t} (
|
|
137
|
-
key_prefix VARCHAR(191) NOT NULL,
|
|
138
|
-
namespace VARCHAR(256) NOT NULL,
|
|
139
|
-
segment VARCHAR(256) NOT NULL,
|
|
140
|
-
chunk_key INT NOT NULL,
|
|
141
|
-
token CHAR(36) NOT NULL,
|
|
142
|
-
payload LONGBLOB NOT NULL,
|
|
143
|
-
PRIMARY KEY (key_prefix, namespace, segment, chunk_key)
|
|
144
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;`;
|
|
145
|
-
}
|
|
146
|
-
var MysqlWarmDriver = class {
|
|
147
|
-
pool;
|
|
148
|
-
table;
|
|
149
|
-
// validated + backtick-quoted for interpolation
|
|
150
|
-
keyPrefix;
|
|
151
|
-
listBatch;
|
|
152
|
-
constructor(options) {
|
|
153
|
-
this.pool = options.pool;
|
|
154
|
-
this.table = validateAndQuoteTable(options.table ?? DEFAULT_TABLE);
|
|
155
|
-
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix);
|
|
156
|
-
const batch = options.listPageSize ?? DEFAULT_LIST_BATCH;
|
|
157
|
-
if (!Number.isSafeInteger(batch) || batch < 1) {
|
|
158
|
-
throw new ValidationError(`listPageSize must be a positive safe integer; got ${batch}`);
|
|
159
|
-
}
|
|
160
|
-
this.listBatch = batch;
|
|
161
|
-
}
|
|
162
|
-
// Reads are always strongly consistent (single primary), so the optional `WarmReadOptions` hint would be a
|
|
163
|
-
// no-op — the structurally-optional param is simply omitted (still satisfies IWarmDriver), as in LocalFs/pg.
|
|
164
|
-
async get(ref) {
|
|
165
|
-
validateChunkRef(ref);
|
|
166
|
-
try {
|
|
167
|
-
const [rows] = await this.pool.query(
|
|
168
|
-
`SELECT token, payload FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ?`,
|
|
169
|
-
[this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey]
|
|
170
|
-
);
|
|
171
|
-
const row = rows[0];
|
|
172
|
-
return row === void 0 ? null : this.rowFrom(row, ref.chunkKey);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
throw this.mapError(err);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async putConditional(ref, bytes, expected) {
|
|
178
|
-
validateChunkRef(ref);
|
|
179
|
-
const token = crypto.randomUUID();
|
|
180
|
-
const payload = Buffer.from(bytes);
|
|
181
|
-
const ns = namespacePart(ref.namespace);
|
|
182
|
-
try {
|
|
183
|
-
if (expected === NO_ROW) {
|
|
184
|
-
await this.pool.query(
|
|
185
|
-
`INSERT INTO ${this.table} (key_prefix, namespace, segment, chunk_key, token, payload) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
186
|
-
[this.keyPrefix, ns, ref.segment, ref.chunkKey, token, payload]
|
|
187
|
-
);
|
|
188
|
-
} else {
|
|
189
|
-
const [res] = await this.pool.query(
|
|
190
|
-
`UPDATE ${this.table} SET token = ?, payload = ? WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,
|
|
191
|
-
[token, payload, this.keyPrefix, ns, ref.segment, ref.chunkKey, expected]
|
|
192
|
-
);
|
|
193
|
-
if (res.affectedRows !== 1) {
|
|
194
|
-
throw new WriteConflictError(`OCC conflict on chunk ${ref.chunkKey}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return { token };
|
|
198
|
-
} catch (err) {
|
|
199
|
-
if (isWriteConflictError(err)) throw err;
|
|
200
|
-
if (expected === NO_ROW && isDuplicateKey(err)) {
|
|
201
|
-
throw new WriteConflictError(`chunk ${ref.chunkKey} already exists (create-if-absent)`);
|
|
202
|
-
}
|
|
203
|
-
throw this.mapError(err);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async deleteConditional(ref, expected) {
|
|
207
|
-
validateChunkRef(ref);
|
|
208
|
-
try {
|
|
209
|
-
const [res] = await this.pool.query(
|
|
210
|
-
`DELETE FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key = ? AND token = ?`,
|
|
211
|
-
[this.keyPrefix, namespacePart(ref.namespace), ref.segment, ref.chunkKey, expected]
|
|
212
|
-
);
|
|
213
|
-
if (res.affectedRows !== 1) {
|
|
214
|
-
throw new WriteConflictError(`fenced-delete conflict on chunk ${ref.chunkKey}`);
|
|
215
|
-
}
|
|
216
|
-
} catch (err) {
|
|
217
|
-
if (isWriteConflictError(err)) throw err;
|
|
218
|
-
throw this.mapError(err);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
async *listChunks(ref) {
|
|
222
|
-
validateSegmentRef(ref);
|
|
223
|
-
const ns = namespacePart(ref.namespace);
|
|
224
|
-
let after = -1;
|
|
225
|
-
for (; ; ) {
|
|
226
|
-
let rows;
|
|
227
|
-
try {
|
|
228
|
-
const [res] = await this.pool.query(
|
|
229
|
-
`SELECT chunk_key, token, payload FROM ${this.table} WHERE key_prefix = ? AND namespace = ? AND segment = ? AND chunk_key > ? ORDER BY chunk_key ASC LIMIT ?`,
|
|
230
|
-
[this.keyPrefix, ns, ref.segment, after, this.listBatch]
|
|
231
|
-
);
|
|
232
|
-
rows = res;
|
|
233
|
-
} catch (err) {
|
|
234
|
-
throw this.mapError(err);
|
|
235
|
-
}
|
|
236
|
-
for (const row of rows) {
|
|
237
|
-
const chunkKey = row.chunk_key;
|
|
238
|
-
if (typeof chunkKey !== "number") {
|
|
239
|
-
throw new IntegrityError("warm row is missing its chunk_key");
|
|
240
|
-
}
|
|
241
|
-
yield { chunkKey, ...this.rowFrom(row, chunkKey) };
|
|
242
|
-
after = chunkKey;
|
|
243
|
-
}
|
|
244
|
-
if (rows.length < this.listBatch) return;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Build a {@link WarmRow} from a raw row. `token` must be a non-empty string and `payload` a Buffer — a
|
|
249
|
-
* missing/typo'd column means a corrupt or foreign row, which we reject (untrusted-data posture) rather
|
|
250
|
-
* than paper over. The returned bytes are a fresh copy (driver-owned, read-only per the contract).
|
|
251
|
-
*/
|
|
252
|
-
rowFrom(row, chunkKey) {
|
|
253
|
-
if (typeof row.token !== "string" || row.token === "") {
|
|
254
|
-
throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its token`);
|
|
255
|
-
}
|
|
256
|
-
if (!(row.payload instanceof Uint8Array)) {
|
|
257
|
-
throw new IntegrityError(`warm row for chunk ${chunkKey} is missing its payload`);
|
|
258
|
-
}
|
|
259
|
-
return { token: row.token, bytes: new Uint8Array(row.payload) };
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Reclassify a transient MySQL fault (lock-wait/deadlock, connection lost, too-many-connections, dropped
|
|
263
|
-
* socket) as a retryable {@link TransientError}; everything else propagates unchanged. Applied at every
|
|
264
|
-
* query site so callers + the retry decorator only ever see typed errors. A deterministic OCC/duplicate-key
|
|
265
|
-
* conflict is signalled by the driver itself — never reclassified here.
|
|
266
|
-
*/
|
|
267
|
-
mapError(err) {
|
|
268
|
-
if (isTransient(err)) {
|
|
269
|
-
return new TransientError(
|
|
270
|
-
`transient MySQL fault: ${err?.code ?? err?.errno ?? "unknown"}`,
|
|
271
|
-
{ cause: err }
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
return err;
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
exports.MysqlWarmDriver = MysqlWarmDriver;
|
|
279
|
-
exports.mysqlWarmTableDDL = mysqlWarmTableDDL;
|
|
280
|
-
//# sourceMappingURL=index.cjs.map
|
|
281
|
-
//# sourceMappingURL=index.cjs.map
|