Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -684,3 +684,7 @@ All notable changes to this project will be documented in this file. Breaking ch
## [3.9.3]
### Changed
- `formatResponse` now constructs its `ElectroError` stack trace lazily in the error path instead of on every successful response, avoiding an unnecessary `Error` allocation on the happy path for read-heavy workloads. [[#587]](https://github.com/tywalch/electrodb/issues/587)

## [3.10.0]
### Added
- [Issue #585] Opt-in JIT compilation of the item format path. When enabled, ElectroDB compiles a per-schema formatter that replaces the interpreted formatter used to shape `get`/`query`/`scan`/`parse` responses, reducing per-item overhead on reads. Enable per-entity with `{ compile: true }`. Compilation is skipped automatically for schemas with user-defined getters and falls back to the interpreted path in environments without `new Function` (e.g. a strict CSP), so behavior is unchanged when it is off or unavailable.
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5490,6 +5490,8 @@ export type EntityConfiguration = {
version?: string;
};
ignoreOwnership?: boolean;
/** JIT-compile the read-path formatter (default false) */
compile?: boolean;
};

export class Entity<
Expand Down Expand Up @@ -6097,6 +6099,8 @@ export type ServiceConfiguration = {
client?: DocumentClient;
listeners?: Array<ElectroEventListener>;
logger?: ElectroEventListener;
/** JIT-compile member entities joined as raw models (no-op for pre-built Entity instances) */
compile?: boolean;
};

declare function createWriteTransaction<
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
{
"name": "electrodb",
"version": "3.9.3",
"version": "3.10.0",
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb",
"main": "index.js",
"scripts": {
"build": "sh buildbrowser.sh",
"build:browser": "browserify playground/browser.js -o playground/bundle.js",
"test": "./test.sh",
"test:ci": "npm install && npm test",
"test:run": "npm run test:types && npm run test:init && npm run test:unit",
"test:run": "npm run test:types && npm run test:init && npm run test:unit:parity",
"test:init": "node ./test/init.js",
"test:init:hard": "node ./test/init.js --recreate",
"test:unit": "mocha -r ts-node/register ./test/**.spec.*",
"test:unit:parity": "mocha -r ts-node/register --file ./test/support/compile-parity.js ./test/**.spec.*",
"test:snapshots": "mocha -r ts-node/register test/offline.compile.spec.js",
"test:snapshots:update": "UPDATE_SNAPSHOTS=1 mocha -r ts-node/register test/offline.compile.spec.js",
"test:types": "tsd",
"test:format": "prettier -c src/**/*.js examples/**/*",
"coverage": "npm run test:init:hard && nyc npm run test:unit && nyc report --reporter=text-lcov | coveralls",
Expand Down
18 changes: 13 additions & 5 deletions src/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ class Entity {
this._validateModel(model);
this.version = EntityVersions.v1;
this.model = this._parseModel(model, this.config);
if (this.config.compile === true) {
this.model.schema.compileRetrievalFormatters();
}
/** start beta/v1 condition **/
this.config.table = config.table || model.table;
/** end beta/v1 condition **/
Expand Down Expand Up @@ -1069,10 +1072,9 @@ class Entity {
if (item === undefined || item === null) {
return null;
}
const config = {
ignoreOwnership: true,
...(options || {}),
};
const config = this._normalizeExecutionOptions({
provided: [{ ignoreOwnership: true }, options || {}],
});
return this.formatResponse(item, TableIndex, config);
}

Expand Down Expand Up @@ -1747,7 +1749,7 @@ class Entity {
}
}

return provided.filter(Boolean).reduce((config, option) => {
const normalized = provided.filter(Boolean).reduce((config, option) => {
if (typeof option.order === "string") {
switch (option.order.toLowerCase()) {
case "asc":
Expand Down Expand Up @@ -2026,6 +2028,12 @@ class Entity {
config.params = Object.assign({}, config.params, option.params);
return config;
}, config);

const returnAttributes = new Set(normalized.attributes);
normalized._returnAttributesFilter =
returnAttributes.size > 0 ? returnAttributes : null;

return normalized;
}

_applyParameterOptions({ params = {}, options = {} } = {}) {
Expand Down
235 changes: 235 additions & 0 deletions src/format.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
"use strict";

const SET = "set";
const MAP = "map";
const LIST = "list";
const STRING = "string";
const ENUM = "enum";

let supported = null;

function supportsCompilation() {
if (supported === null) {
try {
new Function("");
supported = true;
} catch (err) {
supported = false;
}
}
return supported;
}

function hasOwn(obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key);
}

function fromSet(value) {
if (value === undefined || value === null) {
return value;
}
const type = typeof value;
if (type === "string" || type === "number" || type === "boolean") {
return value;
}
if (Array.isArray(value)) {
return value;
}
if (value.wrapperName === "Set") {
return [...value.values];
}
if (value.constructor.name === "Set") {
return Array.from(value);
}
return value;
}

function generate(schema) {
const attributes = schema.attributes;
const hiddenAttributes = schema.hiddenAttributes;
const names = Object.keys(attributes);
const unformatters = [];
const lines = [];
let temp = 0;
const J = JSON.stringify;
const push = (indent, text) => lines.push(" ".repeat(indent) + text);
const nextVar = (prefix) => `${prefix}${temp++}`;

const isScalarPassthrough = (attribute) =>
attribute.type !== MAP &&
attribute.type !== LIST &&
attribute.type !== SET &&
!(
(attribute.type === STRING || attribute.type === ENUM) &&
attribute.isKeyField
);

function emitGuardedValue({
attribute,
valueVar,
indent,
place,
guardExtra,
guard = true,
}) {
const checks = [];
if (guard) checks.push(`${valueVar} !== undefined`);
if (guardExtra) checks.push(guardExtra);
const cond = checks.length ? checks.join(" && ") : null;
const guarded = (body) => (cond ? `if (${cond}) ${body}` : body);
if (attribute.type === MAP) {
const result = nextVar("r");
push(indent, guarded(`{`));
push(indent + 1, `var ${result} = {};`);
emitMapChildren({
mapAttribute: attribute,
sourceVar: valueVar,
targetVar: result,
indent: indent + 1,
});
push(indent + 1, place(result));
push(indent, `}`);
} else if (attribute.type === LIST) {
const items = attribute.items;
if (isScalarPassthrough(items) && !items.hidden) {
push(indent, guarded(place(valueVar)));
} else {
const result = nextVar("r");
push(indent, guarded(`{`));
push(indent + 1, `var ${result} = [];`);
emitListLoop({
listAttribute: attribute,
valuesVar: valueVar,
targetVar: result,
indent: indent + 1,
});
push(indent + 1, place(result));
push(indent, `}`);
}
} else if (attribute.type === SET) {
push(indent, guarded(place(`fromSet(${valueVar})`)));
} else if (
(attribute.type === STRING || attribute.type === ENUM) &&
attribute.isKeyField
) {
const index = unformatters.push(attribute.unformat) - 1;
push(indent, guarded(place(`unformat[${index}](${valueVar})`)));
} else {
push(indent, guarded(place(valueVar)));
}
}

function emitMapChildren({ mapAttribute, sourceVar, targetVar, indent }) {
const children = mapAttribute.properties.attributes;
for (const childName of Object.keys(children)) {
const child = children[childName];
if (child.hidden) {
continue;
}
const value = nextVar("v");
push(indent, `var ${value} = ${sourceVar}[${J(child.field)}];`);
emitGuardedValue({
attribute: child,
valueVar: value,
indent,
place: (expr) => `${targetVar}[${J(childName)}] = ${expr};`,
});
}
}

function emitListLoop({ listAttribute, valuesVar, targetVar, indent }) {
const items = listAttribute.items;
if (items.hidden) return;
const element = nextVar("v");
push(indent, `{`);
push(indent + 1, `const values = ${valuesVar};`);
push(indent + 1, `for (const ${element} of values) {`);
emitGuardedValue({
attribute: items,
valueVar: element,
indent: indent + 2,
place: (expr) => `${targetVar}.push(${expr});`,
guard: false,
});
push(indent + 1, `}`);
push(indent, `}`);
}

function emitPrologue() {
push(1, `var out = {};`);
}

function emitRootAttribute(name) {
const attribute = attributes[name];
if (hiddenAttributes.has(name)) {
return;
}
const value = nextVar("v");
push(1, `var ${value} = item[${J(attribute.field)}];`);
emitGuardedValue({
attribute,
valueVar: value,
indent: 1,
place: (expr) => `out[${J(name)}] = ${expr};`,
guardExtra: `(filter == null || filter.has(${J(name)}))`,
});
}

function emitAttributeLoop() {
for (const name of names) {
emitRootAttribute(name);
}
}

function emitReturn() {
push(1, `return out;`);
}

function assembleFactory() {
const source =
'"use strict";\n' +
"return function fromDocument(item, filter) {\n" +
lines.join("\n") +
"\n};\n" +
"//# sourceURL=electrodb-jit-format.js\n";
const factory = new Function("fromSet", "unformat", source);
return {
fromDocument: factory(fromSet, unformatters),
source,
};
}

emitPrologue();
emitAttributeLoop();
emitReturn();
return assembleFactory();
}

function isCompilable(schema) {
for (const entry of schema.traverser.getAll()) {
if (entry[1].hasUserGet) {
return false;
}
}
return true;
}

function compileDocumentFormatter(schema) {
if (!supportsCompilation() || !isCompilable(schema)) {
return null;
}
try {
return generate(schema);
} catch (err) {
return null;
}
}

const runtime = { fromSet, hasOwn };

module.exports = {
supportsCompilation,
isCompilable,
compileDocumentFormatter,
runtime,
};
Loading