Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

- **Open-source release.** Apache-2.0 license; `package.json` now carries `license`, `author`, `repository`, `bugs`, `homepage`, `keywords`, `engines`, `sideEffects`, and a `files` allowlist. `private: true` removed.
- **Build pipeline.** New `tsconfig.build.json` + `npm run build` emit ESM `dist/` via `tsc` with `rewriteRelativeImportExtensions` (rewrites `.ts` → `.js`). `exports` map points at `dist/index.js`, `dist/react/index.js`, `dist/vue/index.js` with matching `.d.ts` declarations. `prepublishOnly` runs `clean` + `check` + `build` so the npm tarball cannot ship without a green gate.
- **`mqdb-wasm` bumped to `0.3.2`** (from `0.3.1`).

### Fixed

- **Polynomial-time backtracking in WASM corruption detection.** `MemoryStore.isWasmCorrupted` and `PersistenceLayer.isDbCorrupted` previously matched error messages with a regex containing `transaction.*null`, which has polynomial complexity on inputs with many `transaction` prefixes (CodeQL `js/polynomial-redos`). Both helpers now delegate to a shared `isCorruptionError(err, extraPatterns)` exported from `internal-wasm-error.ts`, which uses substring `indexOf` checks — same semantics, linear time, and the corruption-pattern list lives in one place.

## 0.3.0

Expand Down
18 changes: 11 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
}
},
"dependencies": {
"mqdb-wasm": "^0.3.1",
"mqdb-wasm": "^0.3.2",
"mqtt5-wasm": "^1.0.0"
},
"peerDependencies": {
Expand Down
11 changes: 9 additions & 2 deletions scripts/extract-changelog.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ if (!version) {
process.exit(1);
}

if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?(?:\+[0-9A-Za-z.]+)?$/.test(version)) {
console.error(`extract-changelog.mjs: "${version}" is not a valid SemVer version.`);
process.exit(1);
}

const here = dirname(fileURLToPath(import.meta.url));
const changelogPath = resolve(here, '..', 'CHANGELOG.md');
const text = readFileSync(changelogPath, 'utf8');
const lines = text.split('\n');

const headingPattern = new RegExp(`^##\\s+${version.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\b`);
const startIdx = lines.findIndex((l) => headingPattern.test(l));
const heading = `## ${version}`;
const startIdx = lines.findIndex(
(l) => l === heading || l.startsWith(`${heading} `) || l.startsWith(`${heading}\t`)
);
if (startIdx === -1) {
console.error(`CHANGELOG.md: no "## ${version}" section found.`);
process.exit(1);
Expand Down
16 changes: 16 additions & 0 deletions src/internal-wasm-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,19 @@ export async function wrapAsync<T>(method: string, fn: () => Promise<T>): Promis
throw wrapWasmError(method, err);
}
}

const BASE_CORRUPTION_PATTERNS = ['arg0 is null', 'transaction error', 'unreachable'] as const;

export function isCorruptionError(err: unknown, extraPatterns: readonly string[] = []): boolean {
const inner = err instanceof MqdbError ? err.cause : err;
if (inner instanceof Error && inner.name === 'RuntimeError') return true;
const msg = (inner instanceof Error ? inner.message : String(inner)).toLowerCase();
for (const pattern of BASE_CORRUPTION_PATTERNS) {
if (msg.includes(pattern)) return true;
}
for (const pattern of extraPatterns) {
if (msg.includes(pattern)) return true;
}
const txIdx = msg.indexOf('transaction');
return txIdx !== -1 && msg.indexOf('null', txIdx) !== -1;
}
6 changes: 2 additions & 4 deletions src/memory-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Database } from 'mqdb-wasm';
import type initWasm from 'mqdb-wasm';
import { wrapWasmError } from './internal-wasm-error.ts';
import { wrapWasmError, isCorruptionError } from './internal-wasm-error.ts';
import type { StoreConfig, MutationEvent, MemoryStore, OriginTag } from './types.ts';

type SubscriptionCallback = () => void;
Expand Down Expand Up @@ -175,9 +175,7 @@ class MemoryStoreImpl implements MemoryStore {
}

private isWasmCorrupted(err: unknown): boolean {
if (err instanceof Error && err.name === 'RuntimeError') return true;
const msg = err instanceof Error ? err.message : String(err);
return /transaction.*null|arg0 is null|transaction error|unreachable/i.test(msg);
return isCorruptionError(err);
}

private notifyCorruption(): void {
Expand Down
9 changes: 2 additions & 7 deletions src/persistence-layer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Database } from 'mqdb-wasm';
import type { StoreConfig, PersistenceLayer, EntityDefinition } from './types.ts';
import { wrapWasmError, MqdbError } from './internal-wasm-error.ts';
import { wrapWasmError, isCorruptionError } from './internal-wasm-error.ts';

const PENDING_SYNC_DEFINITION: EntityDefinition = {
fields: [
Expand Down Expand Up @@ -396,12 +396,7 @@ class PersistenceLayerImpl implements PersistenceLayer {
}

private isDbCorrupted(err: unknown): boolean {
const inner = err instanceof MqdbError ? err.cause : err;
if (inner instanceof Error && inner.name === 'RuntimeError') return true;
const msg = inner instanceof Error ? inner.message : String(inner);
return /transaction.*null|arg0 is null|transaction error|index out of bounds|database is busy|unreachable/i.test(
msg
);
return isCorruptionError(err, ['index out of bounds', 'database is busy']);
}

private async recoverDb(): Promise<boolean> {
Expand Down
Loading