@docstack/client 0.1.8 → 0.2.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/README.md +350 -122
- package/lib/core/index.d.ts +3 -1
- package/lib/core/stack.d.ts +147 -0
- package/lib/core/sync/index.d.ts +17 -2
- package/lib/core/transaction-engine/errors.d.ts +57 -0
- package/lib/core/transaction-engine/handle.d.ts +165 -0
- package/lib/core/transaction-engine/index.d.ts +82 -0
- package/lib/core/transaction-engine/overlay.d.ts +66 -0
- package/lib/core/transaction-engine/stage.d.ts +50 -0
- package/lib/core/transaction-engine/sweep.d.ts +26 -0
- package/lib/index.d.ts +12 -2
- package/lib/index.js +4083 -267
- package/lib/index.umd.js +4094 -269
- package/lib/utils/index.d.ts +4 -2
- package/package.json +2 -1
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import PouchDB from 'pouchdb-browser';
|
|
2
|
-
import z$1, { z } from 'zod';
|
|
3
|
-
import PouchDBFind from 'pouchdb-find';
|
|
4
2
|
import semver from 'semver';
|
|
5
3
|
import { diff } from 'jsondiffpatch';
|
|
4
|
+
import z$1, { z } from 'zod';
|
|
5
|
+
import PouchDBFind from 'pouchdb-find';
|
|
6
6
|
|
|
7
7
|
/******************************************************************************
|
|
8
8
|
Copyright (c) Microsoft Corporation.
|
|
@@ -925,6 +925,80 @@ Attribute.build = async (attributeObj) => {
|
|
|
925
925
|
}
|
|
926
926
|
};
|
|
927
927
|
|
|
928
|
+
const attributeEffect = async (operation, model, classObj, doc) => {
|
|
929
|
+
const attribute = new Attribute(null, model.name, model.type, model.description, model.config);
|
|
930
|
+
if (operation === "add" && !(attribute.name in doc)) {
|
|
931
|
+
// Only documents that lack the key get the empty value. A document may
|
|
932
|
+
// already hold one - a repair patch re-declaring an attribute the model
|
|
933
|
+
// lost is exactly that case (ADR-0038) - and stamping `getEmpty()` over
|
|
934
|
+
// it would be data loss; validation below still runs against it.
|
|
935
|
+
doc = Object.assign(Object.assign({}, doc), attribute.getEmpty());
|
|
936
|
+
}
|
|
937
|
+
if (operation === "delete") {
|
|
938
|
+
delete doc[attribute.name];
|
|
939
|
+
}
|
|
940
|
+
else { // when addition or change also perform validation
|
|
941
|
+
const res = await attribute.validate(doc[attribute.name]);
|
|
942
|
+
// `safeParseAsync` resolves to a result object, which is truthy whether or not the
|
|
943
|
+
// value is valid: the outcome has to be read off `success`, otherwise every invalid
|
|
944
|
+
// value passes straight through.
|
|
945
|
+
if (!res.success) {
|
|
946
|
+
throw new Error(`Attribute '${classObj.name}.${attribute.name}' ${operation} fails for current document because of its validation: ${z$1.prettifyError(res.error)}`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (attribute.isPrimaryKey()) {
|
|
950
|
+
const result = classObj.bulkUniqueCheck(classObj.getPrimaryKeys());
|
|
951
|
+
if (!result) {
|
|
952
|
+
throw new Error(`With attribute '${attribute.name}' ${operation} of class '${classObj.name}', docs fail primary keys check.`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (operation !== "delete" && attribute.isMandatory() && doc[attribute.name] == undefined) {
|
|
956
|
+
throw new Error(`Attribute '${classObj.name}.${attribute.name}' is mandatory but has no value.`);
|
|
957
|
+
}
|
|
958
|
+
return doc;
|
|
959
|
+
};
|
|
960
|
+
const applySchemaDelta = async (doc, schemaDelta, classObj, newSchema) => {
|
|
961
|
+
const fnLogger = createLogger().child({ method: "applySchemaDelta" });
|
|
962
|
+
let updatedDoc = Object.assign({}, doc);
|
|
963
|
+
// Every entry applies - the delta names one attribute per key, and a schema
|
|
964
|
+
// change routinely touches several. Returning after the first one is how a
|
|
965
|
+
// two-attribute patch used to stamp only whichever came first (ADR-0036).
|
|
966
|
+
for (const [name, delta] of Object.entries(schemaDelta)) {
|
|
967
|
+
fnLogger.debug(`Delta of attribute '${name}'`);
|
|
968
|
+
if (Array.isArray(delta)) {
|
|
969
|
+
// jsondiffpatch's array shapes: [new] is an addition, [old, new] a
|
|
970
|
+
// wholesale replacement, [old, 0, 0] a removal.
|
|
971
|
+
if (delta.length === 1) {
|
|
972
|
+
updatedDoc = await attributeEffect("add", delta[0], classObj, updatedDoc);
|
|
973
|
+
}
|
|
974
|
+
else if (delta.length === 2) {
|
|
975
|
+
// The *new* model: validating against the one on its way out
|
|
976
|
+
// would pin every document to the definition being replaced.
|
|
977
|
+
updatedDoc = await attributeEffect("change", delta[1], classObj, updatedDoc);
|
|
978
|
+
}
|
|
979
|
+
else if (delta.length === 3) {
|
|
980
|
+
updatedDoc = await attributeEffect("delete", delta[0], classObj, updatedDoc);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
// A nested delta: jsondiffpatch recurses into object values, so an
|
|
985
|
+
// attribute model edited in place (one config flag, a description)
|
|
986
|
+
// lands here rather than in the [old, new] branch above - this is the
|
|
987
|
+
// ordinary shape of an edit, not an exotic one. The delta carries
|
|
988
|
+
// only the changed fragment, so the full model to validate against
|
|
989
|
+
// comes from the schema being written.
|
|
990
|
+
const attrModel = newSchema === null || newSchema === void 0 ? void 0 : newSchema[name];
|
|
991
|
+
if (attrModel) {
|
|
992
|
+
updatedDoc = await attributeEffect("change", attrModel, classObj, updatedDoc);
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
fnLogger.warn(`Unhandled delta shape for attribute '${name}': no new model to validate against`, { delta });
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
return updatedDoc;
|
|
1000
|
+
};
|
|
1001
|
+
|
|
928
1002
|
/**
|
|
929
1003
|
* The Trigger class dynamically hydrates a function from a string.
|
|
930
1004
|
* This allows for declarative, data-driven logic to be executed at runtime.
|
|
@@ -3564,10 +3638,12 @@ const sys_016 = {
|
|
|
3564
3638
|
"name": "class",
|
|
3565
3639
|
"description": "A class document representing a data model class",
|
|
3566
3640
|
"~class": "~self",
|
|
3567
|
-
// Patch hydration
|
|
3568
|
-
//
|
|
3569
|
-
//
|
|
3570
|
-
//
|
|
3641
|
+
// Patch hydration merges `schema` attribute by attribute (ADR-0038): an
|
|
3642
|
+
// entry here overlays the stored one, an absent attribute stays as stored,
|
|
3643
|
+
// and an explicit `null` drops it. The restatement of `ephemeral`/`simple`
|
|
3644
|
+
// below predates the merge - hydration used to replace the whole schema, so
|
|
3645
|
+
// a patch had to carry the full set - and is kept: under merge it lands on
|
|
3646
|
+
// identical definitions and changes nothing.
|
|
3571
3647
|
"schema": {
|
|
3572
3648
|
"ephemeral": {
|
|
3573
3649
|
"name": "ephemeral",
|
|
@@ -3602,7 +3678,32 @@ const sys_016 = {
|
|
|
3602
3678
|
}
|
|
3603
3679
|
]
|
|
3604
3680
|
};
|
|
3605
|
-
|
|
3681
|
+
/**
|
|
3682
|
+
* `~JobRun.jobId` becomes optional (ADR-0044): a patch's one-shot job leaves its
|
|
3683
|
+
* receipt as a `~JobRun`, but patch jobs are deliberately never persisted as
|
|
3684
|
+
* `~Job` documents, so their receipts have no row to point at - they omit
|
|
3685
|
+
* `jobId` and carry the patch identity in `runtimeArgs` instead. A scheduler run
|
|
3686
|
+
* still writes its `jobId`; the foreign key still validates whenever present.
|
|
3687
|
+
* An ADR-0038 merge fragment: one attribute restated, everything else untouched.
|
|
3688
|
+
*/
|
|
3689
|
+
const sys_017 = {
|
|
3690
|
+
"_id": "~sys-0.0.17",
|
|
3691
|
+
"~class": "patch",
|
|
3692
|
+
"version": "0.0.17",
|
|
3693
|
+
"target": "system",
|
|
3694
|
+
"changelog": "### Schema Patch: v0.0.17\\n#### ~JobRun.jobId optional (patch job receipts, ADR-0044)",
|
|
3695
|
+
"docs": [
|
|
3696
|
+
{
|
|
3697
|
+
"_id": "~JobRun",
|
|
3698
|
+
"_rev": "auto",
|
|
3699
|
+
"~class": "class",
|
|
3700
|
+
"schema": {
|
|
3701
|
+
"jobId": { "name": "jobId", "type": "foreign_key", "config": { "mandatory": false, "targetClass": "~Job" } }
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
]
|
|
3705
|
+
};
|
|
3706
|
+
syspatches.push(sys_011, sys_012, sys_013, sys_014, sys_015, sys_016, sys_017);
|
|
3606
3707
|
/**
|
|
3607
3708
|
* Every document id the system patches seed.
|
|
3608
3709
|
*
|
|
@@ -3642,64 +3743,7 @@ async function getSystemPatches(currentVersion) {
|
|
|
3642
3743
|
.map((patch) => JSON.parse(JSON.stringify(patch)));
|
|
3643
3744
|
}
|
|
3644
3745
|
|
|
3645
|
-
const
|
|
3646
|
-
const attribute = new Attribute(null, model.name, model.type, model.description, model.config);
|
|
3647
|
-
if (operation === "add") {
|
|
3648
|
-
doc = Object.assign(Object.assign({}, doc), attribute.getEmpty());
|
|
3649
|
-
}
|
|
3650
|
-
if (operation === "delete") {
|
|
3651
|
-
delete doc[attribute.name];
|
|
3652
|
-
}
|
|
3653
|
-
else { // when addition or change also perform validation
|
|
3654
|
-
const res = await attribute.validate(doc[attribute.name]);
|
|
3655
|
-
// `safeParseAsync` resolves to a result object, which is truthy whether or not the
|
|
3656
|
-
// value is valid: the outcome has to be read off `success`, otherwise every invalid
|
|
3657
|
-
// value passes straight through.
|
|
3658
|
-
if (!res.success) {
|
|
3659
|
-
throw new Error(`Attribute '${classObj.name}.${attribute.name}' ${operation} fails for current document because of its validation: ${z$1.prettifyError(res.error)}`);
|
|
3660
|
-
}
|
|
3661
|
-
}
|
|
3662
|
-
if (attribute.isPrimaryKey()) {
|
|
3663
|
-
const result = classObj.bulkUniqueCheck(classObj.getPrimaryKeys());
|
|
3664
|
-
if (!result) {
|
|
3665
|
-
throw new Error(`With attribute '${attribute.name}' ${operation} of class '${classObj.name}', docs fail primary keys check.`);
|
|
3666
|
-
}
|
|
3667
|
-
}
|
|
3668
|
-
if (operation !== "delete" && attribute.isMandatory() && doc[attribute.name] == undefined) {
|
|
3669
|
-
throw new Error(`Attribute '${classObj.name}.${attribute.name}' is mandatory but has no value.`);
|
|
3670
|
-
}
|
|
3671
|
-
return doc;
|
|
3672
|
-
};
|
|
3673
|
-
const applySchemaDelta = async (doc, schemaDelta, classObj) => {
|
|
3674
|
-
const fnLogger = createLogger().child({ method: "applySchemaDelta" });
|
|
3675
|
-
let updatedDoc = Object.assign({}, doc);
|
|
3676
|
-
const t = Object.entries(schemaDelta);
|
|
3677
|
-
for (const e of t) {
|
|
3678
|
-
fnLogger.debug(`Delta of attribute '${e[0]}'`);
|
|
3679
|
-
if (Array.isArray(e[1])) {
|
|
3680
|
-
// e[1] can represent an addition, a deletion or an edit
|
|
3681
|
-
// it's an addition when the array has only one element
|
|
3682
|
-
if (e[1].length === 1) {
|
|
3683
|
-
const attrModel = e[1][0];
|
|
3684
|
-
updatedDoc = await attributeEffect("add", attrModel, classObj, updatedDoc);
|
|
3685
|
-
}
|
|
3686
|
-
// it's an edit when it has 2 elements
|
|
3687
|
-
if (e[1].length === 1) {
|
|
3688
|
-
const attrModel = e[1][0];
|
|
3689
|
-
updatedDoc = await attributeEffect("change", attrModel, classObj, updatedDoc);
|
|
3690
|
-
}
|
|
3691
|
-
// it's a removal when it has 3 elements
|
|
3692
|
-
if (e[1].length === 3) {
|
|
3693
|
-
const attrModel = e[1][0];
|
|
3694
|
-
updatedDoc = await attributeEffect("delete", attrModel, classObj, updatedDoc);
|
|
3695
|
-
}
|
|
3696
|
-
return updatedDoc;
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
return updatedDoc;
|
|
3700
|
-
};
|
|
3701
|
-
|
|
3702
|
-
const logger$3 = createLogger().child({ module: "pouchdb" });
|
|
3746
|
+
const logger$4 = createLogger().child({ module: "pouchdb" });
|
|
3703
3747
|
/**
|
|
3704
3748
|
* Raised when a locked stack is asked to write a class carrying encrypted attributes.
|
|
3705
3749
|
*
|
|
@@ -3768,7 +3812,7 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
3768
3812
|
return Promise.resolve("pong");
|
|
3769
3813
|
},
|
|
3770
3814
|
bulkDocs: async function (docs, options, callback) {
|
|
3771
|
-
const fnLogger = logger$
|
|
3815
|
+
const fnLogger = logger$4.child({ method: "bulkDocs" });
|
|
3772
3816
|
if (typeof options == 'function') {
|
|
3773
3817
|
callback = options;
|
|
3774
3818
|
options = {};
|
|
@@ -3831,10 +3875,7 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
3831
3875
|
const triggerQueue = Object.create(null);
|
|
3832
3876
|
const classCache = new Map();
|
|
3833
3877
|
const postOperations = async (error, result) => {
|
|
3834
|
-
if (
|
|
3835
|
-
return { error, result };
|
|
3836
|
-
}
|
|
3837
|
-
else if (result) {
|
|
3878
|
+
if (result) {
|
|
3838
3879
|
const docs = [];
|
|
3839
3880
|
for (const docRes of result) {
|
|
3840
3881
|
if (docRes.id) {
|
|
@@ -3927,25 +3968,22 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
3927
3968
|
// When a class document is updated, its change must have an effect on its children
|
|
3928
3969
|
const classDocId = doc._id;
|
|
3929
3970
|
const className = typeof doc.name === "string" ? doc.name : classDocId;
|
|
3971
|
+
// Fetch the version this write replaces. Its *absence* is what
|
|
3972
|
+
// "just created" means - a revision count is not: a stored class
|
|
3973
|
+
// at rev 1 receiving its first schema change already has documents
|
|
3974
|
+
// to propagate to, and the old `_revisions.ids.length == 1` check
|
|
3975
|
+
// skipped exactly that write (ADR-0038).
|
|
3976
|
+
let previousClassDoc;
|
|
3930
3977
|
try {
|
|
3931
|
-
|
|
3932
|
-
const docWithRevs = await stack.db.get(classDocId, { revs: true });
|
|
3933
|
-
const revisionIDList = docWithRevs._revisions.ids;
|
|
3934
|
-
if (revisionIDList.length == 1) {
|
|
3935
|
-
fnLogger.info(`Class '${className}' (doc '${classDocId}') was just created. Nothing to do.`);
|
|
3936
|
-
continue;
|
|
3937
|
-
return pouchBulkDocs.call(this, docs, options, postExec);
|
|
3938
|
-
}
|
|
3978
|
+
previousClassDoc = await stack.db.get(classDocId);
|
|
3939
3979
|
}
|
|
3940
3980
|
catch (e) {
|
|
3941
3981
|
if (e.name === 'not_found') {
|
|
3942
3982
|
fnLogger.info(`Class '${className}' (doc '${classDocId}') was just created. Nothing to do.`);
|
|
3943
3983
|
continue;
|
|
3944
|
-
return pouchBulkDocs.call(this, docs, options, postExec);
|
|
3945
3984
|
}
|
|
3985
|
+
throw e;
|
|
3946
3986
|
}
|
|
3947
|
-
// Fetch the current (next old) version of the class document.
|
|
3948
|
-
const previousClassDoc = await stack.db.get(classDocId);
|
|
3949
3987
|
// Built rather than looked up: this is the class as it was *before*
|
|
3950
3988
|
// this write, which the cache does not hold. Detached, because all it
|
|
3951
3989
|
// is used for is diffing and applying the delta.
|
|
@@ -3961,16 +3999,54 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
3961
3999
|
fnLogger.info(`Class '${className}' has no changes on schema.`);
|
|
3962
4000
|
continue;
|
|
3963
4001
|
}
|
|
3964
|
-
|
|
4002
|
+
// System-level read, not getCards: propagation must see every
|
|
4003
|
+
// document of the class - a policy-filtered read would rewrite
|
|
4004
|
+
// only the session's subset - and it runs during create()'s
|
|
4005
|
+
// patch application, before any session exists (ADR-0044).
|
|
4006
|
+
// Encrypted attributes are opened explicitly when a key is
|
|
4007
|
+
// present; a keyless encrypting class never reaches here, its
|
|
4008
|
+
// patch deferred upstream (ADR-0040).
|
|
4009
|
+
const rawFetch = await stack.db.find({
|
|
4010
|
+
selector: { "~class": className, active: true },
|
|
4011
|
+
limit: 2 ** 31 - 1,
|
|
4012
|
+
});
|
|
4013
|
+
const fetched = rawFetch.docs;
|
|
4014
|
+
if (stack.cryptoEngine.isEnabled() && stack.cryptoEngine.getDocumentKey()
|
|
4015
|
+
&& classObj.getEncryptedAttributes().length) {
|
|
4016
|
+
for (const stored of fetched) {
|
|
4017
|
+
await stack.cryptoEngine.decryptDocument(stored, classObj);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
// A committed document superseded by a batch-mate belongs to the
|
|
4021
|
+
// batch, not to propagation (ADR-0044): its batch version - a
|
|
4022
|
+
// pre-apply job's massage - is validated by the document branch
|
|
4023
|
+
// against the batch model, and judging or stamping the stale
|
|
4024
|
+
// committed copy here would refuse (or undo) exactly what the
|
|
4025
|
+
// massage fixed. A delegation, not a skip.
|
|
4026
|
+
const superseded = new Set(documentsToProcess.map((mate) => mate === null || mate === void 0 ? void 0 : mate._id).filter((id) => typeof id === "string"));
|
|
4027
|
+
const documents = fetched.filter(existing => !superseded.has(existing._id));
|
|
3965
4028
|
if (documents.length === 0) {
|
|
3966
4029
|
fnLogger.info(`No documents found for class '${className}' after its update.`);
|
|
3967
4030
|
continue;
|
|
3968
4031
|
}
|
|
4032
|
+
// The schema being written, captured before the map shadows `doc`:
|
|
4033
|
+
// an attribute edited in place arrives as a nested delta carrying
|
|
4034
|
+
// only the changed fragment, and the full new model to validate
|
|
4035
|
+
// against has to come from here.
|
|
4036
|
+
const nextSchema = doc.schema;
|
|
3969
4037
|
const updates = await Promise.all(documents.map(async (doc) => {
|
|
3970
|
-
const updatedDoc = await applySchemaDelta(doc, schemaDelta, classObj);
|
|
4038
|
+
const updatedDoc = await applySchemaDelta(doc, schemaDelta, classObj, nextSchema);
|
|
3971
4039
|
return updatedDoc;
|
|
3972
4040
|
}));
|
|
3973
|
-
|
|
4041
|
+
// PouchDB reports per-document failures in the *resolved* array,
|
|
4042
|
+
// not by rejecting - awaiting alone would let a conflicted update
|
|
4043
|
+
// silently drop one document's propagation.
|
|
4044
|
+
const propagated = await stack.db.bulkDocs(updates);
|
|
4045
|
+
const failures = propagated.filter(entry => entry && entry.error);
|
|
4046
|
+
if (failures.length) {
|
|
4047
|
+
throw new Error(`Schema propagation failed for ${failures.length} document(s) of class '${className}': ` +
|
|
4048
|
+
failures.map((f) => { var _a; return `${f.id}: ${(_a = f.message) !== null && _a !== void 0 ? _a : f.name}`; }).join("; "));
|
|
4049
|
+
}
|
|
3974
4050
|
fnLogger.info('Propagated updates');
|
|
3975
4051
|
}
|
|
3976
4052
|
else if (isRelation(doc)) {
|
|
@@ -3981,9 +4057,20 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
3981
4057
|
if (doc.sourceClass !== domain.sourceClass.id || doc.targetClass !== domain.targetClass.id) {
|
|
3982
4058
|
throw new Error(`Relation document classes do not match domain '${domain.name}'.`);
|
|
3983
4059
|
}
|
|
4060
|
+
// An endpoint may be a batch-mate rather than stored: a relation
|
|
4061
|
+
// written together with its documents in one bulkDocs - which is
|
|
4062
|
+
// exactly what a transaction commit is (ADR-0039) - checks the
|
|
4063
|
+
// batch before declaring the endpoint missing.
|
|
4064
|
+
const resolveEndpoint = async (endpointId) => {
|
|
4065
|
+
var _a;
|
|
4066
|
+
const stored = await stack.db.get(endpointId).catch(() => null);
|
|
4067
|
+
if (stored)
|
|
4068
|
+
return stored;
|
|
4069
|
+
return (_a = documentsToProcess.find((mate) => (mate === null || mate === void 0 ? void 0 : mate._id) === endpointId && !(mate === null || mate === void 0 ? void 0 : mate._deleted))) !== null && _a !== void 0 ? _a : null;
|
|
4070
|
+
};
|
|
3984
4071
|
const [sourceDoc, targetDoc] = await Promise.all([
|
|
3985
|
-
|
|
3986
|
-
|
|
4072
|
+
resolveEndpoint(doc.sourceId),
|
|
4073
|
+
resolveEndpoint(doc.targetId),
|
|
3987
4074
|
]);
|
|
3988
4075
|
if (!sourceDoc) {
|
|
3989
4076
|
throw new Error(`Source document '${doc.sourceId}' does not exist for domain '${domain.name}'.`);
|
|
@@ -4009,8 +4096,29 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
4009
4096
|
continue;
|
|
4010
4097
|
try {
|
|
4011
4098
|
let classObj;
|
|
4099
|
+
// The newest statement of a schema may ride this same batch
|
|
4100
|
+
// (ADR-0043): a patch introduces a class and seeds its first
|
|
4101
|
+
// document in one docs array. The batch outranks the store even
|
|
4102
|
+
// for a class that already exists - it is what is about to be
|
|
4103
|
+
// committed - and the search sees the whole array, so ordering
|
|
4104
|
+
// within the batch does not matter. The relation branch above
|
|
4105
|
+
// has made the same decision for its endpoints since ADR-0039.
|
|
4106
|
+
const batchModel = documentsToProcess.find((mate) => isClassModel(mate) && !mate._deleted
|
|
4107
|
+
&& (mate.name === className || mate._id === className));
|
|
4012
4108
|
try {
|
|
4013
|
-
|
|
4109
|
+
// Built DETACHED - `Class.get` + `setModel`, the same path
|
|
4110
|
+
// `buildFromModel` takes for a stored doc. `buildFromModel`
|
|
4111
|
+
// itself would route a rev-less model through `Class.create`,
|
|
4112
|
+
// which writes the class doc and makes the batch's own
|
|
4113
|
+
// insert conflict with it.
|
|
4114
|
+
let fromBatch = null;
|
|
4115
|
+
if (batchModel && !classCache.has(className)) {
|
|
4116
|
+
fromBatch = Class.get(stack, batchModel._id, batchModel.name, batchModel["~class"], batchModel.description, batchModel.schema, { subscribe: false });
|
|
4117
|
+
fromBatch.setModel(batchModel);
|
|
4118
|
+
}
|
|
4119
|
+
classObj = classCache.get(className)
|
|
4120
|
+
|| fromBatch
|
|
4121
|
+
|| await stack.getClassSnapshot(className);
|
|
4014
4122
|
}
|
|
4015
4123
|
catch (error) {
|
|
4016
4124
|
throw new Error(`Class '${className}' not found for document '${doc._id}'.`);
|
|
@@ -4205,6 +4313,19 @@ const StackPlugin = (pouch, stack, pristine) => {
|
|
|
4205
4313
|
const exec = async () => {
|
|
4206
4314
|
var _a, _b;
|
|
4207
4315
|
const result = await pouchGet.call(this, docId, options !== null && options !== void 0 ? options : {});
|
|
4316
|
+
// A revision-addressed read serves the STORED form, never plaintext.
|
|
4317
|
+
// This is not an optimization but the seal on the replication surface
|
|
4318
|
+
// (ADR-0040): PouchDB hard-binds its instance methods at construction,
|
|
4319
|
+
// so the pristine `bulkGet` the sync layer uses cannot be pointed away
|
|
4320
|
+
// from this instance - its shim, and `get`'s own `open_revs` branch,
|
|
4321
|
+
// re-enter THIS override per revision (`{rev}` / `{open_revs}`).
|
|
4322
|
+
// Decrypting those hops pushed every encrypted attribute to remotes in
|
|
4323
|
+
// plaintext under the local revision id. Taxonomically it is the same
|
|
4324
|
+
// line ADR-0020 draws: naming a revision is a replication/forensic
|
|
4325
|
+
// read; the winning-revision read below is the one that decrypts.
|
|
4326
|
+
if ((options === null || options === void 0 ? void 0 : options.rev) || (options === null || options === void 0 ? void 0 : options.open_revs)) {
|
|
4327
|
+
return result;
|
|
4328
|
+
}
|
|
4208
4329
|
// Optional-chained on purpose: this override serves `initialize` itself
|
|
4209
4330
|
// (`checkSystem` reads `~system` through it), which runs before the
|
|
4210
4331
|
// crypto engine is constructed. And gated on the *key*, not just the
|
|
@@ -4417,6 +4538,13 @@ const createGuardedDb = (db) => {
|
|
|
4417
4538
|
* @internal
|
|
4418
4539
|
*/
|
|
4419
4540
|
const createReplicationDb = (db, pristine) => {
|
|
4541
|
+
// Note the limit of what this handle can restore: PouchDB hard-binds instance
|
|
4542
|
+
// methods at construction (`this.bulkGet = adapterFun(...).bind(this)`), so the
|
|
4543
|
+
// pristine functions run against the raw instance no matter what they are bound
|
|
4544
|
+
// to here - and pouchdb-core's `bulkGet`/`open_revs` internals re-enter
|
|
4545
|
+
// `this.get`, which is the plugin's override. The override itself is therefore
|
|
4546
|
+
// the one place that can keep those internal hops raw: it serves the stored form
|
|
4547
|
+
// for any revision-addressed read (ADR-0040).
|
|
4420
4548
|
const bulkDocs = pristine.bulkDocs.bind(db);
|
|
4421
4549
|
const bulkGet = pristine.bulkGet.bind(db);
|
|
4422
4550
|
// `get` restored for the same reason as `bulkGet`: since ADR-0032 the plugin
|
|
@@ -4932,7 +5060,7 @@ const deriveTenantScope = async (stacks, entitlement) => {
|
|
|
4932
5060
|
return scope;
|
|
4933
5061
|
};
|
|
4934
5062
|
|
|
4935
|
-
const logger$
|
|
5063
|
+
const logger$3 = createLogger().child({ module: "sync" });
|
|
4936
5064
|
/**
|
|
4937
5065
|
* Raised when a remote is ahead of this device's data model.
|
|
4938
5066
|
*
|
|
@@ -4942,14 +5070,18 @@ const logger$2 = createLogger().child({ module: "sync" });
|
|
|
4942
5070
|
* tells the application to ship the newer build.
|
|
4943
5071
|
*/
|
|
4944
5072
|
class SyncSchemaMismatchError extends Error {
|
|
4945
|
-
constructor(stack, localVersion, remoteVersion) {
|
|
4946
|
-
super(`Stack '${stack}' cannot sync: the remote was last written with
|
|
4947
|
-
`${
|
|
4948
|
-
|
|
5073
|
+
constructor(stack, localVersion, remoteVersion, scope = "system") {
|
|
5074
|
+
super(`Stack '${stack}' cannot sync: the remote was last written with ` +
|
|
5075
|
+
`${scope === "consumer" ? "consumer patch" : "schema"} version ` +
|
|
5076
|
+
`${remoteVersion}, this device has ${localVersion || "none"}. ` +
|
|
5077
|
+
(scope === "consumer"
|
|
5078
|
+
? `Apply the application's patches up to ${remoteVersion} - a deferred patch applies on unlock - before syncing again.`
|
|
5079
|
+
: `Update the application so its patches reach ${remoteVersion} before syncing again.`));
|
|
4949
5080
|
this.name = "SyncSchemaMismatchError";
|
|
4950
5081
|
this.stack = stack;
|
|
4951
5082
|
this.localVersion = localVersion;
|
|
4952
5083
|
this.remoteVersion = remoteVersion;
|
|
5084
|
+
this.scope = scope;
|
|
4953
5085
|
}
|
|
4954
5086
|
}
|
|
4955
5087
|
/**
|
|
@@ -4985,6 +5117,20 @@ const readRemoteSchemaVersion = async (remote) => {
|
|
|
4985
5117
|
});
|
|
4986
5118
|
return (system === null || system === void 0 ? void 0 : system.schemaVersion) || null;
|
|
4987
5119
|
};
|
|
5120
|
+
/**
|
|
5121
|
+
* Reads the highest consumer patch version recorded on a remote.
|
|
5122
|
+
*
|
|
5123
|
+
* `null` for a remote nobody has written, or one written only by builds that
|
|
5124
|
+
* predate the consumer half of the gate.
|
|
5125
|
+
*/
|
|
5126
|
+
const readRemoteConsumerSchemaVersion = async (remote) => {
|
|
5127
|
+
const meta = await remote.get(SYNC_META_DOC_ID).catch((error) => {
|
|
5128
|
+
if (isMissing(error))
|
|
5129
|
+
return null;
|
|
5130
|
+
throw error;
|
|
5131
|
+
});
|
|
5132
|
+
return (meta === null || meta === void 0 ? void 0 : meta.consumerSchemaVersion) || null;
|
|
5133
|
+
};
|
|
4988
5134
|
/**
|
|
4989
5135
|
* Records this device's schema version on a remote, if it is the newest seen.
|
|
4990
5136
|
*
|
|
@@ -4992,20 +5138,27 @@ const readRemoteSchemaVersion = async (remote) => {
|
|
|
4992
5138
|
* @param schemaVersion - The local schema version; a missing value writes nothing.
|
|
4993
5139
|
* @param appVersion - The local application version, stored for diagnostics.
|
|
4994
5140
|
*/
|
|
4995
|
-
const publishSchemaVersion = async (remote, schemaVersion, appVersion) => {
|
|
4996
|
-
if (!schemaVersion)
|
|
5141
|
+
const publishSchemaVersion = async (remote, schemaVersion, appVersion, consumerSchemaVersion) => {
|
|
5142
|
+
if (!schemaVersion && !consumerSchemaVersion)
|
|
4997
5143
|
return;
|
|
4998
5144
|
const existing = await remote.get(SYNC_META_DOC_ID).catch((error) => {
|
|
4999
5145
|
if (isMissing(error))
|
|
5000
5146
|
return null;
|
|
5001
5147
|
throw error;
|
|
5002
5148
|
});
|
|
5003
|
-
|
|
5004
|
-
|
|
5149
|
+
// Each version field is monotonic on its own: the marker records the highest
|
|
5150
|
+
// either kind has ever reached, whichever device wrote it.
|
|
5151
|
+
const advances = (candidate, recorded) => Boolean(candidate) && (!recorded
|
|
5152
|
+
|| !semver.valid(recorded) || !semver.valid(candidate)
|
|
5153
|
+
|| semver.gt(candidate, recorded));
|
|
5154
|
+
const systemAdvances = advances(schemaVersion, existing === null || existing === void 0 ? void 0 : existing.schemaVersion);
|
|
5155
|
+
const consumerAdvances = advances(consumerSchemaVersion, existing === null || existing === void 0 ? void 0 : existing.consumerSchemaVersion);
|
|
5156
|
+
if (!systemAdvances && !consumerAdvances) {
|
|
5005
5157
|
return;
|
|
5006
5158
|
}
|
|
5007
|
-
const doc = Object.assign(Object.assign({}, (existing || {})), { _id: SYNC_META_DOC_ID, schemaVersion,
|
|
5008
|
-
|
|
5159
|
+
const doc = Object.assign(Object.assign(Object.assign(Object.assign({}, (existing || {})), { _id: SYNC_META_DOC_ID, schemaVersion: systemAdvances ? schemaVersion : existing === null || existing === void 0 ? void 0 : existing.schemaVersion }), (consumerAdvances
|
|
5160
|
+
? { consumerSchemaVersion: consumerSchemaVersion }
|
|
5161
|
+
: ((existing === null || existing === void 0 ? void 0 : existing.consumerSchemaVersion) ? { consumerSchemaVersion: existing.consumerSchemaVersion } : {}))), { appVersion, updatedAt: Date.now() });
|
|
5009
5162
|
await remote.put(doc).catch((error) => {
|
|
5010
5163
|
// Another device claimed the same version between the read and the write; its
|
|
5011
5164
|
// document says the same thing this one would have.
|
|
@@ -5097,7 +5250,7 @@ class StackSyncHandle extends EventTarget {
|
|
|
5097
5250
|
* @throws {SyncSchemaMismatchError} When the remote is ahead of this device.
|
|
5098
5251
|
*/
|
|
5099
5252
|
async start() {
|
|
5100
|
-
const fnLogger = logger$
|
|
5253
|
+
const fnLogger = logger$3.child({ method: "start", stack: this.stack.name });
|
|
5101
5254
|
this.cancelled = false;
|
|
5102
5255
|
this.setState("starting");
|
|
5103
5256
|
try {
|
|
@@ -5136,7 +5289,7 @@ class StackSyncHandle extends EventTarget {
|
|
|
5136
5289
|
this.replication.cancel();
|
|
5137
5290
|
}
|
|
5138
5291
|
catch (error) {
|
|
5139
|
-
logger$
|
|
5292
|
+
logger$3.warn("Error while cancelling replication", { error, stack: this.stack.name });
|
|
5140
5293
|
}
|
|
5141
5294
|
if (typeof this.replication.removeAllListeners === "function") {
|
|
5142
5295
|
this.replication.removeAllListeners();
|
|
@@ -5220,11 +5373,27 @@ class StackSyncHandle extends EventTarget {
|
|
|
5220
5373
|
throw new SyncSchemaMismatchError(this.stack.name, localVersion, remoteVersion);
|
|
5221
5374
|
}
|
|
5222
5375
|
}
|
|
5376
|
+
// The consumer half of the gate: the system version cannot see consumer-patch
|
|
5377
|
+
// skew - two devices on the same build always agree on it - so a device whose
|
|
5378
|
+
// application patches trail the remote (deferred behind the document key, or
|
|
5379
|
+
// an older build) would pull documents shaped by a schema it does not have,
|
|
5380
|
+
// and the deferred replay would then propagate over them (ADR-0040). The
|
|
5381
|
+
// ledger answers locally; a remote written only by older builds records
|
|
5382
|
+
// nothing and gates nothing.
|
|
5383
|
+
const localConsumer = await this.stack.getConsumerSchemaVersion();
|
|
5384
|
+
const remoteConsumer = await readRemoteConsumerSchemaVersion(remote);
|
|
5385
|
+
if (remoteConsumer && semver.valid(remoteConsumer)) {
|
|
5386
|
+
const ahead = !localConsumer
|
|
5387
|
+
|| (Boolean(semver.valid(localConsumer)) && semver.gt(remoteConsumer, localConsumer));
|
|
5388
|
+
if (ahead) {
|
|
5389
|
+
throw new SyncSchemaMismatchError(this.stack.name, localConsumer !== null && localConsumer !== void 0 ? localConsumer : undefined, remoteConsumer, "consumer");
|
|
5390
|
+
}
|
|
5391
|
+
}
|
|
5223
5392
|
// Only a device that writes to the remote gets to claim its schema version.
|
|
5224
5393
|
// A pull-only device publishing would lock older peers out of a remote that
|
|
5225
5394
|
// holds nothing they cannot read.
|
|
5226
5395
|
if (this.direction !== "pull") {
|
|
5227
|
-
await publishSchemaVersion(remote, localVersion, this.stack.appVersion);
|
|
5396
|
+
await publishSchemaVersion(remote, localVersion, this.stack.appVersion, localConsumer);
|
|
5228
5397
|
}
|
|
5229
5398
|
}
|
|
5230
5399
|
seededDocIdsFromPatches() {
|
|
@@ -7758,7 +7927,7 @@ const calculateHash = async (content) => {
|
|
|
7758
7927
|
const buffer = await crypto.subtle.digest('SHA-256', data);
|
|
7759
7928
|
return Array.from(new Uint8Array(buffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
7760
7929
|
};
|
|
7761
|
-
const now = () => Date.now();
|
|
7930
|
+
const now$1 = () => Date.now();
|
|
7762
7931
|
/**
|
|
7763
7932
|
* Represents an executable job that can perform background tasks.
|
|
7764
7933
|
*
|
|
@@ -7829,7 +7998,7 @@ class Job {
|
|
|
7829
7998
|
jobId: this.model._id,
|
|
7830
7999
|
status: "PENDING",
|
|
7831
8000
|
triggerType,
|
|
7832
|
-
startTime: now(),
|
|
8001
|
+
startTime: now$1(),
|
|
7833
8002
|
runtimeArgs,
|
|
7834
8003
|
initialMetadata: this.model.metadata ? Object.assign({}, this.model.metadata) : undefined,
|
|
7835
8004
|
};
|
|
@@ -7873,7 +8042,7 @@ class Job {
|
|
|
7873
8042
|
if (!this.model.isEnabled) {
|
|
7874
8043
|
run.status = "SKIPPED";
|
|
7875
8044
|
run.errorMessage = `Job ${this.model._id} is disabled`;
|
|
7876
|
-
run.endTime = now();
|
|
8045
|
+
run.endTime = now$1();
|
|
7877
8046
|
run.durationMs = run.endTime - run.startTime;
|
|
7878
8047
|
await this.persistRun(run);
|
|
7879
8048
|
throw new Error(run.errorMessage);
|
|
@@ -7881,14 +8050,14 @@ class Job {
|
|
|
7881
8050
|
if (await this.hasRunningInstance()) {
|
|
7882
8051
|
run.status = "SKIPPED";
|
|
7883
8052
|
run.errorMessage = `Job ${this.model._id} already has a running instance`;
|
|
7884
|
-
run.endTime = now();
|
|
8053
|
+
run.endTime = now$1();
|
|
7885
8054
|
run.durationMs = run.endTime - run.startTime;
|
|
7886
8055
|
await this.persistRun(run);
|
|
7887
8056
|
throw new Error(run.errorMessage);
|
|
7888
8057
|
}
|
|
7889
8058
|
try {
|
|
7890
8059
|
run.status = "RUNNING";
|
|
7891
|
-
run.startTime = now();
|
|
8060
|
+
run.startTime = now$1();
|
|
7892
8061
|
run = await this.persistRun(run);
|
|
7893
8062
|
const executor = this.hydrate();
|
|
7894
8063
|
const params = Object.assign(Object.assign({}, (this.model.defaultParams || {})), (runtimeArgs || {}));
|
|
@@ -7900,7 +8069,7 @@ class Job {
|
|
|
7900
8069
|
await this.persistJobMetadata(finalMetadata);
|
|
7901
8070
|
}
|
|
7902
8071
|
run.status = "SUCCESS";
|
|
7903
|
-
run.endTime = now();
|
|
8072
|
+
run.endTime = now$1();
|
|
7904
8073
|
run.durationMs = run.endTime - run.startTime;
|
|
7905
8074
|
return await this.persistRun(run);
|
|
7906
8075
|
}
|
|
@@ -7908,7 +8077,7 @@ class Job {
|
|
|
7908
8077
|
run.status = "FAILURE";
|
|
7909
8078
|
run.errorMessage = (error === null || error === void 0 ? void 0 : error.message) || String(error);
|
|
7910
8079
|
run.errorStack = error === null || error === void 0 ? void 0 : error.stack;
|
|
7911
|
-
run.endTime = now();
|
|
8080
|
+
run.endTime = now$1();
|
|
7912
8081
|
run.durationMs = run.endTime - run.startTime;
|
|
7913
8082
|
return await this.persistRun(run);
|
|
7914
8083
|
}
|
|
@@ -8171,7 +8340,7 @@ const isImplausible = (nextRunAt, schedule, now) => nextRunAt - now > periodCeil
|
|
|
8171
8340
|
*
|
|
8172
8341
|
* @module
|
|
8173
8342
|
*/
|
|
8174
|
-
const logger$
|
|
8343
|
+
const logger$2 = createLogger().child({ module: "job-scheduler" });
|
|
8175
8344
|
/** The `_local/` document holding this device's schedule state. Never replicates. */
|
|
8176
8345
|
const JOB_SCHEDULE_DOC_ID = "_local/docstack-job-schedule";
|
|
8177
8346
|
const DEFAULTS = {
|
|
@@ -8321,7 +8490,7 @@ class JobScheduler {
|
|
|
8321
8490
|
if (pinned && pinned !== job.hash) {
|
|
8322
8491
|
// Fail closed. The content behind an unexpected hash is exactly the case
|
|
8323
8492
|
// the allow-list exists for.
|
|
8324
|
-
logger$
|
|
8493
|
+
logger$2.warn("tick - job hash does not match the pinned value; not running", { jobId });
|
|
8325
8494
|
report.skipped.push({ jobId, reason: "hash-mismatch" });
|
|
8326
8495
|
continue;
|
|
8327
8496
|
}
|
|
@@ -8331,7 +8500,7 @@ class JobScheduler {
|
|
|
8331
8500
|
}
|
|
8332
8501
|
const schedule = parseSchedule(job.schedule);
|
|
8333
8502
|
if (!schedule) {
|
|
8334
|
-
logger$
|
|
8503
|
+
logger$2.warn("tick - job carries a schedule this client cannot read", {
|
|
8335
8504
|
jobId,
|
|
8336
8505
|
schedule: job.schedule,
|
|
8337
8506
|
});
|
|
@@ -8354,7 +8523,7 @@ class JobScheduler {
|
|
|
8354
8523
|
continue;
|
|
8355
8524
|
}
|
|
8356
8525
|
if (isImplausible(state.nextRunAt, schedule, at)) {
|
|
8357
|
-
logger$
|
|
8526
|
+
logger$2.warn("tick - stored nextRunAt is further out than the schedule allows; recomputing", {
|
|
8358
8527
|
jobId,
|
|
8359
8528
|
nextRunAt: state.nextRunAt,
|
|
8360
8529
|
});
|
|
@@ -8402,7 +8571,7 @@ class JobScheduler {
|
|
|
8402
8571
|
// rejects if the content hash no longer matches. All three are outcomes, not
|
|
8403
8572
|
// crashes; the backoff is what keeps a permanently broken job from becoming
|
|
8404
8573
|
// a write per minute.
|
|
8405
|
-
logger$
|
|
8574
|
+
logger$2.warn("dispatch - scheduled job did not run", { jobId, error: (error === null || error === void 0 ? void 0 : error.message) || String(error) });
|
|
8406
8575
|
await this.recordOutcome(jobId, "FAILURE", options);
|
|
8407
8576
|
}
|
|
8408
8577
|
finally {
|
|
@@ -8449,11 +8618,11 @@ class JobScheduler {
|
|
|
8449
8618
|
if (!stale.length)
|
|
8450
8619
|
return 0;
|
|
8451
8620
|
await this.host.db.bulkDocs(stale.map((run) => (Object.assign(Object.assign({}, run), { status: "CANCELED", endTime: at, durationMs: at - run.startTime, errorMessage: "Run abandoned — the client stopped before it finished." }))));
|
|
8452
|
-
logger$
|
|
8621
|
+
logger$2.info("sweepAbandonedRuns - reaped abandoned runs", { count: stale.length });
|
|
8453
8622
|
return stale.length;
|
|
8454
8623
|
}
|
|
8455
8624
|
catch (error) {
|
|
8456
|
-
logger$
|
|
8625
|
+
logger$2.warn("sweepAbandonedRuns - could not sweep", { error: (error === null || error === void 0 ? void 0 : error.message) || String(error) });
|
|
8457
8626
|
return 0;
|
|
8458
8627
|
}
|
|
8459
8628
|
}
|
|
@@ -8478,7 +8647,7 @@ class JobScheduler {
|
|
|
8478
8647
|
catch (error) {
|
|
8479
8648
|
// A 409 means another tick wrote first; its state is as good as this one's,
|
|
8480
8649
|
// and the next tick reads the winner.
|
|
8481
|
-
logger$
|
|
8650
|
+
logger$2.warn("writeState - could not persist schedule state", {
|
|
8482
8651
|
error: (error === null || error === void 0 ? void 0 : error.message) || String(error),
|
|
8483
8652
|
});
|
|
8484
8653
|
}
|
|
@@ -9267,113 +9436,3189 @@ class CryptoEngine {
|
|
|
9267
9436
|
}
|
|
9268
9437
|
|
|
9269
9438
|
/**
|
|
9270
|
-
*
|
|
9271
|
-
*
|
|
9272
|
-
* `stack.dump()` is the other kind of export: every document exactly as stored, which
|
|
9273
|
-
* means class models, patches, users, sessions, policies, design documents - and
|
|
9274
|
-
* encrypted attributes as unreadable {@link EncryptedPayload} blobs. That is a debugging
|
|
9275
|
-
* tool and a backup of one database.
|
|
9276
|
-
*
|
|
9277
|
-
* This is the portable one. It carries the documents an application put in, in the clear,
|
|
9278
|
-
* so that {@link ClientStack.importContent} can place them into a *different* stack -
|
|
9279
|
-
* different device, different key, schema built by that stack's own patches.
|
|
9439
|
+
* The write journal of one transaction: authored documents keyed by id, in memory.
|
|
9280
9440
|
*
|
|
9281
|
-
*
|
|
9441
|
+
* Also partitioned by class (`~class`, or `~domain` for relations) so a read can ask
|
|
9442
|
+
* "could this stage affect a query over class X" in O(1) - the overlay only pays the
|
|
9443
|
+
* merge for queries whose class the transaction actually touched (ADR-0039).
|
|
9282
9444
|
*/
|
|
9283
|
-
|
|
9284
|
-
|
|
9445
|
+
class TransactionStage {
|
|
9446
|
+
constructor() {
|
|
9447
|
+
this.entries = new Map();
|
|
9448
|
+
this.partitions = new Map();
|
|
9449
|
+
}
|
|
9450
|
+
partitionKeys(doc) {
|
|
9451
|
+
const keys = [];
|
|
9452
|
+
const className = doc["~class"];
|
|
9453
|
+
const domainName = doc["~domain"];
|
|
9454
|
+
if (typeof className === "string")
|
|
9455
|
+
keys.push(className);
|
|
9456
|
+
if (typeof domainName === "string")
|
|
9457
|
+
keys.push(domainName);
|
|
9458
|
+
return keys;
|
|
9459
|
+
}
|
|
9460
|
+
/**
|
|
9461
|
+
* Stages an entry. Re-staging an id replaces the document but keeps the original
|
|
9462
|
+
* `baseRev` and `isNew` - the conflict check is against the world as it was when
|
|
9463
|
+
* the transaction first touched the id, not against its own previous draft.
|
|
9464
|
+
*/
|
|
9465
|
+
set(id, entry) {
|
|
9466
|
+
var _a;
|
|
9467
|
+
const existing = this.entries.get(id);
|
|
9468
|
+
if (existing) {
|
|
9469
|
+
for (const key of this.partitionKeys(existing.doc)) {
|
|
9470
|
+
(_a = this.partitions.get(key)) === null || _a === void 0 ? void 0 : _a.delete(id);
|
|
9471
|
+
}
|
|
9472
|
+
entry = Object.assign(Object.assign({}, entry), { baseRev: existing.baseRev, isNew: existing.isNew });
|
|
9473
|
+
}
|
|
9474
|
+
this.entries.set(id, entry);
|
|
9475
|
+
for (const key of this.partitionKeys(entry.doc)) {
|
|
9476
|
+
let ids = this.partitions.get(key);
|
|
9477
|
+
if (!ids)
|
|
9478
|
+
this.partitions.set(key, (ids = new Set()));
|
|
9479
|
+
ids.add(id);
|
|
9480
|
+
}
|
|
9481
|
+
}
|
|
9482
|
+
get(id) {
|
|
9483
|
+
return this.entries.get(id);
|
|
9484
|
+
}
|
|
9485
|
+
has(id) {
|
|
9486
|
+
return this.entries.has(id);
|
|
9487
|
+
}
|
|
9488
|
+
get size() {
|
|
9489
|
+
return this.entries.size;
|
|
9490
|
+
}
|
|
9491
|
+
ids() {
|
|
9492
|
+
return [...this.entries.keys()];
|
|
9493
|
+
}
|
|
9494
|
+
/** Entries in stage order (insertion order of first staging). */
|
|
9495
|
+
values() {
|
|
9496
|
+
return [...this.entries.values()];
|
|
9497
|
+
}
|
|
9498
|
+
hasPartition(name) {
|
|
9499
|
+
var _a, _b;
|
|
9500
|
+
return ((_b = (_a = this.partitions.get(name)) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0) > 0;
|
|
9501
|
+
}
|
|
9502
|
+
/** Keeps only the given ids - what a partial commit leaves behind. */
|
|
9503
|
+
retain(ids) {
|
|
9504
|
+
for (const id of [...this.entries.keys()]) {
|
|
9505
|
+
if (!ids.has(id))
|
|
9506
|
+
this.remove(id);
|
|
9507
|
+
}
|
|
9508
|
+
}
|
|
9509
|
+
/**
|
|
9510
|
+
* A point-in-time copy of the journal, for {@link restore}. Used by the patch
|
|
9511
|
+
* chain (ADR-0044) to unwind exactly one patch's staging - a pre-apply job's
|
|
9512
|
+
* writes included - when a locked refusal converts that patch to a deferral
|
|
9513
|
+
* while the already-staged prefix goes on to commit.
|
|
9514
|
+
*/
|
|
9515
|
+
snapshot() {
|
|
9516
|
+
return new Map(this.entries);
|
|
9517
|
+
}
|
|
9518
|
+
restore(snapshot) {
|
|
9519
|
+
this.clear();
|
|
9520
|
+
for (const [id, entry] of snapshot) {
|
|
9521
|
+
this.set(id, entry);
|
|
9522
|
+
}
|
|
9523
|
+
}
|
|
9524
|
+
remove(id) {
|
|
9525
|
+
var _a;
|
|
9526
|
+
const entry = this.entries.get(id);
|
|
9527
|
+
if (!entry)
|
|
9528
|
+
return;
|
|
9529
|
+
for (const key of this.partitionKeys(entry.doc)) {
|
|
9530
|
+
(_a = this.partitions.get(key)) === null || _a === void 0 ? void 0 : _a.delete(id);
|
|
9531
|
+
}
|
|
9532
|
+
this.entries.delete(id);
|
|
9533
|
+
}
|
|
9534
|
+
clear() {
|
|
9535
|
+
this.entries.clear();
|
|
9536
|
+
this.partitions.clear();
|
|
9537
|
+
}
|
|
9538
|
+
}
|
|
9539
|
+
|
|
9285
9540
|
/**
|
|
9286
|
-
*
|
|
9541
|
+
* Errors of the transaction engine (ADR-0039).
|
|
9287
9542
|
*
|
|
9288
|
-
*
|
|
9289
|
-
*
|
|
9543
|
+
* Every one of them leaves the database untouched: a transaction failure is a refusal,
|
|
9544
|
+
* never a partial application. The one exception is named where it happens -
|
|
9545
|
+
* a commit on a non-atomic adapter can land a subset, and that outcome is reported as
|
|
9546
|
+
* a `partial` status on the handle, not thrown as one of these.
|
|
9547
|
+
*
|
|
9548
|
+
* @module
|
|
9290
9549
|
*/
|
|
9291
|
-
|
|
9550
|
+
/** Raised by `beginTransaction()` on a stack opened without `transactions: true`. */
|
|
9551
|
+
class TransactionsDisabledError extends Error {
|
|
9552
|
+
constructor(stackName) {
|
|
9553
|
+
super(`Stack '${stackName}' was opened without 'transactions: true'. ` +
|
|
9554
|
+
`Transactions are enabled per stack through its configuration, like encryption.`);
|
|
9555
|
+
this.name = "TransactionsDisabledError";
|
|
9556
|
+
}
|
|
9557
|
+
}
|
|
9558
|
+
/** Raised when a handle is used in a state that cannot accept the operation. */
|
|
9559
|
+
class TransactionStateError extends Error {
|
|
9560
|
+
constructor(transactionId, status, operation) {
|
|
9561
|
+
super(`Transaction '${transactionId}' is '${status}' and cannot ${operation}.`);
|
|
9562
|
+
this.name = "TransactionStateError";
|
|
9563
|
+
}
|
|
9564
|
+
}
|
|
9292
9565
|
/**
|
|
9293
|
-
*
|
|
9294
|
-
*
|
|
9295
|
-
* DocStack names everything it owns with a leading `~` - `~User`, `~Group`, `~Policy`,
|
|
9296
|
-
* `~Job`, `~JobRun`, `~UserSession`, `~AuthModule`, `~lock` - and reserves the handful of
|
|
9297
|
-
* unprefixed names in {@link META_CLASSES} for the datamodel itself. Everything else was
|
|
9298
|
-
* created by an application.
|
|
9299
|
-
*
|
|
9300
|
-
* @param className - A `~class` value or class-model id.
|
|
9301
|
-
*
|
|
9302
|
-
* @example
|
|
9303
|
-
* ```typescript
|
|
9304
|
-
* isContentClassName("Task"); // true
|
|
9305
|
-
* isContentClassName("~User"); // false - DocStack's own
|
|
9306
|
-
* isContentClassName("class"); // false - a class model
|
|
9307
|
-
* ```
|
|
9566
|
+
* Raised when the validation sweep refuses a document - at stage time (the write is
|
|
9567
|
+
* not staged) or at commit time (nothing is written, the transaction stays open).
|
|
9308
9568
|
*/
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
}
|
|
9569
|
+
class TransactionValidationError extends Error {
|
|
9570
|
+
constructor(message, docId) {
|
|
9571
|
+
super(message);
|
|
9572
|
+
this.name = "TransactionValidationError";
|
|
9573
|
+
this.docId = docId;
|
|
9574
|
+
}
|
|
9575
|
+
}
|
|
9316
9576
|
/**
|
|
9317
|
-
*
|
|
9318
|
-
*
|
|
9319
|
-
*
|
|
9577
|
+
* Raised by commit when a staged document's base revision no longer matches the
|
|
9578
|
+
* stored winner - a direct write, another transaction's commit, or replication moved
|
|
9579
|
+
* it. Nothing is written; the transaction stays open for re-staging or discard.
|
|
9320
9580
|
*/
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9325
|
-
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
}
|
|
9581
|
+
class TransactionConflictError extends Error {
|
|
9582
|
+
constructor(conflicts) {
|
|
9583
|
+
super(`Commit refused: ${conflicts.length} staged document(s) changed underneath the transaction: ` +
|
|
9584
|
+
conflicts.map(conflict => conflict.id).join(", "));
|
|
9585
|
+
this.name = "TransactionConflictError";
|
|
9586
|
+
this.conflicts = conflicts;
|
|
9587
|
+
}
|
|
9588
|
+
}
|
|
9329
9589
|
/**
|
|
9330
|
-
*
|
|
9331
|
-
*
|
|
9332
|
-
*
|
|
9333
|
-
* they need their own test rather than falling out of {@link isContentDocument}.
|
|
9334
|
-
*
|
|
9335
|
-
* @param doc - Any stored document.
|
|
9590
|
+
* Raised at stage time for documents transactions cannot carry: class models (their
|
|
9591
|
+
* write propagates to other documents mid-pipeline and cannot be staged or rolled
|
|
9592
|
+
* back - ADR-0039), `_local/` device state, and design documents.
|
|
9336
9593
|
*/
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
/** Fields PouchDB owns, or that describe one database's copy of a document. */
|
|
9345
|
-
const TRANSIENT_FIELDS = ["_rev", "_revisions", "_revs_info", "_conflicts", "_deleted"];
|
|
9594
|
+
class TransactionUnsupportedDocError extends Error {
|
|
9595
|
+
constructor(docId, reason) {
|
|
9596
|
+
super(`Document '${docId}' cannot be staged: ${reason}`);
|
|
9597
|
+
this.name = "TransactionUnsupportedDocError";
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9600
|
+
|
|
9346
9601
|
/**
|
|
9347
|
-
*
|
|
9348
|
-
*
|
|
9349
|
-
*
|
|
9350
|
-
*
|
|
9351
|
-
*
|
|
9352
|
-
*
|
|
9353
|
-
* @returns A copy without the transient fields.
|
|
9602
|
+
* Resolves a class from the transaction's own stage: the ADR-0043 rule
|
|
9603
|
+
* (the batch outranks the store - it is what is about to be committed) applied to
|
|
9604
|
+
* staging. A patch chain's job can create documents of a class an earlier patch
|
|
9605
|
+
* staged (ADR-0044), and the sweep must judge them by that staged model, not by a
|
|
9606
|
+
* committed predecessor or a not-found. Built DETACHED - `Class.get` + `setModel`,
|
|
9607
|
+
* never `buildFromModel`, which writes rev-less models (ADR-0043).
|
|
9354
9608
|
*/
|
|
9355
|
-
const
|
|
9356
|
-
const
|
|
9357
|
-
|
|
9358
|
-
delete
|
|
9359
|
-
|
|
9609
|
+
const classFromStage = (stack, stage, className) => {
|
|
9610
|
+
for (const entry of stage.values()) {
|
|
9611
|
+
const doc = entry.doc;
|
|
9612
|
+
if (entry.op !== "delete" && isClassModel(doc) && (doc._id === className || doc.name === className)) {
|
|
9613
|
+
const built = Class.get(stack, doc._id, doc.name, doc["~class"], doc.description, doc.schema, { subscribe: false });
|
|
9614
|
+
built.setModel(doc);
|
|
9615
|
+
return built;
|
|
9616
|
+
}
|
|
9617
|
+
}
|
|
9618
|
+
return null;
|
|
9360
9619
|
};
|
|
9361
9620
|
/**
|
|
9362
|
-
*
|
|
9363
|
-
*
|
|
9364
|
-
* A plain validator rather than an `asserts` signature: the caller already types its
|
|
9365
|
-
* parameter, so there is nothing to narrow, and an assertion function reached through an
|
|
9366
|
-
* import needs a declaration TypeScript can see (TS2775).
|
|
9621
|
+
* The validation sweep - the transaction's atomicity boundary in practice.
|
|
9367
9622
|
*
|
|
9368
|
-
*
|
|
9369
|
-
*
|
|
9623
|
+
* Runs read-only checks against public stack APIs: it decides whether a document
|
|
9624
|
+
* *could* be written, and touches nothing. Stage time runs it so a bad write fails at
|
|
9625
|
+
* the call site with zero consequences; commit re-runs it so the batch is judged
|
|
9626
|
+
* against the world as it stands at commit. The commit-time pipeline (the plugin's
|
|
9627
|
+
* `bulkDocs`) remains the sole authority - this sweep is a deliberate subset, and a
|
|
9628
|
+
* document it passes can still be refused there, atomically for the whole batch.
|
|
9370
9629
|
*/
|
|
9371
|
-
const
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9630
|
+
const sweepEntry = async (stack, stage, entry, options) => {
|
|
9631
|
+
var _a;
|
|
9632
|
+
const doc = entry.doc;
|
|
9633
|
+
const docId = doc._id;
|
|
9634
|
+
if (typeof docId === "string") {
|
|
9635
|
+
if (docId.startsWith("_local/")) {
|
|
9636
|
+
throw new TransactionUnsupportedDocError(docId, "'_local/' documents are device state, not transactional content.");
|
|
9637
|
+
}
|
|
9638
|
+
if (docId.startsWith("_design/")) {
|
|
9639
|
+
throw new TransactionUnsupportedDocError(docId, "design documents are index machinery, not transactional content.");
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
if (isClassModel(doc)) {
|
|
9643
|
+
// Public transactions refuse class models: a class commit's propagation is a
|
|
9644
|
+
// side effect beyond the batch, and a public handle promises none (ADR-0039).
|
|
9645
|
+
// An INTERNAL handle - patch application, ADR-0042 - claims only staged
|
|
9646
|
+
// validation and a single class-write batch, so it stages them; the parent
|
|
9647
|
+
// validation the pipeline would run at commit runs here instead, where a
|
|
9648
|
+
// refusal still costs nothing.
|
|
9649
|
+
if (!(options === null || options === void 0 ? void 0 : options.allowClassModels)) {
|
|
9650
|
+
throw new TransactionUnsupportedDocError(docId, "a class-model write propagates to the class's documents mid-pipeline and cannot be staged or rolled back (ADR-0039).");
|
|
9651
|
+
}
|
|
9652
|
+
const parentName = doc["~class"];
|
|
9653
|
+
if (parentName !== "~self") {
|
|
9654
|
+
const parentClass = await stack.getClass(parentName);
|
|
9655
|
+
if (!parentClass) {
|
|
9656
|
+
throw new TransactionValidationError(`Parent class '${parentName}' not found for class model '${docId}'.`, docId);
|
|
9657
|
+
}
|
|
9658
|
+
const valid = await parentClass.validate(doc);
|
|
9659
|
+
if (!valid) {
|
|
9660
|
+
throw new TransactionValidationError(`Class model '${docId}' is not valid for its parent class '${parentName}'.`, docId);
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
return;
|
|
9664
|
+
}
|
|
9665
|
+
if (isPatch(doc)) {
|
|
9666
|
+
throw new TransactionUnsupportedDocError(docId, "patches carry class models and apply through 'applyPatch'.");
|
|
9667
|
+
}
|
|
9668
|
+
// A hard delete carries no content to validate; write access is still the
|
|
9669
|
+
// author's to prove - unless this is DocStack's own machinery (an internal
|
|
9670
|
+
// handle: patch application runs before any session exists, and the patch
|
|
9671
|
+
// path's direct writes never pass through policy either - ADR-0044).
|
|
9672
|
+
if (entry.op === "delete") {
|
|
9673
|
+
const type = doc["~class"];
|
|
9674
|
+
if (typeof type === "string" && !(options === null || options === void 0 ? void 0 : options.skipPolicy)) {
|
|
9675
|
+
await stack.policyEngine.ensureWriteAllowed(type, doc);
|
|
9676
|
+
}
|
|
9677
|
+
return;
|
|
9678
|
+
}
|
|
9679
|
+
if (isRelation(doc)) {
|
|
9680
|
+
const domain = await stack.getDomain(doc["~domain"]);
|
|
9681
|
+
if (!domain) {
|
|
9682
|
+
throw new TransactionValidationError(`Domain not found: ${doc["~domain"]}`, docId);
|
|
9683
|
+
}
|
|
9684
|
+
// An endpoint is resolvable if it is committed, or staged in this same
|
|
9685
|
+
// transaction - the batch commits together (the plugin resolves batch-mates
|
|
9686
|
+
// since ADR-0039).
|
|
9687
|
+
for (const endpoint of [doc.sourceId, doc.targetId]) {
|
|
9688
|
+
if (stage.has(endpoint) && stage.get(endpoint).op !== "delete")
|
|
9689
|
+
continue;
|
|
9690
|
+
const stored = await stack.db.get(endpoint).catch(() => null);
|
|
9691
|
+
if (!stored) {
|
|
9692
|
+
throw new TransactionValidationError(`Relation endpoint '${endpoint}' does not exist, committed or staged, for domain '${domain.name}'.`, docId);
|
|
9693
|
+
}
|
|
9694
|
+
}
|
|
9695
|
+
return;
|
|
9696
|
+
}
|
|
9697
|
+
const type = doc["~class"];
|
|
9698
|
+
if (typeof type !== "string" || !type) {
|
|
9699
|
+
throw new TransactionValidationError(`Document '${docId}' carries no '~class'.`, docId);
|
|
9700
|
+
}
|
|
9701
|
+
if (stack.isSimpleClass(type))
|
|
9702
|
+
return;
|
|
9703
|
+
const classObj = (_a = classFromStage(stack, stage, type)) !== null && _a !== void 0 ? _a : await stack.getClassSnapshot(type).catch(() => null);
|
|
9704
|
+
if (!classObj) {
|
|
9705
|
+
throw new TransactionValidationError(`Class '${type}' not found for document '${docId}'.`, docId);
|
|
9706
|
+
}
|
|
9707
|
+
// Same refusal the plugin makes (ADR-0018): a locked stack cannot encrypt, and
|
|
9708
|
+
// committing later while still locked would land the fields in the clear.
|
|
9709
|
+
if (classObj.getEncryptedAttributes().length && stack.isLocked()) {
|
|
9710
|
+
throw new StackLockedError(type);
|
|
9711
|
+
}
|
|
9712
|
+
const valid = await classObj.validate(doc);
|
|
9713
|
+
if (!valid) {
|
|
9714
|
+
throw new TransactionValidationError(`Document '${docId}' does not validate against class '${type}'.`, docId);
|
|
9715
|
+
}
|
|
9716
|
+
if (!(options === null || options === void 0 ? void 0 : options.skipPolicy)) {
|
|
9717
|
+
await stack.policyEngine.ensureWriteAllowed(type, doc);
|
|
9718
|
+
}
|
|
9719
|
+
};
|
|
9720
|
+
|
|
9721
|
+
class PouchError extends Error {
|
|
9722
|
+
constructor(status, error, reason) {
|
|
9723
|
+
super();
|
|
9724
|
+
this.status = status;
|
|
9725
|
+
this.name = error;
|
|
9726
|
+
this.message = reason;
|
|
9727
|
+
this.error = true;
|
|
9728
|
+
}
|
|
9729
|
+
|
|
9730
|
+
toString() {
|
|
9731
|
+
return JSON.stringify({
|
|
9732
|
+
status: this.status,
|
|
9733
|
+
name: this.name,
|
|
9734
|
+
message: this.message,
|
|
9735
|
+
reason: this.reason
|
|
9736
|
+
});
|
|
9737
|
+
}
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9740
|
+
new PouchError(401, 'unauthorized', "Name or password is incorrect.");
|
|
9741
|
+
new PouchError(400, 'bad_request', "Missing JSON list of 'docs'");
|
|
9742
|
+
new PouchError(404, 'not_found', 'missing');
|
|
9743
|
+
new PouchError(409, 'conflict', 'Document update conflict');
|
|
9744
|
+
new PouchError(400, 'bad_request', '_id field must contain a string');
|
|
9745
|
+
new PouchError(412, 'missing_id', '_id is required for puts');
|
|
9746
|
+
new PouchError(400, 'bad_request', 'Only reserved document ids may start with underscore.');
|
|
9747
|
+
new PouchError(412, 'precondition_failed', 'Database not open');
|
|
9748
|
+
new PouchError(500, 'unknown_error', 'Database encountered an unknown error');
|
|
9749
|
+
new PouchError(500, 'badarg', 'Some query argument is invalid');
|
|
9750
|
+
new PouchError(400, 'invalid_request', 'Request was invalid');
|
|
9751
|
+
new PouchError(400, 'query_parse_error', 'Some query parameter is invalid');
|
|
9752
|
+
new PouchError(500, 'doc_validation', 'Bad special document member');
|
|
9753
|
+
new PouchError(400, 'bad_request', 'Something wrong with the request');
|
|
9754
|
+
new PouchError(400, 'bad_request', 'Document must be a JSON object');
|
|
9755
|
+
new PouchError(404, 'not_found', 'Database not found');
|
|
9756
|
+
new PouchError(500, 'indexed_db_went_bad', 'unknown');
|
|
9757
|
+
new PouchError(500, 'web_sql_went_bad', 'unknown');
|
|
9758
|
+
new PouchError(500, 'levelDB_went_went_bad', 'unknown');
|
|
9759
|
+
new PouchError(403, 'forbidden', 'Forbidden by design doc validate_doc_update function');
|
|
9760
|
+
new PouchError(400, 'bad_request', 'Invalid rev format');
|
|
9761
|
+
new PouchError(412, 'file_exists', 'The database could not be created, the file already exists.');
|
|
9762
|
+
new PouchError(412, 'missing_stub', 'A pre-existing attachment stub wasn\'t found');
|
|
9763
|
+
new PouchError(413, 'invalid_url', 'Provided URL is invalid');
|
|
9764
|
+
|
|
9765
|
+
var events = {exports: {}};
|
|
9766
|
+
|
|
9767
|
+
var R = typeof Reflect === 'object' ? Reflect : null;
|
|
9768
|
+
var ReflectApply = R && typeof R.apply === 'function'
|
|
9769
|
+
? R.apply
|
|
9770
|
+
: function ReflectApply(target, receiver, args) {
|
|
9771
|
+
return Function.prototype.apply.call(target, receiver, args);
|
|
9772
|
+
};
|
|
9773
|
+
|
|
9774
|
+
var ReflectOwnKeys;
|
|
9775
|
+
if (R && typeof R.ownKeys === 'function') {
|
|
9776
|
+
ReflectOwnKeys = R.ownKeys;
|
|
9777
|
+
} else if (Object.getOwnPropertySymbols) {
|
|
9778
|
+
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
|
9779
|
+
return Object.getOwnPropertyNames(target)
|
|
9780
|
+
.concat(Object.getOwnPropertySymbols(target));
|
|
9781
|
+
};
|
|
9782
|
+
} else {
|
|
9783
|
+
ReflectOwnKeys = function ReflectOwnKeys(target) {
|
|
9784
|
+
return Object.getOwnPropertyNames(target);
|
|
9785
|
+
};
|
|
9786
|
+
}
|
|
9787
|
+
|
|
9788
|
+
function ProcessEmitWarning(warning) {
|
|
9789
|
+
if (console && console.warn) console.warn(warning);
|
|
9790
|
+
}
|
|
9791
|
+
|
|
9792
|
+
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
|
|
9793
|
+
return value !== value;
|
|
9794
|
+
};
|
|
9795
|
+
|
|
9796
|
+
function EventEmitter() {
|
|
9797
|
+
EventEmitter.init.call(this);
|
|
9798
|
+
}
|
|
9799
|
+
events.exports = EventEmitter;
|
|
9800
|
+
events.exports.once = once;
|
|
9801
|
+
|
|
9802
|
+
// Backwards-compat with node 0.10.x
|
|
9803
|
+
EventEmitter.EventEmitter = EventEmitter;
|
|
9804
|
+
|
|
9805
|
+
EventEmitter.prototype._events = undefined;
|
|
9806
|
+
EventEmitter.prototype._eventsCount = 0;
|
|
9807
|
+
EventEmitter.prototype._maxListeners = undefined;
|
|
9808
|
+
|
|
9809
|
+
// By default EventEmitters will print a warning if more than 10 listeners are
|
|
9810
|
+
// added to it. This is a useful default which helps finding memory leaks.
|
|
9811
|
+
var defaultMaxListeners = 10;
|
|
9812
|
+
|
|
9813
|
+
function checkListener(listener) {
|
|
9814
|
+
if (typeof listener !== 'function') {
|
|
9815
|
+
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
|
|
9819
|
+
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
|
|
9820
|
+
enumerable: true,
|
|
9821
|
+
get: function() {
|
|
9822
|
+
return defaultMaxListeners;
|
|
9823
|
+
},
|
|
9824
|
+
set: function(arg) {
|
|
9825
|
+
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
|
|
9826
|
+
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
|
|
9827
|
+
}
|
|
9828
|
+
defaultMaxListeners = arg;
|
|
9829
|
+
}
|
|
9830
|
+
});
|
|
9831
|
+
|
|
9832
|
+
EventEmitter.init = function() {
|
|
9833
|
+
|
|
9834
|
+
if (this._events === undefined ||
|
|
9835
|
+
this._events === Object.getPrototypeOf(this)._events) {
|
|
9836
|
+
this._events = Object.create(null);
|
|
9837
|
+
this._eventsCount = 0;
|
|
9838
|
+
}
|
|
9839
|
+
|
|
9840
|
+
this._maxListeners = this._maxListeners || undefined;
|
|
9841
|
+
};
|
|
9842
|
+
|
|
9843
|
+
// Obviously not all Emitters should be limited to 10. This function allows
|
|
9844
|
+
// that to be increased. Set to zero for unlimited.
|
|
9845
|
+
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
|
|
9846
|
+
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
|
|
9847
|
+
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
|
9848
|
+
}
|
|
9849
|
+
this._maxListeners = n;
|
|
9850
|
+
return this;
|
|
9851
|
+
};
|
|
9852
|
+
|
|
9853
|
+
function _getMaxListeners(that) {
|
|
9854
|
+
if (that._maxListeners === undefined)
|
|
9855
|
+
return EventEmitter.defaultMaxListeners;
|
|
9856
|
+
return that._maxListeners;
|
|
9857
|
+
}
|
|
9858
|
+
|
|
9859
|
+
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
|
|
9860
|
+
return _getMaxListeners(this);
|
|
9861
|
+
};
|
|
9862
|
+
|
|
9863
|
+
EventEmitter.prototype.emit = function emit(type) {
|
|
9864
|
+
var args = [];
|
|
9865
|
+
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
|
|
9866
|
+
var doError = (type === 'error');
|
|
9867
|
+
|
|
9868
|
+
var events = this._events;
|
|
9869
|
+
if (events !== undefined)
|
|
9870
|
+
doError = (doError && events.error === undefined);
|
|
9871
|
+
else if (!doError)
|
|
9872
|
+
return false;
|
|
9873
|
+
|
|
9874
|
+
// If there is no 'error' event listener then throw.
|
|
9875
|
+
if (doError) {
|
|
9876
|
+
var er;
|
|
9877
|
+
if (args.length > 0)
|
|
9878
|
+
er = args[0];
|
|
9879
|
+
if (er instanceof Error) {
|
|
9880
|
+
// Note: The comments on the `throw` lines are intentional, they show
|
|
9881
|
+
// up in Node's output if this results in an unhandled exception.
|
|
9882
|
+
throw er; // Unhandled 'error' event
|
|
9883
|
+
}
|
|
9884
|
+
// At least give some kind of context to the user
|
|
9885
|
+
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
|
|
9886
|
+
err.context = er;
|
|
9887
|
+
throw err; // Unhandled 'error' event
|
|
9888
|
+
}
|
|
9889
|
+
|
|
9890
|
+
var handler = events[type];
|
|
9891
|
+
|
|
9892
|
+
if (handler === undefined)
|
|
9893
|
+
return false;
|
|
9894
|
+
|
|
9895
|
+
if (typeof handler === 'function') {
|
|
9896
|
+
ReflectApply(handler, this, args);
|
|
9897
|
+
} else {
|
|
9898
|
+
var len = handler.length;
|
|
9899
|
+
var listeners = arrayClone(handler, len);
|
|
9900
|
+
for (var i = 0; i < len; ++i)
|
|
9901
|
+
ReflectApply(listeners[i], this, args);
|
|
9902
|
+
}
|
|
9903
|
+
|
|
9904
|
+
return true;
|
|
9905
|
+
};
|
|
9906
|
+
|
|
9907
|
+
function _addListener(target, type, listener, prepend) {
|
|
9908
|
+
var m;
|
|
9909
|
+
var events;
|
|
9910
|
+
var existing;
|
|
9911
|
+
|
|
9912
|
+
checkListener(listener);
|
|
9913
|
+
|
|
9914
|
+
events = target._events;
|
|
9915
|
+
if (events === undefined) {
|
|
9916
|
+
events = target._events = Object.create(null);
|
|
9917
|
+
target._eventsCount = 0;
|
|
9918
|
+
} else {
|
|
9919
|
+
// To avoid recursion in the case that type === "newListener"! Before
|
|
9920
|
+
// adding it to the listeners, first emit "newListener".
|
|
9921
|
+
if (events.newListener !== undefined) {
|
|
9922
|
+
target.emit('newListener', type,
|
|
9923
|
+
listener.listener ? listener.listener : listener);
|
|
9924
|
+
|
|
9925
|
+
// Re-assign `events` because a newListener handler could have caused the
|
|
9926
|
+
// this._events to be assigned to a new object
|
|
9927
|
+
events = target._events;
|
|
9928
|
+
}
|
|
9929
|
+
existing = events[type];
|
|
9930
|
+
}
|
|
9931
|
+
|
|
9932
|
+
if (existing === undefined) {
|
|
9933
|
+
// Optimize the case of one listener. Don't need the extra array object.
|
|
9934
|
+
existing = events[type] = listener;
|
|
9935
|
+
++target._eventsCount;
|
|
9936
|
+
} else {
|
|
9937
|
+
if (typeof existing === 'function') {
|
|
9938
|
+
// Adding the second element, need to change to array.
|
|
9939
|
+
existing = events[type] =
|
|
9940
|
+
prepend ? [listener, existing] : [existing, listener];
|
|
9941
|
+
// If we've already got an array, just append.
|
|
9942
|
+
} else if (prepend) {
|
|
9943
|
+
existing.unshift(listener);
|
|
9944
|
+
} else {
|
|
9945
|
+
existing.push(listener);
|
|
9946
|
+
}
|
|
9947
|
+
|
|
9948
|
+
// Check for listener leak
|
|
9949
|
+
m = _getMaxListeners(target);
|
|
9950
|
+
if (m > 0 && existing.length > m && !existing.warned) {
|
|
9951
|
+
existing.warned = true;
|
|
9952
|
+
// No error code for this since it is a Warning
|
|
9953
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
9954
|
+
var w = new Error('Possible EventEmitter memory leak detected. ' +
|
|
9955
|
+
existing.length + ' ' + String(type) + ' listeners ' +
|
|
9956
|
+
'added. Use emitter.setMaxListeners() to ' +
|
|
9957
|
+
'increase limit');
|
|
9958
|
+
w.name = 'MaxListenersExceededWarning';
|
|
9959
|
+
w.emitter = target;
|
|
9960
|
+
w.type = type;
|
|
9961
|
+
w.count = existing.length;
|
|
9962
|
+
ProcessEmitWarning(w);
|
|
9963
|
+
}
|
|
9964
|
+
}
|
|
9965
|
+
|
|
9966
|
+
return target;
|
|
9967
|
+
}
|
|
9968
|
+
|
|
9969
|
+
EventEmitter.prototype.addListener = function addListener(type, listener) {
|
|
9970
|
+
return _addListener(this, type, listener, false);
|
|
9971
|
+
};
|
|
9972
|
+
|
|
9973
|
+
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
|
|
9974
|
+
|
|
9975
|
+
EventEmitter.prototype.prependListener =
|
|
9976
|
+
function prependListener(type, listener) {
|
|
9977
|
+
return _addListener(this, type, listener, true);
|
|
9978
|
+
};
|
|
9979
|
+
|
|
9980
|
+
function onceWrapper() {
|
|
9981
|
+
if (!this.fired) {
|
|
9982
|
+
this.target.removeListener(this.type, this.wrapFn);
|
|
9983
|
+
this.fired = true;
|
|
9984
|
+
if (arguments.length === 0)
|
|
9985
|
+
return this.listener.call(this.target);
|
|
9986
|
+
return this.listener.apply(this.target, arguments);
|
|
9987
|
+
}
|
|
9988
|
+
}
|
|
9989
|
+
|
|
9990
|
+
function _onceWrap(target, type, listener) {
|
|
9991
|
+
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
|
|
9992
|
+
var wrapped = onceWrapper.bind(state);
|
|
9993
|
+
wrapped.listener = listener;
|
|
9994
|
+
state.wrapFn = wrapped;
|
|
9995
|
+
return wrapped;
|
|
9996
|
+
}
|
|
9997
|
+
|
|
9998
|
+
EventEmitter.prototype.once = function once(type, listener) {
|
|
9999
|
+
checkListener(listener);
|
|
10000
|
+
this.on(type, _onceWrap(this, type, listener));
|
|
10001
|
+
return this;
|
|
10002
|
+
};
|
|
10003
|
+
|
|
10004
|
+
EventEmitter.prototype.prependOnceListener =
|
|
10005
|
+
function prependOnceListener(type, listener) {
|
|
10006
|
+
checkListener(listener);
|
|
10007
|
+
this.prependListener(type, _onceWrap(this, type, listener));
|
|
10008
|
+
return this;
|
|
10009
|
+
};
|
|
10010
|
+
|
|
10011
|
+
// Emits a 'removeListener' event if and only if the listener was removed.
|
|
10012
|
+
EventEmitter.prototype.removeListener =
|
|
10013
|
+
function removeListener(type, listener) {
|
|
10014
|
+
var list, events, position, i, originalListener;
|
|
10015
|
+
|
|
10016
|
+
checkListener(listener);
|
|
10017
|
+
|
|
10018
|
+
events = this._events;
|
|
10019
|
+
if (events === undefined)
|
|
10020
|
+
return this;
|
|
10021
|
+
|
|
10022
|
+
list = events[type];
|
|
10023
|
+
if (list === undefined)
|
|
10024
|
+
return this;
|
|
10025
|
+
|
|
10026
|
+
if (list === listener || list.listener === listener) {
|
|
10027
|
+
if (--this._eventsCount === 0)
|
|
10028
|
+
this._events = Object.create(null);
|
|
10029
|
+
else {
|
|
10030
|
+
delete events[type];
|
|
10031
|
+
if (events.removeListener)
|
|
10032
|
+
this.emit('removeListener', type, list.listener || listener);
|
|
10033
|
+
}
|
|
10034
|
+
} else if (typeof list !== 'function') {
|
|
10035
|
+
position = -1;
|
|
10036
|
+
|
|
10037
|
+
for (i = list.length - 1; i >= 0; i--) {
|
|
10038
|
+
if (list[i] === listener || list[i].listener === listener) {
|
|
10039
|
+
originalListener = list[i].listener;
|
|
10040
|
+
position = i;
|
|
10041
|
+
break;
|
|
10042
|
+
}
|
|
10043
|
+
}
|
|
10044
|
+
|
|
10045
|
+
if (position < 0)
|
|
10046
|
+
return this;
|
|
10047
|
+
|
|
10048
|
+
if (position === 0)
|
|
10049
|
+
list.shift();
|
|
10050
|
+
else {
|
|
10051
|
+
spliceOne(list, position);
|
|
10052
|
+
}
|
|
10053
|
+
|
|
10054
|
+
if (list.length === 1)
|
|
10055
|
+
events[type] = list[0];
|
|
10056
|
+
|
|
10057
|
+
if (events.removeListener !== undefined)
|
|
10058
|
+
this.emit('removeListener', type, originalListener || listener);
|
|
10059
|
+
}
|
|
10060
|
+
|
|
10061
|
+
return this;
|
|
10062
|
+
};
|
|
10063
|
+
|
|
10064
|
+
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
|
|
10065
|
+
|
|
10066
|
+
EventEmitter.prototype.removeAllListeners =
|
|
10067
|
+
function removeAllListeners(type) {
|
|
10068
|
+
var listeners, events, i;
|
|
10069
|
+
|
|
10070
|
+
events = this._events;
|
|
10071
|
+
if (events === undefined)
|
|
10072
|
+
return this;
|
|
10073
|
+
|
|
10074
|
+
// not listening for removeListener, no need to emit
|
|
10075
|
+
if (events.removeListener === undefined) {
|
|
10076
|
+
if (arguments.length === 0) {
|
|
10077
|
+
this._events = Object.create(null);
|
|
10078
|
+
this._eventsCount = 0;
|
|
10079
|
+
} else if (events[type] !== undefined) {
|
|
10080
|
+
if (--this._eventsCount === 0)
|
|
10081
|
+
this._events = Object.create(null);
|
|
10082
|
+
else
|
|
10083
|
+
delete events[type];
|
|
10084
|
+
}
|
|
10085
|
+
return this;
|
|
10086
|
+
}
|
|
10087
|
+
|
|
10088
|
+
// emit removeListener for all listeners on all events
|
|
10089
|
+
if (arguments.length === 0) {
|
|
10090
|
+
var keys = Object.keys(events);
|
|
10091
|
+
var key;
|
|
10092
|
+
for (i = 0; i < keys.length; ++i) {
|
|
10093
|
+
key = keys[i];
|
|
10094
|
+
if (key === 'removeListener') continue;
|
|
10095
|
+
this.removeAllListeners(key);
|
|
10096
|
+
}
|
|
10097
|
+
this.removeAllListeners('removeListener');
|
|
10098
|
+
this._events = Object.create(null);
|
|
10099
|
+
this._eventsCount = 0;
|
|
10100
|
+
return this;
|
|
10101
|
+
}
|
|
10102
|
+
|
|
10103
|
+
listeners = events[type];
|
|
10104
|
+
|
|
10105
|
+
if (typeof listeners === 'function') {
|
|
10106
|
+
this.removeListener(type, listeners);
|
|
10107
|
+
} else if (listeners !== undefined) {
|
|
10108
|
+
// LIFO order
|
|
10109
|
+
for (i = listeners.length - 1; i >= 0; i--) {
|
|
10110
|
+
this.removeListener(type, listeners[i]);
|
|
10111
|
+
}
|
|
10112
|
+
}
|
|
10113
|
+
|
|
10114
|
+
return this;
|
|
10115
|
+
};
|
|
10116
|
+
|
|
10117
|
+
function _listeners(target, type, unwrap) {
|
|
10118
|
+
var events = target._events;
|
|
10119
|
+
|
|
10120
|
+
if (events === undefined)
|
|
10121
|
+
return [];
|
|
10122
|
+
|
|
10123
|
+
var evlistener = events[type];
|
|
10124
|
+
if (evlistener === undefined)
|
|
10125
|
+
return [];
|
|
10126
|
+
|
|
10127
|
+
if (typeof evlistener === 'function')
|
|
10128
|
+
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
|
|
10129
|
+
|
|
10130
|
+
return unwrap ?
|
|
10131
|
+
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
|
|
10132
|
+
}
|
|
10133
|
+
|
|
10134
|
+
EventEmitter.prototype.listeners = function listeners(type) {
|
|
10135
|
+
return _listeners(this, type, true);
|
|
10136
|
+
};
|
|
10137
|
+
|
|
10138
|
+
EventEmitter.prototype.rawListeners = function rawListeners(type) {
|
|
10139
|
+
return _listeners(this, type, false);
|
|
10140
|
+
};
|
|
10141
|
+
|
|
10142
|
+
EventEmitter.listenerCount = function(emitter, type) {
|
|
10143
|
+
if (typeof emitter.listenerCount === 'function') {
|
|
10144
|
+
return emitter.listenerCount(type);
|
|
10145
|
+
} else {
|
|
10146
|
+
return listenerCount.call(emitter, type);
|
|
10147
|
+
}
|
|
10148
|
+
};
|
|
10149
|
+
|
|
10150
|
+
EventEmitter.prototype.listenerCount = listenerCount;
|
|
10151
|
+
function listenerCount(type) {
|
|
10152
|
+
var events = this._events;
|
|
10153
|
+
|
|
10154
|
+
if (events !== undefined) {
|
|
10155
|
+
var evlistener = events[type];
|
|
10156
|
+
|
|
10157
|
+
if (typeof evlistener === 'function') {
|
|
10158
|
+
return 1;
|
|
10159
|
+
} else if (evlistener !== undefined) {
|
|
10160
|
+
return evlistener.length;
|
|
10161
|
+
}
|
|
10162
|
+
}
|
|
10163
|
+
|
|
10164
|
+
return 0;
|
|
10165
|
+
}
|
|
10166
|
+
|
|
10167
|
+
EventEmitter.prototype.eventNames = function eventNames() {
|
|
10168
|
+
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
|
|
10169
|
+
};
|
|
10170
|
+
|
|
10171
|
+
function arrayClone(arr, n) {
|
|
10172
|
+
var copy = new Array(n);
|
|
10173
|
+
for (var i = 0; i < n; ++i)
|
|
10174
|
+
copy[i] = arr[i];
|
|
10175
|
+
return copy;
|
|
10176
|
+
}
|
|
10177
|
+
|
|
10178
|
+
function spliceOne(list, index) {
|
|
10179
|
+
for (; index + 1 < list.length; index++)
|
|
10180
|
+
list[index] = list[index + 1];
|
|
10181
|
+
list.pop();
|
|
10182
|
+
}
|
|
10183
|
+
|
|
10184
|
+
function unwrapListeners(arr) {
|
|
10185
|
+
var ret = new Array(arr.length);
|
|
10186
|
+
for (var i = 0; i < ret.length; ++i) {
|
|
10187
|
+
ret[i] = arr[i].listener || arr[i];
|
|
10188
|
+
}
|
|
10189
|
+
return ret;
|
|
10190
|
+
}
|
|
10191
|
+
|
|
10192
|
+
function once(emitter, name) {
|
|
10193
|
+
return new Promise(function (resolve, reject) {
|
|
10194
|
+
function errorListener(err) {
|
|
10195
|
+
emitter.removeListener(name, resolver);
|
|
10196
|
+
reject(err);
|
|
10197
|
+
}
|
|
10198
|
+
|
|
10199
|
+
function resolver() {
|
|
10200
|
+
if (typeof emitter.removeListener === 'function') {
|
|
10201
|
+
emitter.removeListener('error', errorListener);
|
|
10202
|
+
}
|
|
10203
|
+
resolve([].slice.call(arguments));
|
|
10204
|
+
}
|
|
10205
|
+
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
|
|
10206
|
+
if (name !== 'error') {
|
|
10207
|
+
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
|
|
10208
|
+
}
|
|
10209
|
+
});
|
|
10210
|
+
}
|
|
10211
|
+
|
|
10212
|
+
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
|
|
10213
|
+
if (typeof emitter.on === 'function') {
|
|
10214
|
+
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
|
|
10218
|
+
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
|
|
10219
|
+
if (typeof emitter.on === 'function') {
|
|
10220
|
+
if (flags.once) {
|
|
10221
|
+
emitter.once(name, listener);
|
|
10222
|
+
} else {
|
|
10223
|
+
emitter.on(name, listener);
|
|
10224
|
+
}
|
|
10225
|
+
} else if (typeof emitter.addEventListener === 'function') {
|
|
10226
|
+
// EventTarget does not have `error` event semantics like Node
|
|
10227
|
+
// EventEmitters, we do not listen for `error` events here.
|
|
10228
|
+
emitter.addEventListener(name, function wrapListener(arg) {
|
|
10229
|
+
// IE does not have builtin `{ once: true }` support so we
|
|
10230
|
+
// have to do it manually.
|
|
10231
|
+
if (flags.once) {
|
|
10232
|
+
emitter.removeEventListener(name, wrapListener);
|
|
10233
|
+
}
|
|
10234
|
+
listener(arg);
|
|
10235
|
+
});
|
|
10236
|
+
} else {
|
|
10237
|
+
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
|
|
10238
|
+
}
|
|
10239
|
+
}
|
|
10240
|
+
|
|
10241
|
+
var sparkMd5 = {exports: {}};
|
|
10242
|
+
|
|
10243
|
+
(function (module, exports$1) {
|
|
10244
|
+
(function (factory) {
|
|
10245
|
+
{
|
|
10246
|
+
// Node/CommonJS
|
|
10247
|
+
module.exports = factory();
|
|
10248
|
+
}
|
|
10249
|
+
}(function (undefined$1) {
|
|
10250
|
+
|
|
10251
|
+
/*
|
|
10252
|
+
* Fastest md5 implementation around (JKM md5).
|
|
10253
|
+
* Credits: Joseph Myers
|
|
10254
|
+
*
|
|
10255
|
+
* @see http://www.myersdaily.org/joseph/javascript/md5-text.html
|
|
10256
|
+
* @see http://jsperf.com/md5-shootout/7
|
|
10257
|
+
*/
|
|
10258
|
+
|
|
10259
|
+
/* this function is much faster,
|
|
10260
|
+
so if possible we use it. Some IEs
|
|
10261
|
+
are the only ones I know of that
|
|
10262
|
+
need the idiotic second function,
|
|
10263
|
+
generated by an if clause. */
|
|
10264
|
+
var hex_chr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
|
10265
|
+
|
|
10266
|
+
function md5cycle(x, k) {
|
|
10267
|
+
var a = x[0],
|
|
10268
|
+
b = x[1],
|
|
10269
|
+
c = x[2],
|
|
10270
|
+
d = x[3];
|
|
10271
|
+
|
|
10272
|
+
a += (b & c | ~b & d) + k[0] - 680876936 | 0;
|
|
10273
|
+
a = (a << 7 | a >>> 25) + b | 0;
|
|
10274
|
+
d += (a & b | ~a & c) + k[1] - 389564586 | 0;
|
|
10275
|
+
d = (d << 12 | d >>> 20) + a | 0;
|
|
10276
|
+
c += (d & a | ~d & b) + k[2] + 606105819 | 0;
|
|
10277
|
+
c = (c << 17 | c >>> 15) + d | 0;
|
|
10278
|
+
b += (c & d | ~c & a) + k[3] - 1044525330 | 0;
|
|
10279
|
+
b = (b << 22 | b >>> 10) + c | 0;
|
|
10280
|
+
a += (b & c | ~b & d) + k[4] - 176418897 | 0;
|
|
10281
|
+
a = (a << 7 | a >>> 25) + b | 0;
|
|
10282
|
+
d += (a & b | ~a & c) + k[5] + 1200080426 | 0;
|
|
10283
|
+
d = (d << 12 | d >>> 20) + a | 0;
|
|
10284
|
+
c += (d & a | ~d & b) + k[6] - 1473231341 | 0;
|
|
10285
|
+
c = (c << 17 | c >>> 15) + d | 0;
|
|
10286
|
+
b += (c & d | ~c & a) + k[7] - 45705983 | 0;
|
|
10287
|
+
b = (b << 22 | b >>> 10) + c | 0;
|
|
10288
|
+
a += (b & c | ~b & d) + k[8] + 1770035416 | 0;
|
|
10289
|
+
a = (a << 7 | a >>> 25) + b | 0;
|
|
10290
|
+
d += (a & b | ~a & c) + k[9] - 1958414417 | 0;
|
|
10291
|
+
d = (d << 12 | d >>> 20) + a | 0;
|
|
10292
|
+
c += (d & a | ~d & b) + k[10] - 42063 | 0;
|
|
10293
|
+
c = (c << 17 | c >>> 15) + d | 0;
|
|
10294
|
+
b += (c & d | ~c & a) + k[11] - 1990404162 | 0;
|
|
10295
|
+
b = (b << 22 | b >>> 10) + c | 0;
|
|
10296
|
+
a += (b & c | ~b & d) + k[12] + 1804603682 | 0;
|
|
10297
|
+
a = (a << 7 | a >>> 25) + b | 0;
|
|
10298
|
+
d += (a & b | ~a & c) + k[13] - 40341101 | 0;
|
|
10299
|
+
d = (d << 12 | d >>> 20) + a | 0;
|
|
10300
|
+
c += (d & a | ~d & b) + k[14] - 1502002290 | 0;
|
|
10301
|
+
c = (c << 17 | c >>> 15) + d | 0;
|
|
10302
|
+
b += (c & d | ~c & a) + k[15] + 1236535329 | 0;
|
|
10303
|
+
b = (b << 22 | b >>> 10) + c | 0;
|
|
10304
|
+
|
|
10305
|
+
a += (b & d | c & ~d) + k[1] - 165796510 | 0;
|
|
10306
|
+
a = (a << 5 | a >>> 27) + b | 0;
|
|
10307
|
+
d += (a & c | b & ~c) + k[6] - 1069501632 | 0;
|
|
10308
|
+
d = (d << 9 | d >>> 23) + a | 0;
|
|
10309
|
+
c += (d & b | a & ~b) + k[11] + 643717713 | 0;
|
|
10310
|
+
c = (c << 14 | c >>> 18) + d | 0;
|
|
10311
|
+
b += (c & a | d & ~a) + k[0] - 373897302 | 0;
|
|
10312
|
+
b = (b << 20 | b >>> 12) + c | 0;
|
|
10313
|
+
a += (b & d | c & ~d) + k[5] - 701558691 | 0;
|
|
10314
|
+
a = (a << 5 | a >>> 27) + b | 0;
|
|
10315
|
+
d += (a & c | b & ~c) + k[10] + 38016083 | 0;
|
|
10316
|
+
d = (d << 9 | d >>> 23) + a | 0;
|
|
10317
|
+
c += (d & b | a & ~b) + k[15] - 660478335 | 0;
|
|
10318
|
+
c = (c << 14 | c >>> 18) + d | 0;
|
|
10319
|
+
b += (c & a | d & ~a) + k[4] - 405537848 | 0;
|
|
10320
|
+
b = (b << 20 | b >>> 12) + c | 0;
|
|
10321
|
+
a += (b & d | c & ~d) + k[9] + 568446438 | 0;
|
|
10322
|
+
a = (a << 5 | a >>> 27) + b | 0;
|
|
10323
|
+
d += (a & c | b & ~c) + k[14] - 1019803690 | 0;
|
|
10324
|
+
d = (d << 9 | d >>> 23) + a | 0;
|
|
10325
|
+
c += (d & b | a & ~b) + k[3] - 187363961 | 0;
|
|
10326
|
+
c = (c << 14 | c >>> 18) + d | 0;
|
|
10327
|
+
b += (c & a | d & ~a) + k[8] + 1163531501 | 0;
|
|
10328
|
+
b = (b << 20 | b >>> 12) + c | 0;
|
|
10329
|
+
a += (b & d | c & ~d) + k[13] - 1444681467 | 0;
|
|
10330
|
+
a = (a << 5 | a >>> 27) + b | 0;
|
|
10331
|
+
d += (a & c | b & ~c) + k[2] - 51403784 | 0;
|
|
10332
|
+
d = (d << 9 | d >>> 23) + a | 0;
|
|
10333
|
+
c += (d & b | a & ~b) + k[7] + 1735328473 | 0;
|
|
10334
|
+
c = (c << 14 | c >>> 18) + d | 0;
|
|
10335
|
+
b += (c & a | d & ~a) + k[12] - 1926607734 | 0;
|
|
10336
|
+
b = (b << 20 | b >>> 12) + c | 0;
|
|
10337
|
+
|
|
10338
|
+
a += (b ^ c ^ d) + k[5] - 378558 | 0;
|
|
10339
|
+
a = (a << 4 | a >>> 28) + b | 0;
|
|
10340
|
+
d += (a ^ b ^ c) + k[8] - 2022574463 | 0;
|
|
10341
|
+
d = (d << 11 | d >>> 21) + a | 0;
|
|
10342
|
+
c += (d ^ a ^ b) + k[11] + 1839030562 | 0;
|
|
10343
|
+
c = (c << 16 | c >>> 16) + d | 0;
|
|
10344
|
+
b += (c ^ d ^ a) + k[14] - 35309556 | 0;
|
|
10345
|
+
b = (b << 23 | b >>> 9) + c | 0;
|
|
10346
|
+
a += (b ^ c ^ d) + k[1] - 1530992060 | 0;
|
|
10347
|
+
a = (a << 4 | a >>> 28) + b | 0;
|
|
10348
|
+
d += (a ^ b ^ c) + k[4] + 1272893353 | 0;
|
|
10349
|
+
d = (d << 11 | d >>> 21) + a | 0;
|
|
10350
|
+
c += (d ^ a ^ b) + k[7] - 155497632 | 0;
|
|
10351
|
+
c = (c << 16 | c >>> 16) + d | 0;
|
|
10352
|
+
b += (c ^ d ^ a) + k[10] - 1094730640 | 0;
|
|
10353
|
+
b = (b << 23 | b >>> 9) + c | 0;
|
|
10354
|
+
a += (b ^ c ^ d) + k[13] + 681279174 | 0;
|
|
10355
|
+
a = (a << 4 | a >>> 28) + b | 0;
|
|
10356
|
+
d += (a ^ b ^ c) + k[0] - 358537222 | 0;
|
|
10357
|
+
d = (d << 11 | d >>> 21) + a | 0;
|
|
10358
|
+
c += (d ^ a ^ b) + k[3] - 722521979 | 0;
|
|
10359
|
+
c = (c << 16 | c >>> 16) + d | 0;
|
|
10360
|
+
b += (c ^ d ^ a) + k[6] + 76029189 | 0;
|
|
10361
|
+
b = (b << 23 | b >>> 9) + c | 0;
|
|
10362
|
+
a += (b ^ c ^ d) + k[9] - 640364487 | 0;
|
|
10363
|
+
a = (a << 4 | a >>> 28) + b | 0;
|
|
10364
|
+
d += (a ^ b ^ c) + k[12] - 421815835 | 0;
|
|
10365
|
+
d = (d << 11 | d >>> 21) + a | 0;
|
|
10366
|
+
c += (d ^ a ^ b) + k[15] + 530742520 | 0;
|
|
10367
|
+
c = (c << 16 | c >>> 16) + d | 0;
|
|
10368
|
+
b += (c ^ d ^ a) + k[2] - 995338651 | 0;
|
|
10369
|
+
b = (b << 23 | b >>> 9) + c | 0;
|
|
10370
|
+
|
|
10371
|
+
a += (c ^ (b | ~d)) + k[0] - 198630844 | 0;
|
|
10372
|
+
a = (a << 6 | a >>> 26) + b | 0;
|
|
10373
|
+
d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0;
|
|
10374
|
+
d = (d << 10 | d >>> 22) + a | 0;
|
|
10375
|
+
c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0;
|
|
10376
|
+
c = (c << 15 | c >>> 17) + d | 0;
|
|
10377
|
+
b += (d ^ (c | ~a)) + k[5] - 57434055 | 0;
|
|
10378
|
+
b = (b << 21 |b >>> 11) + c | 0;
|
|
10379
|
+
a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0;
|
|
10380
|
+
a = (a << 6 | a >>> 26) + b | 0;
|
|
10381
|
+
d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0;
|
|
10382
|
+
d = (d << 10 | d >>> 22) + a | 0;
|
|
10383
|
+
c += (a ^ (d | ~b)) + k[10] - 1051523 | 0;
|
|
10384
|
+
c = (c << 15 | c >>> 17) + d | 0;
|
|
10385
|
+
b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0;
|
|
10386
|
+
b = (b << 21 |b >>> 11) + c | 0;
|
|
10387
|
+
a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0;
|
|
10388
|
+
a = (a << 6 | a >>> 26) + b | 0;
|
|
10389
|
+
d += (b ^ (a | ~c)) + k[15] - 30611744 | 0;
|
|
10390
|
+
d = (d << 10 | d >>> 22) + a | 0;
|
|
10391
|
+
c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0;
|
|
10392
|
+
c = (c << 15 | c >>> 17) + d | 0;
|
|
10393
|
+
b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0;
|
|
10394
|
+
b = (b << 21 |b >>> 11) + c | 0;
|
|
10395
|
+
a += (c ^ (b | ~d)) + k[4] - 145523070 | 0;
|
|
10396
|
+
a = (a << 6 | a >>> 26) + b | 0;
|
|
10397
|
+
d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0;
|
|
10398
|
+
d = (d << 10 | d >>> 22) + a | 0;
|
|
10399
|
+
c += (a ^ (d | ~b)) + k[2] + 718787259 | 0;
|
|
10400
|
+
c = (c << 15 | c >>> 17) + d | 0;
|
|
10401
|
+
b += (d ^ (c | ~a)) + k[9] - 343485551 | 0;
|
|
10402
|
+
b = (b << 21 | b >>> 11) + c | 0;
|
|
10403
|
+
|
|
10404
|
+
x[0] = a + x[0] | 0;
|
|
10405
|
+
x[1] = b + x[1] | 0;
|
|
10406
|
+
x[2] = c + x[2] | 0;
|
|
10407
|
+
x[3] = d + x[3] | 0;
|
|
10408
|
+
}
|
|
10409
|
+
|
|
10410
|
+
function md5blk(s) {
|
|
10411
|
+
var md5blks = [],
|
|
10412
|
+
i; /* Andy King said do it this way. */
|
|
10413
|
+
|
|
10414
|
+
for (i = 0; i < 64; i += 4) {
|
|
10415
|
+
md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24);
|
|
10416
|
+
}
|
|
10417
|
+
return md5blks;
|
|
10418
|
+
}
|
|
10419
|
+
|
|
10420
|
+
function md5blk_array(a) {
|
|
10421
|
+
var md5blks = [],
|
|
10422
|
+
i; /* Andy King said do it this way. */
|
|
10423
|
+
|
|
10424
|
+
for (i = 0; i < 64; i += 4) {
|
|
10425
|
+
md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24);
|
|
10426
|
+
}
|
|
10427
|
+
return md5blks;
|
|
10428
|
+
}
|
|
10429
|
+
|
|
10430
|
+
function md51(s) {
|
|
10431
|
+
var n = s.length,
|
|
10432
|
+
state = [1732584193, -271733879, -1732584194, 271733878],
|
|
10433
|
+
i,
|
|
10434
|
+
length,
|
|
10435
|
+
tail,
|
|
10436
|
+
tmp,
|
|
10437
|
+
lo,
|
|
10438
|
+
hi;
|
|
10439
|
+
|
|
10440
|
+
for (i = 64; i <= n; i += 64) {
|
|
10441
|
+
md5cycle(state, md5blk(s.substring(i - 64, i)));
|
|
10442
|
+
}
|
|
10443
|
+
s = s.substring(i - 64);
|
|
10444
|
+
length = s.length;
|
|
10445
|
+
tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
10446
|
+
for (i = 0; i < length; i += 1) {
|
|
10447
|
+
tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3);
|
|
10448
|
+
}
|
|
10449
|
+
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
|
10450
|
+
if (i > 55) {
|
|
10451
|
+
md5cycle(state, tail);
|
|
10452
|
+
for (i = 0; i < 16; i += 1) {
|
|
10453
|
+
tail[i] = 0;
|
|
10454
|
+
}
|
|
10455
|
+
}
|
|
10456
|
+
|
|
10457
|
+
// Beware that the final length might not fit in 32 bits so we take care of that
|
|
10458
|
+
tmp = n * 8;
|
|
10459
|
+
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
|
10460
|
+
lo = parseInt(tmp[2], 16);
|
|
10461
|
+
hi = parseInt(tmp[1], 16) || 0;
|
|
10462
|
+
|
|
10463
|
+
tail[14] = lo;
|
|
10464
|
+
tail[15] = hi;
|
|
10465
|
+
|
|
10466
|
+
md5cycle(state, tail);
|
|
10467
|
+
return state;
|
|
10468
|
+
}
|
|
10469
|
+
|
|
10470
|
+
function md51_array(a) {
|
|
10471
|
+
var n = a.length,
|
|
10472
|
+
state = [1732584193, -271733879, -1732584194, 271733878],
|
|
10473
|
+
i,
|
|
10474
|
+
length,
|
|
10475
|
+
tail,
|
|
10476
|
+
tmp,
|
|
10477
|
+
lo,
|
|
10478
|
+
hi;
|
|
10479
|
+
|
|
10480
|
+
for (i = 64; i <= n; i += 64) {
|
|
10481
|
+
md5cycle(state, md5blk_array(a.subarray(i - 64, i)));
|
|
10482
|
+
}
|
|
10483
|
+
|
|
10484
|
+
// Not sure if it is a bug, however IE10 will always produce a sub array of length 1
|
|
10485
|
+
// containing the last element of the parent array if the sub array specified starts
|
|
10486
|
+
// beyond the length of the parent array - weird.
|
|
10487
|
+
// https://connect.microsoft.com/IE/feedback/details/771452/typed-array-subarray-issue
|
|
10488
|
+
a = (i - 64) < n ? a.subarray(i - 64) : new Uint8Array(0);
|
|
10489
|
+
|
|
10490
|
+
length = a.length;
|
|
10491
|
+
tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
|
10492
|
+
for (i = 0; i < length; i += 1) {
|
|
10493
|
+
tail[i >> 2] |= a[i] << ((i % 4) << 3);
|
|
10494
|
+
}
|
|
10495
|
+
|
|
10496
|
+
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
|
10497
|
+
if (i > 55) {
|
|
10498
|
+
md5cycle(state, tail);
|
|
10499
|
+
for (i = 0; i < 16; i += 1) {
|
|
10500
|
+
tail[i] = 0;
|
|
10501
|
+
}
|
|
10502
|
+
}
|
|
10503
|
+
|
|
10504
|
+
// Beware that the final length might not fit in 32 bits so we take care of that
|
|
10505
|
+
tmp = n * 8;
|
|
10506
|
+
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
|
10507
|
+
lo = parseInt(tmp[2], 16);
|
|
10508
|
+
hi = parseInt(tmp[1], 16) || 0;
|
|
10509
|
+
|
|
10510
|
+
tail[14] = lo;
|
|
10511
|
+
tail[15] = hi;
|
|
10512
|
+
|
|
10513
|
+
md5cycle(state, tail);
|
|
10514
|
+
|
|
10515
|
+
return state;
|
|
10516
|
+
}
|
|
10517
|
+
|
|
10518
|
+
function rhex(n) {
|
|
10519
|
+
var s = '',
|
|
10520
|
+
j;
|
|
10521
|
+
for (j = 0; j < 4; j += 1) {
|
|
10522
|
+
s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F];
|
|
10523
|
+
}
|
|
10524
|
+
return s;
|
|
10525
|
+
}
|
|
10526
|
+
|
|
10527
|
+
function hex(x) {
|
|
10528
|
+
var i;
|
|
10529
|
+
for (i = 0; i < x.length; i += 1) {
|
|
10530
|
+
x[i] = rhex(x[i]);
|
|
10531
|
+
}
|
|
10532
|
+
return x.join('');
|
|
10533
|
+
}
|
|
10534
|
+
|
|
10535
|
+
// In some cases the fast add32 function cannot be used..
|
|
10536
|
+
if (hex(md51('hello')) !== '5d41402abc4b2a76b9719d911017c592') ;
|
|
10537
|
+
|
|
10538
|
+
// ---------------------------------------------------
|
|
10539
|
+
|
|
10540
|
+
/**
|
|
10541
|
+
* ArrayBuffer slice polyfill.
|
|
10542
|
+
*
|
|
10543
|
+
* @see https://github.com/ttaubert/node-arraybuffer-slice
|
|
10544
|
+
*/
|
|
10545
|
+
|
|
10546
|
+
if (typeof ArrayBuffer !== 'undefined' && !ArrayBuffer.prototype.slice) {
|
|
10547
|
+
(function () {
|
|
10548
|
+
function clamp(val, length) {
|
|
10549
|
+
val = (val | 0) || 0;
|
|
10550
|
+
|
|
10551
|
+
if (val < 0) {
|
|
10552
|
+
return Math.max(val + length, 0);
|
|
10553
|
+
}
|
|
10554
|
+
|
|
10555
|
+
return Math.min(val, length);
|
|
10556
|
+
}
|
|
10557
|
+
|
|
10558
|
+
ArrayBuffer.prototype.slice = function (from, to) {
|
|
10559
|
+
var length = this.byteLength,
|
|
10560
|
+
begin = clamp(from, length),
|
|
10561
|
+
end = length,
|
|
10562
|
+
num,
|
|
10563
|
+
target,
|
|
10564
|
+
targetArray,
|
|
10565
|
+
sourceArray;
|
|
10566
|
+
|
|
10567
|
+
if (to !== undefined$1) {
|
|
10568
|
+
end = clamp(to, length);
|
|
10569
|
+
}
|
|
10570
|
+
|
|
10571
|
+
if (begin > end) {
|
|
10572
|
+
return new ArrayBuffer(0);
|
|
10573
|
+
}
|
|
10574
|
+
|
|
10575
|
+
num = end - begin;
|
|
10576
|
+
target = new ArrayBuffer(num);
|
|
10577
|
+
targetArray = new Uint8Array(target);
|
|
10578
|
+
|
|
10579
|
+
sourceArray = new Uint8Array(this, begin, num);
|
|
10580
|
+
targetArray.set(sourceArray);
|
|
10581
|
+
|
|
10582
|
+
return target;
|
|
10583
|
+
};
|
|
10584
|
+
})();
|
|
10585
|
+
}
|
|
10586
|
+
|
|
10587
|
+
// ---------------------------------------------------
|
|
10588
|
+
|
|
10589
|
+
/**
|
|
10590
|
+
* Helpers.
|
|
10591
|
+
*/
|
|
10592
|
+
|
|
10593
|
+
function toUtf8(str) {
|
|
10594
|
+
if (/[\u0080-\uFFFF]/.test(str)) {
|
|
10595
|
+
str = unescape(encodeURIComponent(str));
|
|
10596
|
+
}
|
|
10597
|
+
|
|
10598
|
+
return str;
|
|
10599
|
+
}
|
|
10600
|
+
|
|
10601
|
+
function utf8Str2ArrayBuffer(str, returnUInt8Array) {
|
|
10602
|
+
var length = str.length,
|
|
10603
|
+
buff = new ArrayBuffer(length),
|
|
10604
|
+
arr = new Uint8Array(buff),
|
|
10605
|
+
i;
|
|
10606
|
+
|
|
10607
|
+
for (i = 0; i < length; i += 1) {
|
|
10608
|
+
arr[i] = str.charCodeAt(i);
|
|
10609
|
+
}
|
|
10610
|
+
|
|
10611
|
+
return returnUInt8Array ? arr : buff;
|
|
10612
|
+
}
|
|
10613
|
+
|
|
10614
|
+
function arrayBuffer2Utf8Str(buff) {
|
|
10615
|
+
return String.fromCharCode.apply(null, new Uint8Array(buff));
|
|
10616
|
+
}
|
|
10617
|
+
|
|
10618
|
+
function concatenateArrayBuffers(first, second, returnUInt8Array) {
|
|
10619
|
+
var result = new Uint8Array(first.byteLength + second.byteLength);
|
|
10620
|
+
|
|
10621
|
+
result.set(new Uint8Array(first));
|
|
10622
|
+
result.set(new Uint8Array(second), first.byteLength);
|
|
10623
|
+
|
|
10624
|
+
return result ;
|
|
10625
|
+
}
|
|
10626
|
+
|
|
10627
|
+
function hexToBinaryString(hex) {
|
|
10628
|
+
var bytes = [],
|
|
10629
|
+
length = hex.length,
|
|
10630
|
+
x;
|
|
10631
|
+
|
|
10632
|
+
for (x = 0; x < length - 1; x += 2) {
|
|
10633
|
+
bytes.push(parseInt(hex.substr(x, 2), 16));
|
|
10634
|
+
}
|
|
10635
|
+
|
|
10636
|
+
return String.fromCharCode.apply(String, bytes);
|
|
10637
|
+
}
|
|
10638
|
+
|
|
10639
|
+
// ---------------------------------------------------
|
|
10640
|
+
|
|
10641
|
+
/**
|
|
10642
|
+
* SparkMD5 OOP implementation.
|
|
10643
|
+
*
|
|
10644
|
+
* Use this class to perform an incremental md5, otherwise use the
|
|
10645
|
+
* static methods instead.
|
|
10646
|
+
*/
|
|
10647
|
+
|
|
10648
|
+
function SparkMD5() {
|
|
10649
|
+
// call reset to init the instance
|
|
10650
|
+
this.reset();
|
|
10651
|
+
}
|
|
10652
|
+
|
|
10653
|
+
/**
|
|
10654
|
+
* Appends a string.
|
|
10655
|
+
* A conversion will be applied if an utf8 string is detected.
|
|
10656
|
+
*
|
|
10657
|
+
* @param {String} str The string to be appended
|
|
10658
|
+
*
|
|
10659
|
+
* @return {SparkMD5} The instance itself
|
|
10660
|
+
*/
|
|
10661
|
+
SparkMD5.prototype.append = function (str) {
|
|
10662
|
+
// Converts the string to utf8 bytes if necessary
|
|
10663
|
+
// Then append as binary
|
|
10664
|
+
this.appendBinary(toUtf8(str));
|
|
10665
|
+
|
|
10666
|
+
return this;
|
|
10667
|
+
};
|
|
10668
|
+
|
|
10669
|
+
/**
|
|
10670
|
+
* Appends a binary string.
|
|
10671
|
+
*
|
|
10672
|
+
* @param {String} contents The binary string to be appended
|
|
10673
|
+
*
|
|
10674
|
+
* @return {SparkMD5} The instance itself
|
|
10675
|
+
*/
|
|
10676
|
+
SparkMD5.prototype.appendBinary = function (contents) {
|
|
10677
|
+
this._buff += contents;
|
|
10678
|
+
this._length += contents.length;
|
|
10679
|
+
|
|
10680
|
+
var length = this._buff.length,
|
|
10681
|
+
i;
|
|
10682
|
+
|
|
10683
|
+
for (i = 64; i <= length; i += 64) {
|
|
10684
|
+
md5cycle(this._hash, md5blk(this._buff.substring(i - 64, i)));
|
|
10685
|
+
}
|
|
10686
|
+
|
|
10687
|
+
this._buff = this._buff.substring(i - 64);
|
|
10688
|
+
|
|
10689
|
+
return this;
|
|
10690
|
+
};
|
|
10691
|
+
|
|
10692
|
+
/**
|
|
10693
|
+
* Finishes the incremental computation, reseting the internal state and
|
|
10694
|
+
* returning the result.
|
|
10695
|
+
*
|
|
10696
|
+
* @param {Boolean} raw True to get the raw string, false to get the hex string
|
|
10697
|
+
*
|
|
10698
|
+
* @return {String} The result
|
|
10699
|
+
*/
|
|
10700
|
+
SparkMD5.prototype.end = function (raw) {
|
|
10701
|
+
var buff = this._buff,
|
|
10702
|
+
length = buff.length,
|
|
10703
|
+
i,
|
|
10704
|
+
tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
10705
|
+
ret;
|
|
10706
|
+
|
|
10707
|
+
for (i = 0; i < length; i += 1) {
|
|
10708
|
+
tail[i >> 2] |= buff.charCodeAt(i) << ((i % 4) << 3);
|
|
10709
|
+
}
|
|
10710
|
+
|
|
10711
|
+
this._finish(tail, length);
|
|
10712
|
+
ret = hex(this._hash);
|
|
10713
|
+
|
|
10714
|
+
if (raw) {
|
|
10715
|
+
ret = hexToBinaryString(ret);
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10718
|
+
this.reset();
|
|
10719
|
+
|
|
10720
|
+
return ret;
|
|
10721
|
+
};
|
|
10722
|
+
|
|
10723
|
+
/**
|
|
10724
|
+
* Resets the internal state of the computation.
|
|
10725
|
+
*
|
|
10726
|
+
* @return {SparkMD5} The instance itself
|
|
10727
|
+
*/
|
|
10728
|
+
SparkMD5.prototype.reset = function () {
|
|
10729
|
+
this._buff = '';
|
|
10730
|
+
this._length = 0;
|
|
10731
|
+
this._hash = [1732584193, -271733879, -1732584194, 271733878];
|
|
10732
|
+
|
|
10733
|
+
return this;
|
|
10734
|
+
};
|
|
10735
|
+
|
|
10736
|
+
/**
|
|
10737
|
+
* Gets the internal state of the computation.
|
|
10738
|
+
*
|
|
10739
|
+
* @return {Object} The state
|
|
10740
|
+
*/
|
|
10741
|
+
SparkMD5.prototype.getState = function () {
|
|
10742
|
+
return {
|
|
10743
|
+
buff: this._buff,
|
|
10744
|
+
length: this._length,
|
|
10745
|
+
hash: this._hash.slice()
|
|
10746
|
+
};
|
|
10747
|
+
};
|
|
10748
|
+
|
|
10749
|
+
/**
|
|
10750
|
+
* Gets the internal state of the computation.
|
|
10751
|
+
*
|
|
10752
|
+
* @param {Object} state The state
|
|
10753
|
+
*
|
|
10754
|
+
* @return {SparkMD5} The instance itself
|
|
10755
|
+
*/
|
|
10756
|
+
SparkMD5.prototype.setState = function (state) {
|
|
10757
|
+
this._buff = state.buff;
|
|
10758
|
+
this._length = state.length;
|
|
10759
|
+
this._hash = state.hash;
|
|
10760
|
+
|
|
10761
|
+
return this;
|
|
10762
|
+
};
|
|
10763
|
+
|
|
10764
|
+
/**
|
|
10765
|
+
* Releases memory used by the incremental buffer and other additional
|
|
10766
|
+
* resources. If you plan to use the instance again, use reset instead.
|
|
10767
|
+
*/
|
|
10768
|
+
SparkMD5.prototype.destroy = function () {
|
|
10769
|
+
delete this._hash;
|
|
10770
|
+
delete this._buff;
|
|
10771
|
+
delete this._length;
|
|
10772
|
+
};
|
|
10773
|
+
|
|
10774
|
+
/**
|
|
10775
|
+
* Finish the final calculation based on the tail.
|
|
10776
|
+
*
|
|
10777
|
+
* @param {Array} tail The tail (will be modified)
|
|
10778
|
+
* @param {Number} length The length of the remaining buffer
|
|
10779
|
+
*/
|
|
10780
|
+
SparkMD5.prototype._finish = function (tail, length) {
|
|
10781
|
+
var i = length,
|
|
10782
|
+
tmp,
|
|
10783
|
+
lo,
|
|
10784
|
+
hi;
|
|
10785
|
+
|
|
10786
|
+
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
|
10787
|
+
if (i > 55) {
|
|
10788
|
+
md5cycle(this._hash, tail);
|
|
10789
|
+
for (i = 0; i < 16; i += 1) {
|
|
10790
|
+
tail[i] = 0;
|
|
10791
|
+
}
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10794
|
+
// Do the final computation based on the tail and length
|
|
10795
|
+
// Beware that the final length may not fit in 32 bits so we take care of that
|
|
10796
|
+
tmp = this._length * 8;
|
|
10797
|
+
tmp = tmp.toString(16).match(/(.*?)(.{0,8})$/);
|
|
10798
|
+
lo = parseInt(tmp[2], 16);
|
|
10799
|
+
hi = parseInt(tmp[1], 16) || 0;
|
|
10800
|
+
|
|
10801
|
+
tail[14] = lo;
|
|
10802
|
+
tail[15] = hi;
|
|
10803
|
+
md5cycle(this._hash, tail);
|
|
10804
|
+
};
|
|
10805
|
+
|
|
10806
|
+
/**
|
|
10807
|
+
* Performs the md5 hash on a string.
|
|
10808
|
+
* A conversion will be applied if utf8 string is detected.
|
|
10809
|
+
*
|
|
10810
|
+
* @param {String} str The string
|
|
10811
|
+
* @param {Boolean} [raw] True to get the raw string, false to get the hex string
|
|
10812
|
+
*
|
|
10813
|
+
* @return {String} The result
|
|
10814
|
+
*/
|
|
10815
|
+
SparkMD5.hash = function (str, raw) {
|
|
10816
|
+
// Converts the string to utf8 bytes if necessary
|
|
10817
|
+
// Then compute it using the binary function
|
|
10818
|
+
return SparkMD5.hashBinary(toUtf8(str), raw);
|
|
10819
|
+
};
|
|
10820
|
+
|
|
10821
|
+
/**
|
|
10822
|
+
* Performs the md5 hash on a binary string.
|
|
10823
|
+
*
|
|
10824
|
+
* @param {String} content The binary string
|
|
10825
|
+
* @param {Boolean} [raw] True to get the raw string, false to get the hex string
|
|
10826
|
+
*
|
|
10827
|
+
* @return {String} The result
|
|
10828
|
+
*/
|
|
10829
|
+
SparkMD5.hashBinary = function (content, raw) {
|
|
10830
|
+
var hash = md51(content),
|
|
10831
|
+
ret = hex(hash);
|
|
10832
|
+
|
|
10833
|
+
return raw ? hexToBinaryString(ret) : ret;
|
|
10834
|
+
};
|
|
10835
|
+
|
|
10836
|
+
// ---------------------------------------------------
|
|
10837
|
+
|
|
10838
|
+
/**
|
|
10839
|
+
* SparkMD5 OOP implementation for array buffers.
|
|
10840
|
+
*
|
|
10841
|
+
* Use this class to perform an incremental md5 ONLY for array buffers.
|
|
10842
|
+
*/
|
|
10843
|
+
SparkMD5.ArrayBuffer = function () {
|
|
10844
|
+
// call reset to init the instance
|
|
10845
|
+
this.reset();
|
|
10846
|
+
};
|
|
10847
|
+
|
|
10848
|
+
/**
|
|
10849
|
+
* Appends an array buffer.
|
|
10850
|
+
*
|
|
10851
|
+
* @param {ArrayBuffer} arr The array to be appended
|
|
10852
|
+
*
|
|
10853
|
+
* @return {SparkMD5.ArrayBuffer} The instance itself
|
|
10854
|
+
*/
|
|
10855
|
+
SparkMD5.ArrayBuffer.prototype.append = function (arr) {
|
|
10856
|
+
var buff = concatenateArrayBuffers(this._buff.buffer, arr),
|
|
10857
|
+
length = buff.length,
|
|
10858
|
+
i;
|
|
10859
|
+
|
|
10860
|
+
this._length += arr.byteLength;
|
|
10861
|
+
|
|
10862
|
+
for (i = 64; i <= length; i += 64) {
|
|
10863
|
+
md5cycle(this._hash, md5blk_array(buff.subarray(i - 64, i)));
|
|
10864
|
+
}
|
|
10865
|
+
|
|
10866
|
+
this._buff = (i - 64) < length ? new Uint8Array(buff.buffer.slice(i - 64)) : new Uint8Array(0);
|
|
10867
|
+
|
|
10868
|
+
return this;
|
|
10869
|
+
};
|
|
10870
|
+
|
|
10871
|
+
/**
|
|
10872
|
+
* Finishes the incremental computation, reseting the internal state and
|
|
10873
|
+
* returning the result.
|
|
10874
|
+
*
|
|
10875
|
+
* @param {Boolean} raw True to get the raw string, false to get the hex string
|
|
10876
|
+
*
|
|
10877
|
+
* @return {String} The result
|
|
10878
|
+
*/
|
|
10879
|
+
SparkMD5.ArrayBuffer.prototype.end = function (raw) {
|
|
10880
|
+
var buff = this._buff,
|
|
10881
|
+
length = buff.length,
|
|
10882
|
+
tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
10883
|
+
i,
|
|
10884
|
+
ret;
|
|
10885
|
+
|
|
10886
|
+
for (i = 0; i < length; i += 1) {
|
|
10887
|
+
tail[i >> 2] |= buff[i] << ((i % 4) << 3);
|
|
10888
|
+
}
|
|
10889
|
+
|
|
10890
|
+
this._finish(tail, length);
|
|
10891
|
+
ret = hex(this._hash);
|
|
10892
|
+
|
|
10893
|
+
if (raw) {
|
|
10894
|
+
ret = hexToBinaryString(ret);
|
|
10895
|
+
}
|
|
10896
|
+
|
|
10897
|
+
this.reset();
|
|
10898
|
+
|
|
10899
|
+
return ret;
|
|
10900
|
+
};
|
|
10901
|
+
|
|
10902
|
+
/**
|
|
10903
|
+
* Resets the internal state of the computation.
|
|
10904
|
+
*
|
|
10905
|
+
* @return {SparkMD5.ArrayBuffer} The instance itself
|
|
10906
|
+
*/
|
|
10907
|
+
SparkMD5.ArrayBuffer.prototype.reset = function () {
|
|
10908
|
+
this._buff = new Uint8Array(0);
|
|
10909
|
+
this._length = 0;
|
|
10910
|
+
this._hash = [1732584193, -271733879, -1732584194, 271733878];
|
|
10911
|
+
|
|
10912
|
+
return this;
|
|
10913
|
+
};
|
|
10914
|
+
|
|
10915
|
+
/**
|
|
10916
|
+
* Gets the internal state of the computation.
|
|
10917
|
+
*
|
|
10918
|
+
* @return {Object} The state
|
|
10919
|
+
*/
|
|
10920
|
+
SparkMD5.ArrayBuffer.prototype.getState = function () {
|
|
10921
|
+
var state = SparkMD5.prototype.getState.call(this);
|
|
10922
|
+
|
|
10923
|
+
// Convert buffer to a string
|
|
10924
|
+
state.buff = arrayBuffer2Utf8Str(state.buff);
|
|
10925
|
+
|
|
10926
|
+
return state;
|
|
10927
|
+
};
|
|
10928
|
+
|
|
10929
|
+
/**
|
|
10930
|
+
* Gets the internal state of the computation.
|
|
10931
|
+
*
|
|
10932
|
+
* @param {Object} state The state
|
|
10933
|
+
*
|
|
10934
|
+
* @return {SparkMD5.ArrayBuffer} The instance itself
|
|
10935
|
+
*/
|
|
10936
|
+
SparkMD5.ArrayBuffer.prototype.setState = function (state) {
|
|
10937
|
+
// Convert string to buffer
|
|
10938
|
+
state.buff = utf8Str2ArrayBuffer(state.buff, true);
|
|
10939
|
+
|
|
10940
|
+
return SparkMD5.prototype.setState.call(this, state);
|
|
10941
|
+
};
|
|
10942
|
+
|
|
10943
|
+
SparkMD5.ArrayBuffer.prototype.destroy = SparkMD5.prototype.destroy;
|
|
10944
|
+
|
|
10945
|
+
SparkMD5.ArrayBuffer.prototype._finish = SparkMD5.prototype._finish;
|
|
10946
|
+
|
|
10947
|
+
/**
|
|
10948
|
+
* Performs the md5 hash on an array buffer.
|
|
10949
|
+
*
|
|
10950
|
+
* @param {ArrayBuffer} arr The array buffer
|
|
10951
|
+
* @param {Boolean} [raw] True to get the raw string, false to get the hex one
|
|
10952
|
+
*
|
|
10953
|
+
* @return {String} The result
|
|
10954
|
+
*/
|
|
10955
|
+
SparkMD5.ArrayBuffer.hash = function (arr, raw) {
|
|
10956
|
+
var hash = md51_array(new Uint8Array(arr)),
|
|
10957
|
+
ret = hex(hash);
|
|
10958
|
+
|
|
10959
|
+
return raw ? hexToBinaryString(ret) : ret;
|
|
10960
|
+
};
|
|
10961
|
+
|
|
10962
|
+
return SparkMD5;
|
|
10963
|
+
}));
|
|
10964
|
+
} (sparkMd5));
|
|
10965
|
+
|
|
10966
|
+
function isBinaryObject(object) {
|
|
10967
|
+
return (typeof ArrayBuffer !== 'undefined' && object instanceof ArrayBuffer) ||
|
|
10968
|
+
(typeof Blob !== 'undefined' && object instanceof Blob);
|
|
10969
|
+
}
|
|
10970
|
+
|
|
10971
|
+
/**
|
|
10972
|
+
* @template {ArrayBuffer | Blob} T
|
|
10973
|
+
* @param {T} object
|
|
10974
|
+
* @returns {T}
|
|
10975
|
+
*/
|
|
10976
|
+
function cloneBinaryObject(object) {
|
|
10977
|
+
return object instanceof ArrayBuffer
|
|
10978
|
+
? object.slice(0)
|
|
10979
|
+
: object.slice(0, object.size, object.type);
|
|
10980
|
+
}
|
|
10981
|
+
|
|
10982
|
+
// most of this is borrowed from lodash.isPlainObject:
|
|
10983
|
+
// https://github.com/fis-components/lodash.isplainobject/
|
|
10984
|
+
// blob/29c358140a74f252aeb08c9eb28bef86f2217d4a/index.js
|
|
10985
|
+
|
|
10986
|
+
var funcToString = Function.prototype.toString;
|
|
10987
|
+
var objectCtorString = funcToString.call(Object);
|
|
10988
|
+
|
|
10989
|
+
function isPlainObject(value) {
|
|
10990
|
+
var proto = Object.getPrototypeOf(value);
|
|
10991
|
+
/* istanbul ignore if */
|
|
10992
|
+
if (proto === null) { // not sure when this happens, but I guess it can
|
|
10993
|
+
return true;
|
|
10994
|
+
}
|
|
10995
|
+
var Ctor = proto.constructor;
|
|
10996
|
+
return (typeof Ctor == 'function' &&
|
|
10997
|
+
Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString);
|
|
10998
|
+
}
|
|
10999
|
+
|
|
11000
|
+
function clone(object) {
|
|
11001
|
+
var newObject;
|
|
11002
|
+
var i;
|
|
11003
|
+
var len;
|
|
11004
|
+
|
|
11005
|
+
if (!object || typeof object !== 'object') {
|
|
11006
|
+
return object;
|
|
11007
|
+
}
|
|
11008
|
+
|
|
11009
|
+
if (Array.isArray(object)) {
|
|
11010
|
+
newObject = [];
|
|
11011
|
+
for (i = 0, len = object.length; i < len; i++) {
|
|
11012
|
+
newObject[i] = clone(object[i]);
|
|
11013
|
+
}
|
|
11014
|
+
return newObject;
|
|
11015
|
+
}
|
|
11016
|
+
|
|
11017
|
+
// special case: to avoid inconsistencies between IndexedDB
|
|
11018
|
+
// and other backends, we automatically stringify Dates
|
|
11019
|
+
if (object instanceof Date && isFinite(object)) {
|
|
11020
|
+
return object.toISOString();
|
|
11021
|
+
}
|
|
11022
|
+
|
|
11023
|
+
if (isBinaryObject(object)) {
|
|
11024
|
+
return cloneBinaryObject(object);
|
|
11025
|
+
}
|
|
11026
|
+
|
|
11027
|
+
if (!isPlainObject(object)) {
|
|
11028
|
+
return object; // don't clone objects like Workers
|
|
11029
|
+
}
|
|
11030
|
+
|
|
11031
|
+
newObject = {};
|
|
11032
|
+
for (i in object) {
|
|
11033
|
+
/* istanbul ignore else */
|
|
11034
|
+
if (Object.prototype.hasOwnProperty.call(object, i)) {
|
|
11035
|
+
var value = clone(object[i]);
|
|
11036
|
+
if (typeof value !== 'undefined') {
|
|
11037
|
+
newObject[i] = value;
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
}
|
|
11041
|
+
return newObject;
|
|
11042
|
+
}
|
|
11043
|
+
|
|
11044
|
+
var hasLocal;
|
|
11045
|
+
|
|
11046
|
+
try {
|
|
11047
|
+
localStorage.setItem('_pouch_check_localstorage', 1);
|
|
11048
|
+
hasLocal = !!localStorage.getItem('_pouch_check_localstorage');
|
|
11049
|
+
} catch (e) {
|
|
11050
|
+
hasLocal = false;
|
|
11051
|
+
}
|
|
11052
|
+
|
|
11053
|
+
function collate(a, b) {
|
|
11054
|
+
|
|
11055
|
+
if (a === b) {
|
|
11056
|
+
return 0;
|
|
11057
|
+
}
|
|
11058
|
+
|
|
11059
|
+
a = normalizeKey(a);
|
|
11060
|
+
b = normalizeKey(b);
|
|
11061
|
+
|
|
11062
|
+
var ai = collationIndex(a);
|
|
11063
|
+
var bi = collationIndex(b);
|
|
11064
|
+
if ((ai - bi) !== 0) {
|
|
11065
|
+
return ai - bi;
|
|
11066
|
+
}
|
|
11067
|
+
switch (typeof a) {
|
|
11068
|
+
case 'number':
|
|
11069
|
+
return a - b;
|
|
11070
|
+
case 'boolean':
|
|
11071
|
+
return a < b ? -1 : 1;
|
|
11072
|
+
case 'string':
|
|
11073
|
+
return stringCollate(a, b);
|
|
11074
|
+
}
|
|
11075
|
+
return Array.isArray(a) ? arrayCollate(a, b) : objectCollate(a, b);
|
|
11076
|
+
}
|
|
11077
|
+
|
|
11078
|
+
// couch considers null/NaN/Infinity/-Infinity === undefined,
|
|
11079
|
+
// for the purposes of mapreduce indexes. also, dates get stringified.
|
|
11080
|
+
function normalizeKey(key) {
|
|
11081
|
+
switch (typeof key) {
|
|
11082
|
+
case 'undefined':
|
|
11083
|
+
return null;
|
|
11084
|
+
case 'number':
|
|
11085
|
+
if (key === Infinity || key === -Infinity || isNaN(key)) {
|
|
11086
|
+
return null;
|
|
11087
|
+
}
|
|
11088
|
+
return key;
|
|
11089
|
+
case 'object':
|
|
11090
|
+
var origKey = key;
|
|
11091
|
+
if (Array.isArray(key)) {
|
|
11092
|
+
var len = key.length;
|
|
11093
|
+
key = new Array(len);
|
|
11094
|
+
for (var i = 0; i < len; i++) {
|
|
11095
|
+
key[i] = normalizeKey(origKey[i]);
|
|
11096
|
+
}
|
|
11097
|
+
/* istanbul ignore next */
|
|
11098
|
+
} else if (key instanceof Date) {
|
|
11099
|
+
return key.toJSON();
|
|
11100
|
+
} else if (key !== null) { // generic object
|
|
11101
|
+
key = {};
|
|
11102
|
+
for (var k in origKey) {
|
|
11103
|
+
if (Object.prototype.hasOwnProperty.call(origKey, k)) {
|
|
11104
|
+
var val = origKey[k];
|
|
11105
|
+
if (typeof val !== 'undefined') {
|
|
11106
|
+
key[k] = normalizeKey(val);
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
}
|
|
11111
|
+
}
|
|
11112
|
+
return key;
|
|
11113
|
+
}
|
|
11114
|
+
|
|
11115
|
+
function arrayCollate(a, b) {
|
|
11116
|
+
var len = Math.min(a.length, b.length);
|
|
11117
|
+
for (var i = 0; i < len; i++) {
|
|
11118
|
+
var sort = collate(a[i], b[i]);
|
|
11119
|
+
if (sort !== 0) {
|
|
11120
|
+
return sort;
|
|
11121
|
+
}
|
|
11122
|
+
}
|
|
11123
|
+
return (a.length === b.length) ? 0 :
|
|
11124
|
+
(a.length > b.length) ? 1 : -1;
|
|
11125
|
+
}
|
|
11126
|
+
function stringCollate(a, b) {
|
|
11127
|
+
// See: https://github.com/daleharvey/pouchdb/issues/40
|
|
11128
|
+
// This is incompatible with the CouchDB implementation, but its the
|
|
11129
|
+
// best we can do for now
|
|
11130
|
+
return (a === b) ? 0 : ((a > b) ? 1 : -1);
|
|
11131
|
+
}
|
|
11132
|
+
function objectCollate(a, b) {
|
|
11133
|
+
var ak = Object.keys(a), bk = Object.keys(b);
|
|
11134
|
+
var len = Math.min(ak.length, bk.length);
|
|
11135
|
+
for (var i = 0; i < len; i++) {
|
|
11136
|
+
// First sort the keys
|
|
11137
|
+
var sort = collate(ak[i], bk[i]);
|
|
11138
|
+
if (sort !== 0) {
|
|
11139
|
+
return sort;
|
|
11140
|
+
}
|
|
11141
|
+
// if the keys are equal sort the values
|
|
11142
|
+
sort = collate(a[ak[i]], b[bk[i]]);
|
|
11143
|
+
if (sort !== 0) {
|
|
11144
|
+
return sort;
|
|
11145
|
+
}
|
|
11146
|
+
|
|
11147
|
+
}
|
|
11148
|
+
return (ak.length === bk.length) ? 0 :
|
|
11149
|
+
(ak.length > bk.length) ? 1 : -1;
|
|
11150
|
+
}
|
|
11151
|
+
// The collation is defined by erlangs ordered terms
|
|
11152
|
+
// the atoms null, true, false come first, then numbers, strings,
|
|
11153
|
+
// arrays, then objects
|
|
11154
|
+
// null/undefined/NaN/Infinity/-Infinity are all considered null
|
|
11155
|
+
function collationIndex(x) {
|
|
11156
|
+
var id = ['boolean', 'number', 'string', 'object'];
|
|
11157
|
+
var idx = id.indexOf(typeof x);
|
|
11158
|
+
//false if -1 otherwise true, but fast!!!!1
|
|
11159
|
+
if (~idx) {
|
|
11160
|
+
if (x === null) {
|
|
11161
|
+
return 1;
|
|
11162
|
+
}
|
|
11163
|
+
if (Array.isArray(x)) {
|
|
11164
|
+
return 5;
|
|
11165
|
+
}
|
|
11166
|
+
return idx < 3 ? (idx + 2) : (idx + 3);
|
|
11167
|
+
}
|
|
11168
|
+
/* istanbul ignore next */
|
|
11169
|
+
if (Array.isArray(x)) {
|
|
11170
|
+
return 5;
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
|
|
11174
|
+
// this would just be "return doc[field]", but fields
|
|
11175
|
+
// can be "deep" due to dot notation
|
|
11176
|
+
function getFieldFromDoc(doc, parsedField) {
|
|
11177
|
+
var value = doc;
|
|
11178
|
+
for (var i = 0, len = parsedField.length; i < len; i++) {
|
|
11179
|
+
var key = parsedField[i];
|
|
11180
|
+
value = value[key];
|
|
11181
|
+
if (!value) {
|
|
11182
|
+
break;
|
|
11183
|
+
}
|
|
11184
|
+
}
|
|
11185
|
+
return value;
|
|
11186
|
+
}
|
|
11187
|
+
|
|
11188
|
+
function compare(left, right) {
|
|
11189
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
11190
|
+
}
|
|
11191
|
+
|
|
11192
|
+
// Converts a string in dot notation to an array of its components, with backslash escaping
|
|
11193
|
+
function parseField(fieldName) {
|
|
11194
|
+
// fields may be deep (e.g. "foo.bar.baz"), so parse
|
|
11195
|
+
var fields = [];
|
|
11196
|
+
var current = '';
|
|
11197
|
+
for (var i = 0, len = fieldName.length; i < len; i++) {
|
|
11198
|
+
var ch = fieldName[i];
|
|
11199
|
+
if (i > 0 && fieldName[i - 1] === '\\' && (ch === '$' || ch === '.')) {
|
|
11200
|
+
// escaped delimiter
|
|
11201
|
+
current = current.substring(0, current.length - 1) + ch;
|
|
11202
|
+
} else if (ch === '.') {
|
|
11203
|
+
// When `.` is not escaped (above), it is a field delimiter
|
|
11204
|
+
fields.push(current);
|
|
11205
|
+
current = '';
|
|
11206
|
+
} else { // normal character
|
|
11207
|
+
current += ch;
|
|
11208
|
+
}
|
|
11209
|
+
}
|
|
11210
|
+
fields.push(current);
|
|
11211
|
+
return fields;
|
|
11212
|
+
}
|
|
11213
|
+
|
|
11214
|
+
var combinationFields = ['$or', '$nor', '$not'];
|
|
11215
|
+
function isCombinationalField(field) {
|
|
11216
|
+
return combinationFields.indexOf(field) > -1;
|
|
11217
|
+
}
|
|
11218
|
+
|
|
11219
|
+
function getKey(obj) {
|
|
11220
|
+
return Object.keys(obj)[0];
|
|
11221
|
+
}
|
|
11222
|
+
|
|
11223
|
+
function getValue(obj) {
|
|
11224
|
+
return obj[getKey(obj)];
|
|
11225
|
+
}
|
|
11226
|
+
|
|
11227
|
+
|
|
11228
|
+
// flatten an array of selectors joined by an $and operator
|
|
11229
|
+
function mergeAndedSelectors(selectors) {
|
|
11230
|
+
|
|
11231
|
+
// sort to ensure that e.g. if the user specified
|
|
11232
|
+
// $and: [{$gt: 'a'}, {$gt: 'b'}], then it's collapsed into
|
|
11233
|
+
// just {$gt: 'b'}
|
|
11234
|
+
var res = {};
|
|
11235
|
+
var first = {$or: true, $nor: true};
|
|
11236
|
+
|
|
11237
|
+
selectors.forEach(function (selector) {
|
|
11238
|
+
Object.keys(selector).forEach(function (field) {
|
|
11239
|
+
var matcher = selector[field];
|
|
11240
|
+
if (typeof matcher !== 'object') {
|
|
11241
|
+
matcher = {$eq: matcher};
|
|
11242
|
+
}
|
|
11243
|
+
|
|
11244
|
+
if (isCombinationalField(field)) {
|
|
11245
|
+
// or, nor
|
|
11246
|
+
if (matcher instanceof Array) {
|
|
11247
|
+
if (first[field]) {
|
|
11248
|
+
first[field] = false;
|
|
11249
|
+
res[field] = matcher;
|
|
11250
|
+
return;
|
|
11251
|
+
}
|
|
11252
|
+
|
|
11253
|
+
var entries = [];
|
|
11254
|
+
res[field].forEach(function (existing) {
|
|
11255
|
+
Object.keys(matcher).forEach(function (key) {
|
|
11256
|
+
var m = matcher[key];
|
|
11257
|
+
var longest = Math.max(Object.keys(existing).length, Object.keys(m).length);
|
|
11258
|
+
var merged = mergeAndedSelectors([existing, m]);
|
|
11259
|
+
if (Object.keys(merged).length <= longest) {
|
|
11260
|
+
// we have a situation like: (a :{$eq :1} || ...) && (a {$eq: 2} || ...)
|
|
11261
|
+
// merging would produce a $eq 2 when actually we shouldn't ever match against these merged conditions
|
|
11262
|
+
// merged should always contain more values to be valid
|
|
11263
|
+
return;
|
|
11264
|
+
}
|
|
11265
|
+
entries.push(merged);
|
|
11266
|
+
});
|
|
11267
|
+
});
|
|
11268
|
+
res[field] = entries;
|
|
11269
|
+
} else {
|
|
11270
|
+
// not
|
|
11271
|
+
res[field] = mergeAndedSelectors([matcher]);
|
|
11272
|
+
}
|
|
11273
|
+
} else {
|
|
11274
|
+
var fieldMatchers = res[field] = res[field] || {};
|
|
11275
|
+
Object.keys(matcher).forEach(function (operator) {
|
|
11276
|
+
var value = matcher[operator];
|
|
11277
|
+
|
|
11278
|
+
if (operator === '$gt' || operator === '$gte') {
|
|
11279
|
+
return mergeGtGte(operator, value, fieldMatchers);
|
|
11280
|
+
} else if (operator === '$lt' || operator === '$lte') {
|
|
11281
|
+
return mergeLtLte(operator, value, fieldMatchers);
|
|
11282
|
+
} else if (operator === '$ne') {
|
|
11283
|
+
return mergeNe(value, fieldMatchers);
|
|
11284
|
+
} else if (operator === '$eq') {
|
|
11285
|
+
return mergeEq(value, fieldMatchers);
|
|
11286
|
+
} else if (operator === "$regex") {
|
|
11287
|
+
return mergeRegex(value, fieldMatchers);
|
|
11288
|
+
}
|
|
11289
|
+
fieldMatchers[operator] = value;
|
|
11290
|
+
});
|
|
11291
|
+
}
|
|
11292
|
+
});
|
|
11293
|
+
});
|
|
11294
|
+
|
|
11295
|
+
return res;
|
|
11296
|
+
}
|
|
11297
|
+
|
|
11298
|
+
|
|
11299
|
+
|
|
11300
|
+
// collapse logically equivalent gt/gte values
|
|
11301
|
+
function mergeGtGte(operator, value, fieldMatchers) {
|
|
11302
|
+
if (typeof fieldMatchers.$eq !== 'undefined') {
|
|
11303
|
+
return; // do nothing
|
|
11304
|
+
}
|
|
11305
|
+
if (typeof fieldMatchers.$gte !== 'undefined') {
|
|
11306
|
+
if (operator === '$gte') {
|
|
11307
|
+
if (value > fieldMatchers.$gte) { // more specificity
|
|
11308
|
+
fieldMatchers.$gte = value;
|
|
11309
|
+
}
|
|
11310
|
+
} else { // operator === '$gt'
|
|
11311
|
+
if (value >= fieldMatchers.$gte) { // more specificity
|
|
11312
|
+
delete fieldMatchers.$gte;
|
|
11313
|
+
fieldMatchers.$gt = value;
|
|
11314
|
+
}
|
|
11315
|
+
}
|
|
11316
|
+
} else if (typeof fieldMatchers.$gt !== 'undefined') {
|
|
11317
|
+
if (operator === '$gte') {
|
|
11318
|
+
if (value > fieldMatchers.$gt) { // more specificity
|
|
11319
|
+
delete fieldMatchers.$gt;
|
|
11320
|
+
fieldMatchers.$gte = value;
|
|
11321
|
+
}
|
|
11322
|
+
} else { // operator === '$gt'
|
|
11323
|
+
if (value > fieldMatchers.$gt) { // more specificity
|
|
11324
|
+
fieldMatchers.$gt = value;
|
|
11325
|
+
}
|
|
11326
|
+
}
|
|
11327
|
+
} else {
|
|
11328
|
+
fieldMatchers[operator] = value;
|
|
11329
|
+
}
|
|
11330
|
+
}
|
|
11331
|
+
|
|
11332
|
+
// collapse logically equivalent lt/lte values
|
|
11333
|
+
function mergeLtLte(operator, value, fieldMatchers) {
|
|
11334
|
+
if (typeof fieldMatchers.$eq !== 'undefined') {
|
|
11335
|
+
return; // do nothing
|
|
11336
|
+
}
|
|
11337
|
+
if (typeof fieldMatchers.$lte !== 'undefined') {
|
|
11338
|
+
if (operator === '$lte') {
|
|
11339
|
+
if (value < fieldMatchers.$lte) { // more specificity
|
|
11340
|
+
fieldMatchers.$lte = value;
|
|
11341
|
+
}
|
|
11342
|
+
} else { // operator === '$gt'
|
|
11343
|
+
if (value <= fieldMatchers.$lte) { // more specificity
|
|
11344
|
+
delete fieldMatchers.$lte;
|
|
11345
|
+
fieldMatchers.$lt = value;
|
|
11346
|
+
}
|
|
11347
|
+
}
|
|
11348
|
+
} else if (typeof fieldMatchers.$lt !== 'undefined') {
|
|
11349
|
+
if (operator === '$lte') {
|
|
11350
|
+
if (value < fieldMatchers.$lt) { // more specificity
|
|
11351
|
+
delete fieldMatchers.$lt;
|
|
11352
|
+
fieldMatchers.$lte = value;
|
|
11353
|
+
}
|
|
11354
|
+
} else { // operator === '$gt'
|
|
11355
|
+
if (value < fieldMatchers.$lt) { // more specificity
|
|
11356
|
+
fieldMatchers.$lt = value;
|
|
11357
|
+
}
|
|
11358
|
+
}
|
|
11359
|
+
} else {
|
|
11360
|
+
fieldMatchers[operator] = value;
|
|
11361
|
+
}
|
|
11362
|
+
}
|
|
11363
|
+
|
|
11364
|
+
// combine $ne values into one array
|
|
11365
|
+
function mergeNe(value, fieldMatchers) {
|
|
11366
|
+
if ('$ne' in fieldMatchers) {
|
|
11367
|
+
// there are many things this could "not" be
|
|
11368
|
+
fieldMatchers.$ne.push(value);
|
|
11369
|
+
} else { // doesn't exist yet
|
|
11370
|
+
fieldMatchers.$ne = [value];
|
|
11371
|
+
}
|
|
11372
|
+
}
|
|
11373
|
+
|
|
11374
|
+
// add $eq into the mix
|
|
11375
|
+
function mergeEq(value, fieldMatchers) {
|
|
11376
|
+
// these all have less specificity than the $eq
|
|
11377
|
+
// TODO: check for user errors here
|
|
11378
|
+
delete fieldMatchers.$gt;
|
|
11379
|
+
delete fieldMatchers.$gte;
|
|
11380
|
+
delete fieldMatchers.$lt;
|
|
11381
|
+
delete fieldMatchers.$lte;
|
|
11382
|
+
delete fieldMatchers.$ne;
|
|
11383
|
+
fieldMatchers.$eq = value;
|
|
11384
|
+
}
|
|
11385
|
+
|
|
11386
|
+
// combine $regex values into one array
|
|
11387
|
+
function mergeRegex(value, fieldMatchers) {
|
|
11388
|
+
if ('$regex' in fieldMatchers) {
|
|
11389
|
+
// a value could match multiple regexes
|
|
11390
|
+
fieldMatchers.$regex.push(value);
|
|
11391
|
+
} else { // doesn't exist yet
|
|
11392
|
+
fieldMatchers.$regex = [value];
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
|
|
11396
|
+
//#7458: execute function mergeAndedSelectors on nested $and
|
|
11397
|
+
function mergeAndedSelectorsNested(obj) {
|
|
11398
|
+
for (var prop in obj) {
|
|
11399
|
+
if (Array.isArray(obj)) {
|
|
11400
|
+
for (var i in obj) {
|
|
11401
|
+
if (obj[i]['$and']) {
|
|
11402
|
+
obj[i] = mergeAndedSelectors(obj[i]['$and']);
|
|
11403
|
+
}
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
var value = obj[prop];
|
|
11407
|
+
if (typeof value === 'object') {
|
|
11408
|
+
mergeAndedSelectorsNested(value); // <- recursive call
|
|
11409
|
+
}
|
|
11410
|
+
}
|
|
11411
|
+
return obj;
|
|
11412
|
+
}
|
|
11413
|
+
|
|
11414
|
+
//#7458: determine id $and is present in selector (at any level)
|
|
11415
|
+
function isAndInSelector(obj, isAnd) {
|
|
11416
|
+
for (var prop in obj) {
|
|
11417
|
+
if (prop === '$and') {
|
|
11418
|
+
isAnd = true;
|
|
11419
|
+
}
|
|
11420
|
+
var value = obj[prop];
|
|
11421
|
+
if (typeof value === 'object') {
|
|
11422
|
+
isAnd = isAndInSelector(value, isAnd); // <- recursive call
|
|
11423
|
+
}
|
|
11424
|
+
}
|
|
11425
|
+
return isAnd;
|
|
11426
|
+
}
|
|
11427
|
+
|
|
11428
|
+
//
|
|
11429
|
+
// normalize the selector
|
|
11430
|
+
//
|
|
11431
|
+
function massageSelector(input) {
|
|
11432
|
+
var result = clone(input);
|
|
11433
|
+
|
|
11434
|
+
//#7458: if $and is present in selector (at any level) merge nested $and
|
|
11435
|
+
if (isAndInSelector(result, false)) {
|
|
11436
|
+
result = mergeAndedSelectorsNested(result);
|
|
11437
|
+
if ('$and' in result) {
|
|
11438
|
+
result = mergeAndedSelectors(result['$and']);
|
|
11439
|
+
}
|
|
11440
|
+
}
|
|
11441
|
+
|
|
11442
|
+
['$or', '$nor'].forEach(function (orOrNor) {
|
|
11443
|
+
if (orOrNor in result) {
|
|
11444
|
+
// message each individual selector
|
|
11445
|
+
// e.g. {foo: 'bar'} becomes {foo: {$eq: 'bar'}}
|
|
11446
|
+
result[orOrNor].forEach(function (subSelector) {
|
|
11447
|
+
var fields = Object.keys(subSelector);
|
|
11448
|
+
for (var i = 0; i < fields.length; i++) {
|
|
11449
|
+
var field = fields[i];
|
|
11450
|
+
var matcher = subSelector[field];
|
|
11451
|
+
if (typeof matcher !== 'object' || matcher === null) {
|
|
11452
|
+
subSelector[field] = {$eq: matcher};
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
});
|
|
11456
|
+
}
|
|
11457
|
+
});
|
|
11458
|
+
|
|
11459
|
+
if ('$not' in result) {
|
|
11460
|
+
//This feels a little like forcing, but it will work for now,
|
|
11461
|
+
//I would like to come back to this and make the merging of selectors a little more generic
|
|
11462
|
+
result['$not'] = mergeAndedSelectors([result['$not']]);
|
|
11463
|
+
}
|
|
11464
|
+
|
|
11465
|
+
var fields = Object.keys(result);
|
|
11466
|
+
|
|
11467
|
+
for (var i = 0; i < fields.length; i++) {
|
|
11468
|
+
var field = fields[i];
|
|
11469
|
+
var matcher = result[field];
|
|
11470
|
+
|
|
11471
|
+
if (typeof matcher !== 'object' || matcher === null) {
|
|
11472
|
+
matcher = {$eq: matcher};
|
|
11473
|
+
}
|
|
11474
|
+
result[field] = matcher;
|
|
11475
|
+
}
|
|
11476
|
+
|
|
11477
|
+
normalizeArrayOperators(result);
|
|
11478
|
+
|
|
11479
|
+
return result;
|
|
11480
|
+
}
|
|
11481
|
+
|
|
11482
|
+
//
|
|
11483
|
+
// The $ne and $regex values must be placed in an array because these operators can be used multiple times on the same field.
|
|
11484
|
+
// When $and is used, mergeAndedSelectors takes care of putting some of them into arrays, otherwise it's done here.
|
|
11485
|
+
//
|
|
11486
|
+
function normalizeArrayOperators(selector) {
|
|
11487
|
+
Object.keys(selector).forEach(function (field) {
|
|
11488
|
+
var matcher = selector[field];
|
|
11489
|
+
|
|
11490
|
+
if (Array.isArray(matcher)) {
|
|
11491
|
+
matcher.forEach(function (matcherItem) {
|
|
11492
|
+
if (matcherItem && typeof matcherItem === 'object') {
|
|
11493
|
+
normalizeArrayOperators(matcherItem);
|
|
11494
|
+
}
|
|
11495
|
+
});
|
|
11496
|
+
} else if (field === '$ne') {
|
|
11497
|
+
selector.$ne = [matcher];
|
|
11498
|
+
} else if (field === '$regex') {
|
|
11499
|
+
selector.$regex = [matcher];
|
|
11500
|
+
} else if (matcher && typeof matcher === 'object') {
|
|
11501
|
+
normalizeArrayOperators(matcher);
|
|
11502
|
+
}
|
|
11503
|
+
});
|
|
11504
|
+
}
|
|
11505
|
+
|
|
11506
|
+
// create a comparator based on the sort object
|
|
11507
|
+
function createFieldSorter(sort) {
|
|
11508
|
+
|
|
11509
|
+
function getFieldValuesAsArray(doc) {
|
|
11510
|
+
return sort.map(function (sorting) {
|
|
11511
|
+
var fieldName = getKey(sorting);
|
|
11512
|
+
var parsedField = parseField(fieldName);
|
|
11513
|
+
var docFieldValue = getFieldFromDoc(doc, parsedField);
|
|
11514
|
+
return docFieldValue;
|
|
11515
|
+
});
|
|
11516
|
+
}
|
|
11517
|
+
|
|
11518
|
+
return function (aRow, bRow) {
|
|
11519
|
+
var aFieldValues = getFieldValuesAsArray(aRow.doc);
|
|
11520
|
+
var bFieldValues = getFieldValuesAsArray(bRow.doc);
|
|
11521
|
+
var collation = collate(aFieldValues, bFieldValues);
|
|
11522
|
+
if (collation !== 0) {
|
|
11523
|
+
return collation;
|
|
11524
|
+
}
|
|
11525
|
+
// this is what mango seems to do
|
|
11526
|
+
return compare(aRow.doc._id, bRow.doc._id);
|
|
11527
|
+
};
|
|
11528
|
+
}
|
|
11529
|
+
|
|
11530
|
+
function filterInMemoryFields(rows, requestDef, inMemoryFields) {
|
|
11531
|
+
rows = rows.filter(function (row) {
|
|
11532
|
+
return rowFilter(row.doc, requestDef.selector, inMemoryFields);
|
|
11533
|
+
});
|
|
11534
|
+
|
|
11535
|
+
if (requestDef.sort) {
|
|
11536
|
+
// in-memory sort
|
|
11537
|
+
var fieldSorter = createFieldSorter(requestDef.sort);
|
|
11538
|
+
rows = rows.sort(fieldSorter);
|
|
11539
|
+
if (typeof requestDef.sort[0] !== 'string' &&
|
|
11540
|
+
getValue(requestDef.sort[0]) === 'desc') {
|
|
11541
|
+
rows = rows.reverse();
|
|
11542
|
+
}
|
|
11543
|
+
}
|
|
11544
|
+
|
|
11545
|
+
if ('limit' in requestDef || 'skip' in requestDef) {
|
|
11546
|
+
// have to do the limit in-memory
|
|
11547
|
+
var skip = requestDef.skip || 0;
|
|
11548
|
+
var limit = ('limit' in requestDef ? requestDef.limit : rows.length) + skip;
|
|
11549
|
+
rows = rows.slice(skip, limit);
|
|
11550
|
+
}
|
|
11551
|
+
return rows;
|
|
11552
|
+
}
|
|
11553
|
+
|
|
11554
|
+
function rowFilter(doc, selector, inMemoryFields) {
|
|
11555
|
+
return inMemoryFields.every(function (field) {
|
|
11556
|
+
var matcher = selector[field];
|
|
11557
|
+
var parsedField = parseField(field);
|
|
11558
|
+
var docFieldValue = getFieldFromDoc(doc, parsedField);
|
|
11559
|
+
if (isCombinationalField(field)) {
|
|
11560
|
+
return matchCominationalSelector(field, matcher, doc);
|
|
11561
|
+
}
|
|
11562
|
+
|
|
11563
|
+
return matchSelector(matcher, doc, parsedField, docFieldValue);
|
|
11564
|
+
});
|
|
11565
|
+
}
|
|
11566
|
+
|
|
11567
|
+
function matchSelector(matcher, doc, parsedField, docFieldValue) {
|
|
11568
|
+
if (!matcher) {
|
|
11569
|
+
// no filtering necessary; this field is just needed for sorting
|
|
11570
|
+
return true;
|
|
11571
|
+
}
|
|
11572
|
+
|
|
11573
|
+
// is matcher an object, if so continue recursion
|
|
11574
|
+
if (typeof matcher === 'object') {
|
|
11575
|
+
return Object.keys(matcher).every(function (maybeUserOperator) {
|
|
11576
|
+
var userValue = matcher[ maybeUserOperator ];
|
|
11577
|
+
// explicit operator
|
|
11578
|
+
if (maybeUserOperator.indexOf("$") === 0) {
|
|
11579
|
+
return match(maybeUserOperator, doc, userValue, parsedField, docFieldValue);
|
|
11580
|
+
} else {
|
|
11581
|
+
var subParsedField = parseField(maybeUserOperator);
|
|
11582
|
+
|
|
11583
|
+
if (
|
|
11584
|
+
docFieldValue === undefined &&
|
|
11585
|
+
typeof userValue !== "object" &&
|
|
11586
|
+
subParsedField.length > 0
|
|
11587
|
+
) {
|
|
11588
|
+
// the field does not exist, return or getFieldFromDoc will throw
|
|
11589
|
+
return false;
|
|
11590
|
+
}
|
|
11591
|
+
|
|
11592
|
+
var subDocFieldValue = getFieldFromDoc(docFieldValue, subParsedField);
|
|
11593
|
+
|
|
11594
|
+
if (typeof userValue === "object") {
|
|
11595
|
+
// field value is an object that might contain more operators
|
|
11596
|
+
return matchSelector(userValue, doc, parsedField, subDocFieldValue);
|
|
11597
|
+
}
|
|
11598
|
+
|
|
11599
|
+
// implicit operator
|
|
11600
|
+
return match("$eq", doc, userValue, subParsedField, subDocFieldValue);
|
|
11601
|
+
}
|
|
11602
|
+
});
|
|
11603
|
+
}
|
|
11604
|
+
|
|
11605
|
+
// no more depth, No need to recurse further
|
|
11606
|
+
return matcher === docFieldValue;
|
|
11607
|
+
}
|
|
11608
|
+
|
|
11609
|
+
function matchCominationalSelector(field, matcher, doc) {
|
|
11610
|
+
|
|
11611
|
+
if (field === '$or') {
|
|
11612
|
+
return matcher.some(function (orMatchers) {
|
|
11613
|
+
return rowFilter(doc, orMatchers, Object.keys(orMatchers));
|
|
11614
|
+
});
|
|
11615
|
+
}
|
|
11616
|
+
|
|
11617
|
+
if (field === '$not') {
|
|
11618
|
+
return !rowFilter(doc, matcher, Object.keys(matcher));
|
|
11619
|
+
}
|
|
11620
|
+
|
|
11621
|
+
//`$nor`
|
|
11622
|
+
return !matcher.find(function (orMatchers) {
|
|
11623
|
+
return rowFilter(doc, orMatchers, Object.keys(orMatchers));
|
|
11624
|
+
});
|
|
11625
|
+
|
|
11626
|
+
}
|
|
11627
|
+
|
|
11628
|
+
function match(userOperator, doc, userValue, parsedField, docFieldValue) {
|
|
11629
|
+
if (!matchers[userOperator]) {
|
|
11630
|
+
/* istanbul ignore next */
|
|
11631
|
+
throw new Error('unknown operator "' + userOperator +
|
|
11632
|
+
'" - should be one of $eq, $lte, $lt, $gt, $gte, $exists, $ne, $in, ' +
|
|
11633
|
+
'$nin, $size, $mod, $regex, $elemMatch, $type, $allMatch or $all');
|
|
11634
|
+
}
|
|
11635
|
+
return matchers[userOperator](doc, userValue, parsedField, docFieldValue);
|
|
11636
|
+
}
|
|
11637
|
+
|
|
11638
|
+
function fieldExists(docFieldValue) {
|
|
11639
|
+
return typeof docFieldValue !== 'undefined' && docFieldValue !== null;
|
|
11640
|
+
}
|
|
11641
|
+
|
|
11642
|
+
function fieldIsNotUndefined(docFieldValue) {
|
|
11643
|
+
return typeof docFieldValue !== 'undefined';
|
|
11644
|
+
}
|
|
11645
|
+
|
|
11646
|
+
function modField(docFieldValue, userValue) {
|
|
11647
|
+
if (typeof docFieldValue !== "number" ||
|
|
11648
|
+
parseInt(docFieldValue, 10) !== docFieldValue) {
|
|
11649
|
+
return false;
|
|
11650
|
+
}
|
|
11651
|
+
|
|
11652
|
+
var divisor = userValue[0];
|
|
11653
|
+
var mod = userValue[1];
|
|
11654
|
+
|
|
11655
|
+
return docFieldValue % divisor === mod;
|
|
11656
|
+
}
|
|
11657
|
+
|
|
11658
|
+
function arrayContainsValue(docFieldValue, userValue) {
|
|
11659
|
+
return userValue.some(function (val) {
|
|
11660
|
+
if (docFieldValue instanceof Array) {
|
|
11661
|
+
return docFieldValue.some(function (docFieldValueItem) {
|
|
11662
|
+
return collate(val, docFieldValueItem) === 0;
|
|
11663
|
+
});
|
|
11664
|
+
}
|
|
11665
|
+
|
|
11666
|
+
return collate(val, docFieldValue) === 0;
|
|
11667
|
+
});
|
|
11668
|
+
}
|
|
11669
|
+
|
|
11670
|
+
function arrayContainsAllValues(docFieldValue, userValue) {
|
|
11671
|
+
return userValue.every(function (val) {
|
|
11672
|
+
return docFieldValue.some(function (docFieldValueItem) {
|
|
11673
|
+
return collate(val, docFieldValueItem) === 0;
|
|
11674
|
+
});
|
|
11675
|
+
});
|
|
11676
|
+
}
|
|
11677
|
+
|
|
11678
|
+
function arraySize(docFieldValue, userValue) {
|
|
11679
|
+
return docFieldValue.length === userValue;
|
|
11680
|
+
}
|
|
11681
|
+
|
|
11682
|
+
function regexMatch(docFieldValue, userValue) {
|
|
11683
|
+
var re = new RegExp(userValue);
|
|
11684
|
+
|
|
11685
|
+
return re.test(docFieldValue);
|
|
11686
|
+
}
|
|
11687
|
+
|
|
11688
|
+
function typeMatch(docFieldValue, userValue) {
|
|
11689
|
+
|
|
11690
|
+
switch (userValue) {
|
|
11691
|
+
case 'null':
|
|
11692
|
+
return docFieldValue === null;
|
|
11693
|
+
case 'boolean':
|
|
11694
|
+
return typeof (docFieldValue) === 'boolean';
|
|
11695
|
+
case 'number':
|
|
11696
|
+
return typeof (docFieldValue) === 'number';
|
|
11697
|
+
case 'string':
|
|
11698
|
+
return typeof (docFieldValue) === 'string';
|
|
11699
|
+
case 'array':
|
|
11700
|
+
return docFieldValue instanceof Array;
|
|
11701
|
+
case 'object':
|
|
11702
|
+
return ({}).toString.call(docFieldValue) === '[object Object]';
|
|
11703
|
+
}
|
|
11704
|
+
}
|
|
11705
|
+
|
|
11706
|
+
var matchers = {
|
|
11707
|
+
|
|
11708
|
+
'$elemMatch': function (doc, userValue, parsedField, docFieldValue) {
|
|
11709
|
+
if (!Array.isArray(docFieldValue)) {
|
|
11710
|
+
return false;
|
|
11711
|
+
}
|
|
11712
|
+
|
|
11713
|
+
if (docFieldValue.length === 0) {
|
|
11714
|
+
return false;
|
|
11715
|
+
}
|
|
11716
|
+
|
|
11717
|
+
if (typeof docFieldValue[0] === 'object' && docFieldValue[0] !== null) {
|
|
11718
|
+
return docFieldValue.some(function (val) {
|
|
11719
|
+
return rowFilter(val, userValue, Object.keys(userValue));
|
|
11720
|
+
});
|
|
11721
|
+
}
|
|
11722
|
+
|
|
11723
|
+
return docFieldValue.some(function (val) {
|
|
11724
|
+
return matchSelector(userValue, doc, parsedField, val);
|
|
11725
|
+
});
|
|
11726
|
+
},
|
|
11727
|
+
|
|
11728
|
+
'$allMatch': function (doc, userValue, parsedField, docFieldValue) {
|
|
11729
|
+
if (!Array.isArray(docFieldValue)) {
|
|
11730
|
+
return false;
|
|
11731
|
+
}
|
|
11732
|
+
|
|
11733
|
+
/* istanbul ignore next */
|
|
11734
|
+
if (docFieldValue.length === 0) {
|
|
11735
|
+
return false;
|
|
11736
|
+
}
|
|
11737
|
+
|
|
11738
|
+
if (typeof docFieldValue[0] === 'object' && docFieldValue[0] !== null) {
|
|
11739
|
+
return docFieldValue.every(function (val) {
|
|
11740
|
+
return rowFilter(val, userValue, Object.keys(userValue));
|
|
11741
|
+
});
|
|
11742
|
+
}
|
|
11743
|
+
|
|
11744
|
+
return docFieldValue.every(function (val) {
|
|
11745
|
+
return matchSelector(userValue, doc, parsedField, val);
|
|
11746
|
+
});
|
|
11747
|
+
},
|
|
11748
|
+
|
|
11749
|
+
'$eq': function (doc, userValue, parsedField, docFieldValue) {
|
|
11750
|
+
return fieldIsNotUndefined(docFieldValue) && collate(docFieldValue, userValue) === 0;
|
|
11751
|
+
},
|
|
11752
|
+
|
|
11753
|
+
'$gte': function (doc, userValue, parsedField, docFieldValue) {
|
|
11754
|
+
return fieldIsNotUndefined(docFieldValue) && collate(docFieldValue, userValue) >= 0;
|
|
11755
|
+
},
|
|
11756
|
+
|
|
11757
|
+
'$gt': function (doc, userValue, parsedField, docFieldValue) {
|
|
11758
|
+
return fieldIsNotUndefined(docFieldValue) && collate(docFieldValue, userValue) > 0;
|
|
11759
|
+
},
|
|
11760
|
+
|
|
11761
|
+
'$lte': function (doc, userValue, parsedField, docFieldValue) {
|
|
11762
|
+
return fieldIsNotUndefined(docFieldValue) && collate(docFieldValue, userValue) <= 0;
|
|
11763
|
+
},
|
|
11764
|
+
|
|
11765
|
+
'$lt': function (doc, userValue, parsedField, docFieldValue) {
|
|
11766
|
+
return fieldIsNotUndefined(docFieldValue) && collate(docFieldValue, userValue) < 0;
|
|
11767
|
+
},
|
|
11768
|
+
|
|
11769
|
+
'$exists': function (doc, userValue, parsedField, docFieldValue) {
|
|
11770
|
+
//a field that is null is still considered to exist
|
|
11771
|
+
if (userValue) {
|
|
11772
|
+
return fieldIsNotUndefined(docFieldValue);
|
|
11773
|
+
}
|
|
11774
|
+
|
|
11775
|
+
return !fieldIsNotUndefined(docFieldValue);
|
|
11776
|
+
},
|
|
11777
|
+
|
|
11778
|
+
'$mod': function (doc, userValue, parsedField, docFieldValue) {
|
|
11779
|
+
return fieldExists(docFieldValue) && modField(docFieldValue, userValue);
|
|
11780
|
+
},
|
|
11781
|
+
|
|
11782
|
+
'$ne': function (doc, userValue, parsedField, docFieldValue) {
|
|
11783
|
+
return userValue.every(function (neValue) {
|
|
11784
|
+
return collate(docFieldValue, neValue) !== 0;
|
|
11785
|
+
});
|
|
11786
|
+
},
|
|
11787
|
+
'$in': function (doc, userValue, parsedField, docFieldValue) {
|
|
11788
|
+
return fieldExists(docFieldValue) && arrayContainsValue(docFieldValue, userValue);
|
|
11789
|
+
},
|
|
11790
|
+
|
|
11791
|
+
'$nin': function (doc, userValue, parsedField, docFieldValue) {
|
|
11792
|
+
return fieldExists(docFieldValue) && !arrayContainsValue(docFieldValue, userValue);
|
|
11793
|
+
},
|
|
11794
|
+
|
|
11795
|
+
'$size': function (doc, userValue, parsedField, docFieldValue) {
|
|
11796
|
+
return fieldExists(docFieldValue) &&
|
|
11797
|
+
Array.isArray(docFieldValue) &&
|
|
11798
|
+
arraySize(docFieldValue, userValue);
|
|
11799
|
+
},
|
|
11800
|
+
|
|
11801
|
+
'$all': function (doc, userValue, parsedField, docFieldValue) {
|
|
11802
|
+
return Array.isArray(docFieldValue) && arrayContainsAllValues(docFieldValue, userValue);
|
|
11803
|
+
},
|
|
11804
|
+
|
|
11805
|
+
'$regex': function (doc, userValue, parsedField, docFieldValue) {
|
|
11806
|
+
return fieldExists(docFieldValue) &&
|
|
11807
|
+
typeof docFieldValue == "string" &&
|
|
11808
|
+
userValue.every(function (regexValue) {
|
|
11809
|
+
return regexMatch(docFieldValue, regexValue);
|
|
11810
|
+
});
|
|
11811
|
+
},
|
|
11812
|
+
|
|
11813
|
+
'$type': function (doc, userValue, parsedField, docFieldValue) {
|
|
11814
|
+
return typeMatch(docFieldValue, userValue);
|
|
11815
|
+
}
|
|
11816
|
+
};
|
|
11817
|
+
|
|
11818
|
+
// return true if the given doc matches the supplied selector
|
|
11819
|
+
function matchesSelector(doc, selector) {
|
|
11820
|
+
/* istanbul ignore if */
|
|
11821
|
+
if (typeof selector !== 'object') {
|
|
11822
|
+
// match the CouchDB error message
|
|
11823
|
+
throw new Error('Selector error: expected a JSON object');
|
|
11824
|
+
}
|
|
11825
|
+
|
|
11826
|
+
selector = massageSelector(selector);
|
|
11827
|
+
var row = {
|
|
11828
|
+
doc
|
|
11829
|
+
};
|
|
11830
|
+
|
|
11831
|
+
var rowsMatched = filterInMemoryFields([row], { selector }, Object.keys(selector));
|
|
11832
|
+
return rowsMatched && rowsMatched.length === 1;
|
|
11833
|
+
}
|
|
11834
|
+
|
|
11835
|
+
// pouchdb-find's own matcher and collation - using anything else would let overlay
|
|
11836
|
+
// semantics drift from what the same selector means against the database.
|
|
11837
|
+
/**
|
|
11838
|
+
* True when a query with this selector could see documents this stage holds.
|
|
11839
|
+
*
|
|
11840
|
+
* Derived from the selector's `~class` / `~domain` constraint against the stage's
|
|
11841
|
+
* partitions; a selector naming no class is answered conservatively. This is the
|
|
11842
|
+
* per-query fast path: a find over a class the transaction never touched runs
|
|
11843
|
+
* exactly as it would outside the transaction.
|
|
11844
|
+
*/
|
|
11845
|
+
const stageCoversSelector = (stage, selector) => {
|
|
11846
|
+
if (stage.size === 0)
|
|
11847
|
+
return false;
|
|
11848
|
+
const names = [
|
|
11849
|
+
...constraintNames(selector["~class"]),
|
|
11850
|
+
...constraintNames(selector["~domain"]),
|
|
11851
|
+
];
|
|
11852
|
+
if (!names.length)
|
|
11853
|
+
return true;
|
|
11854
|
+
return names.some(name => stage.hasPartition(name));
|
|
11855
|
+
};
|
|
11856
|
+
const constraintNames = (constraint) => {
|
|
11857
|
+
if (typeof constraint === "string")
|
|
11858
|
+
return [constraint];
|
|
11859
|
+
if (constraint && typeof constraint === "object") {
|
|
11860
|
+
const eq = constraint.$eq;
|
|
11861
|
+
if (typeof eq === "string")
|
|
11862
|
+
return [eq];
|
|
11863
|
+
const within = constraint.$in;
|
|
11864
|
+
if (Array.isArray(within))
|
|
11865
|
+
return within.filter((name) => typeof name === "string");
|
|
11866
|
+
}
|
|
11867
|
+
return [];
|
|
11868
|
+
};
|
|
11869
|
+
/**
|
|
11870
|
+
* Fields the widened committed query (and the staged docs) must carry beyond the
|
|
11871
|
+
* caller's projection, so masking, sorting and the read pipeline can work; the
|
|
11872
|
+
* extras are stripped again after the merge.
|
|
11873
|
+
*/
|
|
11874
|
+
const widenProjection = (fields, sort) => {
|
|
11875
|
+
if (!fields || !fields.length)
|
|
11876
|
+
return { queryFields: undefined, extras: [] };
|
|
11877
|
+
const needed = new Set(["_id", "~class"]);
|
|
11878
|
+
for (const entry of sort !== null && sort !== void 0 ? sort : []) {
|
|
11879
|
+
needed.add(typeof entry === "string" ? entry : Object.keys(entry)[0]);
|
|
11880
|
+
}
|
|
11881
|
+
const extras = [...needed].filter(field => !fields.includes(field));
|
|
11882
|
+
return { queryFields: [...fields, ...extras], extras };
|
|
11883
|
+
};
|
|
11884
|
+
const projectDoc = (doc, fields) => {
|
|
11885
|
+
const projected = {};
|
|
11886
|
+
for (const field of fields) {
|
|
11887
|
+
const value = getFieldFromDoc(doc, parseField(field));
|
|
11888
|
+
if (value !== undefined)
|
|
11889
|
+
projected[field] = value;
|
|
11890
|
+
}
|
|
11891
|
+
return projected;
|
|
11892
|
+
};
|
|
11893
|
+
const sortComparator = (sort) => {
|
|
11894
|
+
const parts = sort.map(entry => {
|
|
11895
|
+
const field = typeof entry === "string" ? entry : Object.keys(entry)[0];
|
|
11896
|
+
const direction = typeof entry === "string" ? "asc" : entry[field];
|
|
11897
|
+
return { parsed: parseField(field), factor: direction === "desc" ? -1 : 1 };
|
|
11898
|
+
});
|
|
11899
|
+
return (a, b) => {
|
|
11900
|
+
for (const { parsed, factor } of parts) {
|
|
11901
|
+
const result = compare(getFieldFromDoc(a, parsed), getFieldFromDoc(b, parsed)) * factor;
|
|
11902
|
+
if (result !== 0)
|
|
11903
|
+
return result;
|
|
11904
|
+
}
|
|
11905
|
+
return 0;
|
|
11906
|
+
};
|
|
11907
|
+
};
|
|
11908
|
+
/**
|
|
11909
|
+
* Merges a stage into a committed query result: every staged id masks its committed
|
|
11910
|
+
* row (superseded and deleted alike), staged writes matching the selector join the
|
|
11911
|
+
* set, then sort, window and projection apply in memory - the database's index can
|
|
11912
|
+
* never see a staged document, so the window has to be computed after the union.
|
|
11913
|
+
*
|
|
11914
|
+
* `committedDocs` must come from a query WITHOUT skip/limit (the mask changes what
|
|
11915
|
+
* the window contains) and carrying `widenProjection`'s fields.
|
|
11916
|
+
*/
|
|
11917
|
+
const mergeStageIntoResults = (stage, selector, committedDocs, options = {}) => {
|
|
11918
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
11919
|
+
const survivors = committedDocs.filter(doc => !stage.has(doc._id));
|
|
11920
|
+
for (const entry of stage.values()) {
|
|
11921
|
+
if (entry.op !== "write")
|
|
11922
|
+
continue;
|
|
11923
|
+
if (!matchesSelector(entry.doc, selector))
|
|
11924
|
+
continue;
|
|
11925
|
+
const overlaid = Object.assign({}, structuredClone(entry.doc));
|
|
11926
|
+
if (entry.baseRev)
|
|
11927
|
+
overlaid._rev = (_a = overlaid._rev) !== null && _a !== void 0 ? _a : entry.baseRev;
|
|
11928
|
+
survivors.push(((_b = options.fields) === null || _b === void 0 ? void 0 : _b.length)
|
|
11929
|
+
? projectDoc(overlaid, [...options.fields, ...((_c = options.extras) !== null && _c !== void 0 ? _c : [])])
|
|
11930
|
+
: overlaid);
|
|
11931
|
+
}
|
|
11932
|
+
if ((_d = options.sort) === null || _d === void 0 ? void 0 : _d.length)
|
|
11933
|
+
survivors.sort(sortComparator(options.sort));
|
|
11934
|
+
const skip = (_e = options.skip) !== null && _e !== void 0 ? _e : 0;
|
|
11935
|
+
const limit = (_f = options.limit) !== null && _f !== void 0 ? _f : survivors.length;
|
|
11936
|
+
const windowed = survivors.slice(skip, skip + limit);
|
|
11937
|
+
if (((_g = options.fields) === null || _g === void 0 ? void 0 : _g.length) && ((_h = options.extras) === null || _h === void 0 ? void 0 : _h.length)) {
|
|
11938
|
+
return windowed.map(doc => projectDoc(doc, options.fields));
|
|
11939
|
+
}
|
|
11940
|
+
return windowed;
|
|
11941
|
+
};
|
|
11942
|
+
/** A PouchDB-shaped `not_found`, so overlay reads refuse like the database does. */
|
|
11943
|
+
const notFoundError = (id) => {
|
|
11944
|
+
const error = new Error("missing");
|
|
11945
|
+
error.status = 404;
|
|
11946
|
+
error.name = "not_found";
|
|
11947
|
+
error.error = true;
|
|
11948
|
+
error.reason = "deleted";
|
|
11949
|
+
error.docId = id;
|
|
11950
|
+
return error;
|
|
11951
|
+
};
|
|
11952
|
+
/**
|
|
11953
|
+
* Read-your-writes for a point read: a staged delete is a 404, a staged write is the
|
|
11954
|
+
* authored plaintext (its `_rev` is the base revision - the revision the commit will
|
|
11955
|
+
* replace), anything else is the stack's ordinary decrypting read.
|
|
11956
|
+
*/
|
|
11957
|
+
const overlayGet = async (stack, stage, id, options) => {
|
|
11958
|
+
const entry = stage.get(id);
|
|
11959
|
+
if (entry) {
|
|
11960
|
+
if (entry.op === "delete")
|
|
11961
|
+
throw notFoundError(id);
|
|
11962
|
+
const doc = structuredClone(entry.doc);
|
|
11963
|
+
if (entry.baseRev && doc._rev === undefined)
|
|
11964
|
+
doc._rev = entry.baseRev;
|
|
11965
|
+
return doc;
|
|
11966
|
+
}
|
|
11967
|
+
return stack.db.get(id, options !== null && options !== void 0 ? options : {});
|
|
11968
|
+
};
|
|
11969
|
+
/** `bulkGet` counterpart of {@link overlayGet}, preserving request order. */
|
|
11970
|
+
const overlayBulkGet = async (stack, stage, request) => {
|
|
11971
|
+
var _a, _b, _c, _d;
|
|
11972
|
+
const passthrough = ((_a = request.docs) !== null && _a !== void 0 ? _a : []).filter(item => !stage.has(item.id));
|
|
11973
|
+
const fetched = passthrough.length
|
|
11974
|
+
? await stack.db.bulkGet(Object.assign(Object.assign({}, request), { docs: passthrough }))
|
|
11975
|
+
: { results: [] };
|
|
11976
|
+
const byId = new Map();
|
|
11977
|
+
for (const result of (_b = fetched.results) !== null && _b !== void 0 ? _b : []) {
|
|
11978
|
+
const list = (_c = byId.get(result.id)) !== null && _c !== void 0 ? _c : [];
|
|
11979
|
+
list.push(result);
|
|
11980
|
+
byId.set(result.id, list);
|
|
11981
|
+
}
|
|
11982
|
+
const results = ((_d = request.docs) !== null && _d !== void 0 ? _d : []).map(item => {
|
|
11983
|
+
var _a, _b;
|
|
11984
|
+
const entry = stage.get(item.id);
|
|
11985
|
+
if (!entry)
|
|
11986
|
+
return (_b = (_a = byId.get(item.id)) === null || _a === void 0 ? void 0 : _a.shift()) !== null && _b !== void 0 ? _b : { id: item.id, docs: [{ error: notFoundError(item.id) }] };
|
|
11987
|
+
if (entry.op === "delete")
|
|
11988
|
+
return { id: item.id, docs: [{ error: notFoundError(item.id) }] };
|
|
11989
|
+
const doc = structuredClone(entry.doc);
|
|
11990
|
+
if (entry.baseRev && doc._rev === undefined)
|
|
11991
|
+
doc._rev = entry.baseRev;
|
|
11992
|
+
return { id: item.id, docs: [{ ok: doc }] };
|
|
11993
|
+
});
|
|
11994
|
+
return { results };
|
|
11995
|
+
};
|
|
11996
|
+
|
|
11997
|
+
const conflictError = (id) => {
|
|
11998
|
+
const error = new Error("Document update conflict");
|
|
11999
|
+
error.status = 409;
|
|
12000
|
+
error.name = "conflict";
|
|
12001
|
+
error.error = true;
|
|
12002
|
+
error.docId = id;
|
|
12003
|
+
return error;
|
|
12004
|
+
};
|
|
12005
|
+
/**
|
|
12006
|
+
* One transaction: a private write journal plus a read view that overlays it on
|
|
12007
|
+
* committed state (ADR-0039).
|
|
12008
|
+
*
|
|
12009
|
+
* Writes through the handle are validated at the call site (the sweep - failing
|
|
12010
|
+
* stages nothing) and stage in memory; nothing reaches the database until
|
|
12011
|
+
* {@link commit}, which flushes the journal as one batch through the stack's full
|
|
12012
|
+
* authoring pipeline. Reads through the handle see the journal; `stack.db`, other
|
|
12013
|
+
* handles, replication and live subscriptions see only committed state.
|
|
12014
|
+
*
|
|
12015
|
+
* @example
|
|
12016
|
+
* ```typescript
|
|
12017
|
+
* const t = stack.beginTransaction();
|
|
12018
|
+
* await t.createDoc(null, "Task", { title: "write-up" });
|
|
12019
|
+
* await t.db.put({ ...(await t.db.get("Task-77")), done: true });
|
|
12020
|
+
* const drafted = await t.findDocuments({ "~class": { $eq: "Task" } });
|
|
12021
|
+
* const report = await t.commit(); // or stack.commit(t)
|
|
12022
|
+
* ```
|
|
12023
|
+
*/
|
|
12024
|
+
class TransactionHandle {
|
|
12025
|
+
/** @internal */
|
|
12026
|
+
constructor(stack, engine, id, internal = false) {
|
|
12027
|
+
/** @internal */
|
|
12028
|
+
this.stage = new TransactionStage();
|
|
12029
|
+
/** @internal - ids this handle minted, counted into the id counter after commit. */
|
|
12030
|
+
this.mintedIds = new Set();
|
|
12031
|
+
this.statusValue = "open";
|
|
12032
|
+
this.stack = stack;
|
|
12033
|
+
this.engine = engine;
|
|
12034
|
+
this.id = id;
|
|
12035
|
+
this.internal = internal;
|
|
12036
|
+
this.db = new TransactionDb(stack, this);
|
|
12037
|
+
}
|
|
12038
|
+
get status() {
|
|
12039
|
+
return this.statusValue;
|
|
12040
|
+
}
|
|
12041
|
+
/** @internal */
|
|
12042
|
+
setStatus(status) {
|
|
12043
|
+
this.statusValue = status;
|
|
12044
|
+
}
|
|
12045
|
+
stagedCount() {
|
|
12046
|
+
return this.stage.size;
|
|
12047
|
+
}
|
|
12048
|
+
/** @internal */
|
|
12049
|
+
assertWritable(operation) {
|
|
12050
|
+
if (this.statusValue !== "open" && this.statusValue !== "partial") {
|
|
12051
|
+
throw new TransactionStateError(this.id, this.statusValue, operation);
|
|
12052
|
+
}
|
|
12053
|
+
}
|
|
12054
|
+
/**
|
|
12055
|
+
* Stages a write. The sweep runs first: a document that fails validation, policy,
|
|
12056
|
+
* or the locked-stack check is not staged and the journal is untouched.
|
|
12057
|
+
* @internal
|
|
12058
|
+
*/
|
|
12059
|
+
async stageWrite(doc, op = "write") {
|
|
12060
|
+
this.assertWritable("accept writes");
|
|
12061
|
+
const docId = doc._id;
|
|
12062
|
+
if (typeof docId !== "string" || !docId) {
|
|
12063
|
+
throw new Error("A staged document needs an '_id' - transactions do not mint ids implicitly; use createDoc(null, ...).");
|
|
12064
|
+
}
|
|
12065
|
+
const already = this.stage.get(docId);
|
|
12066
|
+
let baseRev;
|
|
12067
|
+
let isNew;
|
|
12068
|
+
if (already) {
|
|
12069
|
+
baseRev = already.baseRev;
|
|
12070
|
+
isNew = already.isNew;
|
|
12071
|
+
}
|
|
12072
|
+
else {
|
|
12073
|
+
baseRev = await this.stack.getDocRevision(docId).catch(() => undefined) || undefined;
|
|
12074
|
+
isNew = baseRev === undefined;
|
|
12075
|
+
}
|
|
12076
|
+
// PouchDB's optimistic-concurrency contract, kept: updating requires stating
|
|
12077
|
+
// the revision being replaced - here, the overlay-visible one.
|
|
12078
|
+
const statedRev = doc._rev;
|
|
12079
|
+
if (!isNew && statedRev !== baseRev)
|
|
12080
|
+
throw conflictError(docId);
|
|
12081
|
+
if (isNew && statedRev !== undefined)
|
|
12082
|
+
throw conflictError(docId);
|
|
12083
|
+
const entry = {
|
|
12084
|
+
doc: structuredClone(Object.assign(Object.assign({}, doc), { _rev: undefined })),
|
|
12085
|
+
baseRev,
|
|
12086
|
+
op,
|
|
12087
|
+
isNew,
|
|
12088
|
+
stagedAt: Date.now(),
|
|
12089
|
+
};
|
|
12090
|
+
delete entry.doc._rev;
|
|
12091
|
+
await sweepEntry(this.stack, this.stage, entry, { allowClassModels: this.internal, skipPolicy: this.internal });
|
|
12092
|
+
this.stage.set(docId, entry);
|
|
12093
|
+
return this.stage.get(docId);
|
|
12094
|
+
}
|
|
12095
|
+
/**
|
|
12096
|
+
* Creates or updates a document in the transaction - `stack.createDoc`'s UX with
|
|
12097
|
+
* a staged destination: `docId: null` mints an id, an existing id merges params
|
|
12098
|
+
* over the overlay-visible document.
|
|
12099
|
+
*/
|
|
12100
|
+
async createDoc(docId, type, params) {
|
|
12101
|
+
this.assertWritable("create documents");
|
|
12102
|
+
let base = null;
|
|
12103
|
+
if (docId) {
|
|
12104
|
+
base = await overlayGet(this.stack, this.stage, docId).catch(() => null);
|
|
12105
|
+
if (base && base["~class"] !== type) {
|
|
12106
|
+
throw new Error(`Existing document '${docId}' is a '${base["~class"]}', not a '${type}'.`);
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
else {
|
|
12110
|
+
docId = this.stack.generateDocId(type);
|
|
12111
|
+
this.mintedIds.add(docId);
|
|
12112
|
+
}
|
|
12113
|
+
const draft = base
|
|
12114
|
+
? Object.assign(Object.assign(Object.assign({}, base), params), { _id: docId, "~updateTimestamp": new Date().getTime() }) : Object.assign(Object.assign(Object.assign({}, this.stack.prepareDoc(docId, type, params, "~class")), params), { _id: docId });
|
|
12115
|
+
const entry = await this.stageWrite(draft, "write").catch(error => {
|
|
12116
|
+
// An id minted for a write that never staged is simply dropped - the
|
|
12117
|
+
// counter only ever advances for committed documents.
|
|
12118
|
+
if (!base && docId)
|
|
12119
|
+
this.mintedIds.delete(docId);
|
|
12120
|
+
throw error;
|
|
12121
|
+
});
|
|
12122
|
+
return structuredClone(entry.doc);
|
|
12123
|
+
}
|
|
12124
|
+
/** Batch counterpart of {@link createDoc}; validated sequentially, fail-fast. */
|
|
12125
|
+
async createDocs(docs, type) {
|
|
12126
|
+
const created = [];
|
|
12127
|
+
for (const draft of docs) {
|
|
12128
|
+
created.push(await this.createDoc(draft.docId, type, draft.params));
|
|
12129
|
+
}
|
|
12130
|
+
return created;
|
|
12131
|
+
}
|
|
12132
|
+
/** Soft-deletes in the transaction: the overlay stops showing the document under the default `active: true`. */
|
|
12133
|
+
async deleteDocument(docId) {
|
|
12134
|
+
var _a, _b;
|
|
12135
|
+
this.assertWritable("delete documents");
|
|
12136
|
+
const doc = await overlayGet(this.stack, this.stage, docId).catch(() => null);
|
|
12137
|
+
if (!doc)
|
|
12138
|
+
return false;
|
|
12139
|
+
await this.stageWrite(Object.assign(Object.assign({}, doc), { active: false, _rev: (_b = (_a = this.stage.get(docId)) === null || _a === void 0 ? void 0 : _a.baseRev) !== null && _b !== void 0 ? _b : doc._rev }), "write");
|
|
12140
|
+
return true;
|
|
12141
|
+
}
|
|
12142
|
+
/** The stack's polished read, against this transaction's view. */
|
|
12143
|
+
async findDocuments(selector, fields, skip, limit, sort) {
|
|
12144
|
+
this.assertWritable("read");
|
|
12145
|
+
return this.stack.findDocumentsForView(this.stage, selector, fields, skip, limit, sort);
|
|
12146
|
+
}
|
|
12147
|
+
/**
|
|
12148
|
+
* SQL against this transaction's view. The executor reaches data only through
|
|
12149
|
+
* stack APIs, so a facade routes them at the overlay; LIMIT/OFFSET pushdown and
|
|
12150
|
+
* sort indexes are disabled while staged - staged documents exist in no index,
|
|
12151
|
+
* so windows and orderings must be computed after the merge.
|
|
12152
|
+
*/
|
|
12153
|
+
async query(sql, ...params) {
|
|
12154
|
+
this.assertWritable("query");
|
|
12155
|
+
const stack = this.stack;
|
|
12156
|
+
const stage = this.stage;
|
|
12157
|
+
const facade = Object.create(stack);
|
|
12158
|
+
facade.findDocuments = (selector, fields, skip, limit, sort) => stack.findDocumentsForView(stage, selector, fields, skip, limit, sort);
|
|
12159
|
+
facade.findDocumentsIterator = function (selector_1) {
|
|
12160
|
+
return __asyncGenerator(this, arguments, function* (selector, options = {}) {
|
|
12161
|
+
const result = yield __await(facade.findDocuments(selector, options.fields));
|
|
12162
|
+
for (const doc of result.docs)
|
|
12163
|
+
yield yield __await(doc);
|
|
12164
|
+
});
|
|
12165
|
+
};
|
|
12166
|
+
facade.canApplyQueryLimitEarly = async () => false;
|
|
12167
|
+
facade.ensureSortIndex = async () => false;
|
|
12168
|
+
facade.getClass = async (name, ...rest) => {
|
|
12169
|
+
const real = await stack.getClass(name, ...rest);
|
|
12170
|
+
if (!real)
|
|
12171
|
+
return real;
|
|
12172
|
+
const wrapped = Object.create(real);
|
|
12173
|
+
// `getCards` is an own arrow property bound to the real stack; shadowed so
|
|
12174
|
+
// the executor's fetches route through the overlay.
|
|
12175
|
+
wrapped.getCards = (selector, fields, skip, limit, sort) => facade.findDocuments(Object.assign(Object.assign({}, (selector || {})), { "~class": { $eq: real.name } }), fields, skip, limit, sort)
|
|
12176
|
+
.then((result) => result.docs);
|
|
12177
|
+
return wrapped;
|
|
12178
|
+
};
|
|
12179
|
+
return stack.runQuery(sql, params, facade);
|
|
12180
|
+
}
|
|
12181
|
+
/** Flushes the journal - sugar for `stack.commit(t)`. */
|
|
12182
|
+
commit() {
|
|
12183
|
+
return this.engine.commit(this);
|
|
12184
|
+
}
|
|
12185
|
+
/** Drops the journal - sugar for `stack.discardTransaction(t)`. */
|
|
12186
|
+
discard() {
|
|
12187
|
+
this.engine.discard(this);
|
|
12188
|
+
}
|
|
12189
|
+
}
|
|
12190
|
+
/**
|
|
12191
|
+
* The handle's db-like surface. Writes stage; reads overlay. Not a Proxy over the
|
|
12192
|
+
* guarded db on purpose: this object *has no* adapter methods or escape hatches to
|
|
12193
|
+
* forward, so staging cannot become a fourth door around the authoring path.
|
|
12194
|
+
*/
|
|
12195
|
+
class TransactionDb {
|
|
12196
|
+
/** @internal */
|
|
12197
|
+
constructor(stack, handle) {
|
|
12198
|
+
this.stack = stack;
|
|
12199
|
+
this.handle = handle;
|
|
12200
|
+
}
|
|
12201
|
+
async get(docId, options) {
|
|
12202
|
+
this.handle.assertWritable("read");
|
|
12203
|
+
return overlayGet(this.stack, this.handle.stage, docId, options);
|
|
12204
|
+
}
|
|
12205
|
+
async bulkGet(request) {
|
|
12206
|
+
this.handle.assertWritable("read");
|
|
12207
|
+
return overlayBulkGet(this.stack, this.handle.stage, request);
|
|
12208
|
+
}
|
|
12209
|
+
/**
|
|
12210
|
+
* Raw-style Mango find over the transaction's view. Like `stack.db.find`, this
|
|
12211
|
+
* skips the read pipeline (no policy filter, no decryption of committed rows);
|
|
12212
|
+
* `findDocuments` on the handle is the polished read.
|
|
12213
|
+
*/
|
|
12214
|
+
async find(query) {
|
|
12215
|
+
this.handle.assertWritable("read");
|
|
12216
|
+
const stage = this.handle.stage;
|
|
12217
|
+
const { selector, fields, skip, limit, sort } = query;
|
|
12218
|
+
const { queryFields, extras } = widenProjection(fields, sort);
|
|
12219
|
+
const committed = await this.stack.db.find(Object.assign(Object.assign({ selector }, (queryFields ? { fields: queryFields } : {})), { limit: 2 ** 31 - 1 }));
|
|
12220
|
+
const docs = mergeStageIntoResults(stage, selector, committed.docs, { sort, skip, limit, fields, extras });
|
|
12221
|
+
return { docs };
|
|
12222
|
+
}
|
|
12223
|
+
async put(doc, options) {
|
|
12224
|
+
if (options && (options.new_edits === false || options.force)) {
|
|
12225
|
+
throw new Error("Writing with 'new_edits: false' or 'force' is reserved for DocStack's sync layer, in and out of transactions.");
|
|
12226
|
+
}
|
|
12227
|
+
const entry = await this.handle.stageWrite(doc, "write");
|
|
12228
|
+
return { ok: true, id: doc._id, rev: entry.baseRev, staged: true };
|
|
12229
|
+
}
|
|
12230
|
+
async post(doc) {
|
|
12231
|
+
var _a;
|
|
12232
|
+
const type = (_a = doc["~class"]) !== null && _a !== void 0 ? _a : doc["~domain"];
|
|
12233
|
+
if (typeof type !== "string" || !type) {
|
|
12234
|
+
throw new Error("post needs '~class' (or '~domain') to mint an id for the document.");
|
|
12235
|
+
}
|
|
12236
|
+
const id = this.stack.generateDocId(type);
|
|
12237
|
+
this.handle.mintedIds.add(id);
|
|
12238
|
+
const entry = await this.handle.stageWrite(Object.assign(Object.assign({}, doc), { _id: id }), "write").catch(error => {
|
|
12239
|
+
this.handle.mintedIds.delete(id);
|
|
12240
|
+
throw error;
|
|
12241
|
+
});
|
|
12242
|
+
return { ok: true, id, rev: entry.baseRev, staged: true };
|
|
12243
|
+
}
|
|
12244
|
+
/** Hard removal, staged: the commit writes `_deleted: true`. Soft deletion is `handle.deleteDocument`. */
|
|
12245
|
+
async remove(doc, rev) {
|
|
12246
|
+
const id = typeof doc === "string" ? doc : doc._id;
|
|
12247
|
+
const statedRev = typeof doc === "string" ? rev : doc._rev;
|
|
12248
|
+
const current = await overlayGet(this.stack, this.handle.stage, id).catch(() => { throw notFoundError(id); });
|
|
12249
|
+
await this.handle.stageWrite(Object.assign(Object.assign({}, current), { _id: id, _rev: statedRev !== null && statedRev !== void 0 ? statedRev : current._rev }), "delete");
|
|
12250
|
+
return { ok: true, id, staged: true };
|
|
12251
|
+
}
|
|
12252
|
+
/**
|
|
12253
|
+
* Stages a batch. Validated sequentially - the first refusal unwinds every entry
|
|
12254
|
+
* this call staged, so a failing batch stages nothing. Documents stage before
|
|
12255
|
+
* relations, mirroring the commit batch, so a relation and its endpoint can
|
|
12256
|
+
* arrive in one array in any order.
|
|
12257
|
+
*/
|
|
12258
|
+
async bulkDocs(docs, options) {
|
|
12259
|
+
const list = Array.isArray(docs) ? docs : docs === null || docs === void 0 ? void 0 : docs.docs;
|
|
12260
|
+
if (!Array.isArray(list))
|
|
12261
|
+
throw new Error("bulkDocs expects an array of documents or { docs: [...] }.");
|
|
12262
|
+
if (readNewEdits(docs, options !== null && options !== void 0 ? options : null) === false || (options === null || options === void 0 ? void 0 : options.force)) {
|
|
12263
|
+
throw new Error("Writing with 'new_edits: false' or 'force' is reserved for DocStack's sync layer, in and out of transactions.");
|
|
12264
|
+
}
|
|
12265
|
+
this.handle.assertWritable("accept writes");
|
|
12266
|
+
const stage = this.handle.stage;
|
|
12267
|
+
const before = new Map();
|
|
12268
|
+
const ordered = [
|
|
12269
|
+
...list.filter(doc => typeof doc["~domain"] !== "string"),
|
|
12270
|
+
...list.filter(doc => typeof doc["~domain"] === "string"),
|
|
12271
|
+
];
|
|
12272
|
+
try {
|
|
12273
|
+
const results = [];
|
|
12274
|
+
for (const doc of ordered) {
|
|
12275
|
+
const id = doc._id;
|
|
12276
|
+
if (typeof id === "string" && !before.has(id))
|
|
12277
|
+
before.set(id, stage.get(id));
|
|
12278
|
+
await this.handle.stageWrite(doc, "write");
|
|
12279
|
+
results.push({ ok: true, id, staged: true });
|
|
12280
|
+
}
|
|
12281
|
+
return results;
|
|
12282
|
+
}
|
|
12283
|
+
catch (error) {
|
|
12284
|
+
// Unwind this call's staging so a failed batch has no consequences at all.
|
|
12285
|
+
for (const [id, previous] of before) {
|
|
12286
|
+
if (previous)
|
|
12287
|
+
stage.set(id, previous);
|
|
12288
|
+
else
|
|
12289
|
+
stage.remove(id);
|
|
12290
|
+
}
|
|
12291
|
+
throw error;
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
}
|
|
12295
|
+
|
|
12296
|
+
const logger$1 = createLogger().child({ module: "TransactionEngine" });
|
|
12297
|
+
/**
|
|
12298
|
+
* Which adapters commit one `bulkDocs` batch as a single storage transaction.
|
|
12299
|
+
* tauri-sqlite composes the whole batch into one `BEGIN IMMEDIATE … COMMIT`;
|
|
12300
|
+
* IndexedDB and the bridge/channel adapters report per document. Reported to
|
|
12301
|
+
* consumers on every commit rather than silently assumed (ADR-0039).
|
|
12302
|
+
*/
|
|
12303
|
+
const ADAPTER_ATOMICITY = {
|
|
12304
|
+
"tauri-sqlite": true,
|
|
12305
|
+
};
|
|
12306
|
+
const now = () => { var _a, _b; return ((_b = (_a = globalThis.performance) === null || _a === void 0 ? void 0 : _a.now()) !== null && _b !== void 0 ? _b : Date.now()); };
|
|
12307
|
+
/**
|
|
12308
|
+
* Named write transactions for one stack (ADR-0039).
|
|
12309
|
+
*
|
|
12310
|
+
* Enabled per stack by `transactions: true` in its configuration - the flag only
|
|
12311
|
+
* unlocks {@link begin}; direct writes stay immediate, and the framework's own
|
|
12312
|
+
* writers (scheduler, jobs, sync) always write directly. The stage lives above the
|
|
12313
|
+
* plugin: nothing a transaction does touches the database until commit, and commit
|
|
12314
|
+
* is exactly one `stack.db.bulkDocs` through the full authoring pipeline.
|
|
12315
|
+
*/
|
|
12316
|
+
class TransactionEngine {
|
|
12317
|
+
constructor(stack, enabled) {
|
|
12318
|
+
this.handles = new Map();
|
|
12319
|
+
/** Commits serialize here so one commit's rev pre-flight cannot be invalidated by another's write. */
|
|
12320
|
+
this.commitChain = Promise.resolve();
|
|
12321
|
+
this.stack = stack;
|
|
12322
|
+
this.enabled = enabled;
|
|
12323
|
+
}
|
|
12324
|
+
isEnabled() {
|
|
12325
|
+
return this.enabled;
|
|
12326
|
+
}
|
|
12327
|
+
/** How many transactions are currently open (or partial). */
|
|
12328
|
+
openCount() {
|
|
12329
|
+
return this.handles.size;
|
|
12330
|
+
}
|
|
12331
|
+
begin() {
|
|
12332
|
+
var _a;
|
|
12333
|
+
if (!this.enabled)
|
|
12334
|
+
throw new TransactionsDisabledError((_a = this.stack.name) !== null && _a !== void 0 ? _a : "");
|
|
12335
|
+
const id = `tx-${this.stack.cryptoEngine.generateRandomString(8)}`;
|
|
12336
|
+
const handle = new TransactionHandle(this.stack, this, id);
|
|
12337
|
+
this.handles.set(id, handle);
|
|
12338
|
+
return handle;
|
|
12339
|
+
}
|
|
12340
|
+
/**
|
|
12341
|
+
* Opens a transaction for DocStack's own machinery - patch application
|
|
12342
|
+
* (ADR-0042). Independent of the `transactions: true` config gate (the flag
|
|
12343
|
+
* governs the consumer feature, not the framework's internals) and permitted to
|
|
12344
|
+
* stage class models: an internal handle claims staged validation and a single
|
|
12345
|
+
* class-write batch, never propagation atomicity.
|
|
12346
|
+
* @internal
|
|
12347
|
+
*/
|
|
12348
|
+
beginInternal() {
|
|
12349
|
+
const id = `tx-internal-${this.stack.cryptoEngine.generateRandomString(8)}`;
|
|
12350
|
+
const handle = new TransactionHandle(this.stack, this, id, true);
|
|
12351
|
+
this.handles.set(id, handle);
|
|
12352
|
+
return handle;
|
|
12353
|
+
}
|
|
12354
|
+
resolve(t, operation) {
|
|
12355
|
+
const handle = typeof t === "string" ? this.handles.get(t) : t;
|
|
12356
|
+
if (!handle)
|
|
12357
|
+
throw new TransactionStateError(typeof t === "string" ? t : "(unknown)", "unknown", operation);
|
|
12358
|
+
return handle;
|
|
12359
|
+
}
|
|
12360
|
+
/**
|
|
12361
|
+
* Drops a transaction's journal. Idempotent, and a no-op on a handle already in
|
|
12362
|
+
* a terminal state - discarding what is already gone is not an error.
|
|
12363
|
+
*/
|
|
12364
|
+
discard(t) {
|
|
12365
|
+
const handle = this.resolve(t, "be discarded");
|
|
12366
|
+
if (handle.status === "discarded" || handle.status === "committed")
|
|
12367
|
+
return;
|
|
12368
|
+
handle.stage.clear();
|
|
12369
|
+
handle.setStatus("discarded");
|
|
12370
|
+
this.handles.delete(handle.id);
|
|
12371
|
+
this.stack.dispatchEvent(new CustomEvent("transactionDiscard", { detail: { transactionId: handle.id } }));
|
|
12372
|
+
}
|
|
12373
|
+
/** Discards every open transaction - what `close()` and `reset()` do. */
|
|
12374
|
+
discardAll() {
|
|
12375
|
+
for (const handle of [...this.handles.values()])
|
|
12376
|
+
this.discard(handle);
|
|
12377
|
+
}
|
|
12378
|
+
/**
|
|
12379
|
+
* Flushes a transaction's journal as one batch through the stack's authoring
|
|
12380
|
+
* pipeline. Refusals - validation, or a staged document whose base revision
|
|
12381
|
+
* moved - throw with nothing persisted and the transaction still open.
|
|
12382
|
+
*/
|
|
12383
|
+
commit(t) {
|
|
12384
|
+
const handle = this.resolve(t, "commit");
|
|
12385
|
+
const run = () => this.commitNow(handle);
|
|
12386
|
+
const chained = this.commitChain.then(run, run);
|
|
12387
|
+
// The chain must survive a refused commit; the caller still sees the rejection.
|
|
12388
|
+
this.commitChain = chained.catch(() => undefined);
|
|
12389
|
+
return chained;
|
|
12390
|
+
}
|
|
12391
|
+
async commitNow(handle) {
|
|
12392
|
+
var _a, _b, _c, _d, _e, _f;
|
|
12393
|
+
const started = now();
|
|
12394
|
+
if (handle.status !== "open" && handle.status !== "partial") {
|
|
12395
|
+
throw new TransactionStateError(handle.id, handle.status, "commit");
|
|
12396
|
+
}
|
|
12397
|
+
const stage = handle.stage;
|
|
12398
|
+
const entries = stage.values();
|
|
12399
|
+
const stagedCount = entries.length;
|
|
12400
|
+
const report = {
|
|
12401
|
+
transactionId: handle.id,
|
|
12402
|
+
written: [],
|
|
12403
|
+
failed: [],
|
|
12404
|
+
stagedCount,
|
|
12405
|
+
durationMs: 0,
|
|
12406
|
+
adapter: this.adapterInfo(),
|
|
12407
|
+
};
|
|
12408
|
+
if (!stagedCount) {
|
|
12409
|
+
handle.setStatus("committed");
|
|
12410
|
+
this.handles.delete(handle.id);
|
|
12411
|
+
report.durationMs = now() - started;
|
|
12412
|
+
return report;
|
|
12413
|
+
}
|
|
12414
|
+
// 1. The sweep again, against the world as it stands now - stage-time answers
|
|
12415
|
+
// can be stale (a policy changed, a class tightened). Zero consequences on
|
|
12416
|
+
// refusal.
|
|
12417
|
+
for (const entry of entries) {
|
|
12418
|
+
await sweepEntry(this.stack, stage, entry, { allowClassModels: handle.internal, skipPolicy: handle.internal });
|
|
12419
|
+
}
|
|
12420
|
+
// 2. Rev pre-flight: every staged id's stored winner must still be the
|
|
12421
|
+
// revision it was staged against. `allDocs` is below the plugin - revs
|
|
12422
|
+
// only, no decrypt cost.
|
|
12423
|
+
const ids = stage.ids();
|
|
12424
|
+
const stored = await this.stack.db.allDocs({ keys: ids });
|
|
12425
|
+
const currentRevs = new Map();
|
|
12426
|
+
for (const row of (_a = stored.rows) !== null && _a !== void 0 ? _a : []) {
|
|
12427
|
+
const id = (_b = row.key) !== null && _b !== void 0 ? _b : row.id;
|
|
12428
|
+
currentRevs.set(id, row.value && !row.value.deleted ? row.value.rev : undefined);
|
|
12429
|
+
}
|
|
12430
|
+
const conflicts = [];
|
|
12431
|
+
for (const id of ids) {
|
|
12432
|
+
const entry = stage.get(id);
|
|
12433
|
+
const current = currentRevs.get(id);
|
|
12434
|
+
const expected = entry.isNew ? undefined : entry.baseRev;
|
|
12435
|
+
if (current !== expected)
|
|
12436
|
+
conflicts.push({ id, baseRev: entry.baseRev, currentRev: current });
|
|
12437
|
+
}
|
|
12438
|
+
if (conflicts.length)
|
|
12439
|
+
throw new TransactionConflictError(conflicts);
|
|
12440
|
+
// 3. One batch: stage order, documents before relations so endpoint checks
|
|
12441
|
+
// can resolve batch-mates.
|
|
12442
|
+
const toBatchDoc = (id) => {
|
|
12443
|
+
const entry = stage.get(id);
|
|
12444
|
+
const doc = structuredClone(entry.doc);
|
|
12445
|
+
if (entry.baseRev)
|
|
12446
|
+
doc._rev = entry.baseRev;
|
|
12447
|
+
else
|
|
12448
|
+
delete doc._rev;
|
|
12449
|
+
if (entry.op === "delete")
|
|
12450
|
+
doc._deleted = true;
|
|
12451
|
+
return doc;
|
|
12452
|
+
};
|
|
12453
|
+
const batchIds = [
|
|
12454
|
+
...ids.filter(id => typeof stage.get(id).doc["~domain"] !== "string"),
|
|
12455
|
+
...ids.filter(id => typeof stage.get(id).doc["~domain"] === "string"),
|
|
12456
|
+
];
|
|
12457
|
+
const batch = batchIds.map(toBatchDoc);
|
|
12458
|
+
// 4. The full authoring pipeline, unchanged: triggers, relation checks,
|
|
12459
|
+
// encryption, and the single adapter write. A pipeline refusal rejects the
|
|
12460
|
+
// whole call before anything is written.
|
|
12461
|
+
const response = await this.stack.db.bulkDocs(batch);
|
|
12462
|
+
const failedIds = new Set();
|
|
12463
|
+
for (let index = 0; index < response.length; index++) {
|
|
12464
|
+
const result = response[index];
|
|
12465
|
+
const id = (_c = result === null || result === void 0 ? void 0 : result.id) !== null && _c !== void 0 ? _c : batchIds[index];
|
|
12466
|
+
if (result && result.ok) {
|
|
12467
|
+
report.written.push({ id, rev: result.rev });
|
|
12468
|
+
}
|
|
12469
|
+
else {
|
|
12470
|
+
failedIds.add(id);
|
|
12471
|
+
report.failed.push({
|
|
12472
|
+
id,
|
|
12473
|
+
error: (_e = (_d = result === null || result === void 0 ? void 0 : result.message) !== null && _d !== void 0 ? _d : result === null || result === void 0 ? void 0 : result.reason) !== null && _e !== void 0 ? _e : String((_f = result === null || result === void 0 ? void 0 : result.name) !== null && _f !== void 0 ? _f : "write failed"),
|
|
12474
|
+
name: result === null || result === void 0 ? void 0 : result.name,
|
|
12475
|
+
});
|
|
12476
|
+
}
|
|
12477
|
+
}
|
|
12478
|
+
// 5. The id counter advances only for minted ids that actually landed -
|
|
12479
|
+
// uniqueness is the promise, not density (as in createDocs).
|
|
12480
|
+
const mintedWritten = report.written.filter(entry => handle.mintedIds.has(entry.id)).length;
|
|
12481
|
+
if (mintedWritten > 0) {
|
|
12482
|
+
await this.stack.advanceLastDocId(mintedWritten);
|
|
12483
|
+
}
|
|
12484
|
+
if (failedIds.size === 0) {
|
|
12485
|
+
stage.clear();
|
|
12486
|
+
handle.setStatus("committed");
|
|
12487
|
+
this.handles.delete(handle.id);
|
|
12488
|
+
}
|
|
12489
|
+
else {
|
|
12490
|
+
// Possible only on adapters without an atomic batch (or a write racing
|
|
12491
|
+
// the pre-flight window). Failed entries keep their base revisions: a
|
|
12492
|
+
// straggler that simply didn't land retries cleanly, and one that was
|
|
12493
|
+
// raced surfaces as a conflict on the next commit instead of being
|
|
12494
|
+
// silently overwritten.
|
|
12495
|
+
stage.retain(failedIds);
|
|
12496
|
+
handle.setStatus("partial");
|
|
12497
|
+
logger$1.warn("Transaction committed partially", { transactionId: handle.id, failed: report.failed });
|
|
12498
|
+
}
|
|
12499
|
+
report.durationMs = now() - started;
|
|
12500
|
+
this.stack.dispatchEvent(new CustomEvent("transactionCommit", { detail: report }));
|
|
12501
|
+
return report;
|
|
12502
|
+
}
|
|
12503
|
+
adapterInfo() {
|
|
12504
|
+
var _a, _b;
|
|
12505
|
+
const name = String((_b = (_a = this.stack.rawDb) === null || _a === void 0 ? void 0 : _a.adapter) !== null && _b !== void 0 ? _b : "unknown");
|
|
12506
|
+
const atomicBatch = ADAPTER_ATOMICITY[name] === true;
|
|
12507
|
+
if (!(name in ADAPTER_ATOMICITY) && name !== "idb" && name !== "indexeddb") {
|
|
12508
|
+
logger$1.warn("Unknown adapter for transaction atomicity - reporting atomicBatch: false", { adapter: name });
|
|
12509
|
+
}
|
|
12510
|
+
return { name, atomicBatch };
|
|
12511
|
+
}
|
|
12512
|
+
}
|
|
12513
|
+
|
|
12514
|
+
/**
|
|
12515
|
+
* Moving a stack's *content* between databases, without its datamodel.
|
|
12516
|
+
*
|
|
12517
|
+
* `stack.dump()` is the other kind of export: every document exactly as stored, which
|
|
12518
|
+
* means class models, patches, users, sessions, policies, design documents - and
|
|
12519
|
+
* encrypted attributes as unreadable {@link EncryptedPayload} blobs. That is a debugging
|
|
12520
|
+
* tool and a backup of one database.
|
|
12521
|
+
*
|
|
12522
|
+
* This is the portable one. It carries the documents an application put in, in the clear,
|
|
12523
|
+
* so that {@link ClientStack.importContent} can place them into a *different* stack -
|
|
12524
|
+
* different device, different key, schema built by that stack's own patches.
|
|
12525
|
+
*
|
|
12526
|
+
* @module
|
|
12527
|
+
*/
|
|
12528
|
+
/** The envelope format identifier, so an importer can refuse what it does not understand. */
|
|
12529
|
+
const CONTENT_EXPORT_FORMAT = "docstack/content-export@1";
|
|
12530
|
+
/**
|
|
12531
|
+
* `~class` values that describe the stack rather than hold its content.
|
|
12532
|
+
*
|
|
12533
|
+
* `~self` is the bootstrap class-of-classes; `class` and `superclass` are class models;
|
|
12534
|
+
* `domain` is a relation definition; `patch` is the local ledger of applied patches.
|
|
12535
|
+
*/
|
|
12536
|
+
const META_CLASSES = ["~self", "class", "superclass", "domain", "patch"];
|
|
12537
|
+
/**
|
|
12538
|
+
* Reports whether a class name belongs to application content.
|
|
12539
|
+
*
|
|
12540
|
+
* DocStack names everything it owns with a leading `~` - `~User`, `~Group`, `~Policy`,
|
|
12541
|
+
* `~Job`, `~JobRun`, `~UserSession`, `~AuthModule`, `~lock` - and reserves the handful of
|
|
12542
|
+
* unprefixed names in {@link META_CLASSES} for the datamodel itself. Everything else was
|
|
12543
|
+
* created by an application.
|
|
12544
|
+
*
|
|
12545
|
+
* @param className - A `~class` value or class-model id.
|
|
12546
|
+
*
|
|
12547
|
+
* @example
|
|
12548
|
+
* ```typescript
|
|
12549
|
+
* isContentClassName("Task"); // true
|
|
12550
|
+
* isContentClassName("~User"); // false - DocStack's own
|
|
12551
|
+
* isContentClassName("class"); // false - a class model
|
|
12552
|
+
* ```
|
|
12553
|
+
*/
|
|
12554
|
+
const isContentClassName = (className) => {
|
|
12555
|
+
if (typeof className !== "string" || !className.length)
|
|
12556
|
+
return false;
|
|
12557
|
+
if (className.startsWith("~"))
|
|
12558
|
+
return false;
|
|
12559
|
+
return !META_CLASSES.includes(className);
|
|
12560
|
+
};
|
|
12561
|
+
/**
|
|
12562
|
+
* Reports whether a document is application content rather than part of the stack.
|
|
12563
|
+
*
|
|
12564
|
+
* @param doc - Any stored document.
|
|
12565
|
+
*/
|
|
12566
|
+
const isContentDocument = (doc) => {
|
|
12567
|
+
if (!doc || typeof doc._id !== "string")
|
|
12568
|
+
return false;
|
|
12569
|
+
// A design document has no `~class` and would fall through the class test.
|
|
12570
|
+
if (doc._id.startsWith("_"))
|
|
12571
|
+
return false;
|
|
12572
|
+
return isContentClassName(doc["~class"]);
|
|
12573
|
+
};
|
|
12574
|
+
/**
|
|
12575
|
+
* Reports whether a document is an application relation.
|
|
12576
|
+
*
|
|
12577
|
+
* Relations carry `~domain` and no `~class` (see `isRelation` in `@docstack/shared`), so
|
|
12578
|
+
* they need their own test rather than falling out of {@link isContentDocument}.
|
|
12579
|
+
*
|
|
12580
|
+
* @param doc - Any stored document.
|
|
12581
|
+
*/
|
|
12582
|
+
const isContentRelation = (doc) => {
|
|
12583
|
+
if (!doc || typeof doc._id !== "string")
|
|
12584
|
+
return false;
|
|
12585
|
+
if (doc["~class"] !== undefined)
|
|
12586
|
+
return false;
|
|
12587
|
+
return isContentClassName(doc["~domain"]);
|
|
12588
|
+
};
|
|
12589
|
+
/** Fields PouchDB owns, or that describe one database's copy of a document. */
|
|
12590
|
+
const TRANSIENT_FIELDS = ["_rev", "_revisions", "_revs_info", "_conflicts", "_deleted"];
|
|
12591
|
+
/**
|
|
12592
|
+
* Strips the fields that belong to the source database rather than to the document.
|
|
12593
|
+
*
|
|
12594
|
+
* `_rev` above all: a revision from one database means nothing in another, and carrying
|
|
12595
|
+
* it into an import turns every write into a conflict.
|
|
12596
|
+
*
|
|
12597
|
+
* @param doc - The document to clean.
|
|
12598
|
+
* @returns A copy without the transient fields.
|
|
12599
|
+
*/
|
|
12600
|
+
const stripTransientFields = (doc) => {
|
|
12601
|
+
const clone = Object.assign({}, doc);
|
|
12602
|
+
for (const field of TRANSIENT_FIELDS)
|
|
12603
|
+
delete clone[field];
|
|
12604
|
+
return clone;
|
|
12605
|
+
};
|
|
12606
|
+
/**
|
|
12607
|
+
* Validates an import payload's envelope before anything is written.
|
|
12608
|
+
*
|
|
12609
|
+
* A plain validator rather than an `asserts` signature: the caller already types its
|
|
12610
|
+
* parameter, so there is nothing to narrow, and an assertion function reached through an
|
|
12611
|
+
* import needs a declaration TypeScript can see (TS2775).
|
|
12612
|
+
*
|
|
12613
|
+
* @param payload - The value handed to {@link ClientStack.importContent}.
|
|
12614
|
+
* @throws Error when it is not a content export this version understands.
|
|
12615
|
+
*/
|
|
12616
|
+
const assertContentExport = (payload) => {
|
|
12617
|
+
const value = payload;
|
|
12618
|
+
if (!value || typeof value !== "object") {
|
|
12619
|
+
throw new Error("importContent - payload is not a content export.");
|
|
12620
|
+
}
|
|
12621
|
+
if (value.format !== CONTENT_EXPORT_FORMAT) {
|
|
9377
12622
|
throw new Error(`importContent - unsupported format '${String(value.format)}'. ` +
|
|
9378
12623
|
`This build reads '${CONTENT_EXPORT_FORMAT}'.`);
|
|
9379
12624
|
}
|
|
@@ -9397,6 +12642,8 @@ const DOCSTACK_OPTION_KEYS = [
|
|
|
9397
12642
|
"credentials",
|
|
9398
12643
|
"disableCryptoEngine",
|
|
9399
12644
|
"documentKey",
|
|
12645
|
+
"transactions",
|
|
12646
|
+
"logLevel",
|
|
9400
12647
|
];
|
|
9401
12648
|
/**
|
|
9402
12649
|
* Extracts the PouchDB half of a {@link StackOptions} object.
|
|
@@ -9947,45 +13194,52 @@ class ClientStack extends Stack {
|
|
|
9947
13194
|
};
|
|
9948
13195
|
this.applyPatch = async (patch) => {
|
|
9949
13196
|
const fnLogger = logger.child({ method: "applyPatch", args: { patch } });
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
|
|
9966
|
-
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
9972
|
-
|
|
9973
|
-
fnLogger.warn("applyPatch - bulkDocs completed with result", { result });
|
|
9974
|
-
fnLogger.warn("Successfully processed patch", { version: patch.version });
|
|
9975
|
-
}).catch((error) => {
|
|
9976
|
-
fnLogger.error("applyPatch - bulkDocs error", { error });
|
|
9977
|
-
reject(error);
|
|
9978
|
-
});
|
|
9979
|
-
// Store patch itself
|
|
9980
|
-
await this.db.post(Object.assign({ createTimestamp: (new Date()).valueOf() }, patch));
|
|
9981
|
-
fnLogger.info("Successfully stored patch", { version: patch.version, target: patch.target });
|
|
9982
|
-
resolve(patch.version);
|
|
9983
|
-
}
|
|
9984
|
-
catch (e) {
|
|
9985
|
-
fnLogger.error("Failed to apply patch", e);
|
|
9986
|
-
reject(new Error(e));
|
|
13197
|
+
if (patch.preApply || patch.postApply) {
|
|
13198
|
+
// Jobs stage through the chain transaction; this path has none, and
|
|
13199
|
+
// silently ignoring them would apply the model without its migration.
|
|
13200
|
+
throw new Error(`applyPatch - patch '${patch.version}' carries one-shot jobs; ` +
|
|
13201
|
+
`job-carrying patches apply through the configured patch chain (ADR-0044).`);
|
|
13202
|
+
}
|
|
13203
|
+
fnLogger.info("Attempting to apply patch", { patch });
|
|
13204
|
+
fnLogger.info("applyPatch - starting to hydrate patch docs", { docCount: patch.docs.length });
|
|
13205
|
+
const hydratedDocs = await Promise.all(patch.docs.map(async (sourceDoc) => {
|
|
13206
|
+
// Work on a copy: the patch definition must survive intact. System
|
|
13207
|
+
// patches are module-level objects shared by every stack in the
|
|
13208
|
+
// process, so deleting `_rev` from one would strip the "auto" marker
|
|
13209
|
+
// permanently — the next ClientStack created in the same context
|
|
13210
|
+
// would then rewrite those documents as fresh inserts, hit a 409,
|
|
13211
|
+
// and end up without its `class` class model.
|
|
13212
|
+
let doc = Object.assign({}, sourceDoc);
|
|
13213
|
+
if (doc._rev === "auto") {
|
|
13214
|
+
delete doc._rev;
|
|
13215
|
+
const existingDoc = await this.db.get(doc._id);
|
|
13216
|
+
if (existingDoc) {
|
|
13217
|
+
doc = Object.assign(Object.assign({}, existingDoc), doc);
|
|
13218
|
+
ClientStack.mergePatchSchema(sourceDoc, existingDoc, doc);
|
|
13219
|
+
}
|
|
9987
13220
|
}
|
|
9988
|
-
|
|
13221
|
+
return doc;
|
|
13222
|
+
}));
|
|
13223
|
+
fnLogger.info("applyPatch - hydration complete, calling bulkDocs", { docCount: hydratedDocs.length });
|
|
13224
|
+
// A failing batch throws here and nothing below runs: the ledger records the
|
|
13225
|
+
// moment of SUCCESSFUL application, never the attempt. The old flow rejected
|
|
13226
|
+
// on failure but kept executing, so a failed patch was recorded as applied
|
|
13227
|
+
// and never retried - a device whose schema trailed forever (ADR-0041).
|
|
13228
|
+
const result = await this.db.bulkDocs(hydratedDocs, { isPatch: true });
|
|
13229
|
+
// Per-document failures arrive in the RESOLVED array (the ADR-0038 discipline):
|
|
13230
|
+
// a patch whose batch half-landed must not arm its ledger entry, or the next
|
|
13231
|
+
// open would skip it and apply its dependents against a schema it never
|
|
13232
|
+
// finished installing. The retry converges - hydration re-merges over whatever
|
|
13233
|
+
// did land.
|
|
13234
|
+
const failures = result.filter(entry => entry && entry.error);
|
|
13235
|
+
if (failures.length) {
|
|
13236
|
+
throw new Error(`applyPatch - patch '${patch.version}' failed for ${failures.length} document(s): ` +
|
|
13237
|
+
failures.map((f) => { var _a; return `${f.id}: ${(_a = f.message) !== null && _a !== void 0 ? _a : f.name}`; }).join("; "));
|
|
13238
|
+
}
|
|
13239
|
+
fnLogger.info("applyPatch - bulkDocs completed", { result });
|
|
13240
|
+
await this.recordPatchApplication(patch);
|
|
13241
|
+
fnLogger.info("Successfully applied and recorded patch", { version: patch.version, target: patch.target });
|
|
13242
|
+
return patch.version;
|
|
9989
13243
|
};
|
|
9990
13244
|
this.setListeners = () => {
|
|
9991
13245
|
const fnLogger = logger.child({ method: "setListeners" });
|
|
@@ -10469,15 +13723,46 @@ class ClientStack extends Stack {
|
|
|
10469
13723
|
* Removes event listeners and terminates background workers.
|
|
10470
13724
|
*/
|
|
10471
13725
|
this.close = () => {
|
|
10472
|
-
var _a;
|
|
13726
|
+
var _a, _b;
|
|
13727
|
+
// First, and before the listeners go, so the discard events can still be
|
|
13728
|
+
// observed: an uncommitted transaction is not real, and close drops it -
|
|
13729
|
+
// database-transaction semantics, by decision (ADR-0039).
|
|
13730
|
+
(_a = this.transactionEngine) === null || _a === void 0 ? void 0 : _a.discardAll();
|
|
10473
13731
|
this.cancelSync();
|
|
10474
13732
|
// Before the listeners go: a surviving interval would tick against a database
|
|
10475
13733
|
// this stack no longer serves.
|
|
10476
|
-
(
|
|
13734
|
+
(_b = this.jobScheduler) === null || _b === void 0 ? void 0 : _b.stop();
|
|
10477
13735
|
this.removeAllListeners();
|
|
10478
13736
|
if (this.modelWorker)
|
|
10479
13737
|
this.modelWorker.terminate();
|
|
10480
13738
|
};
|
|
13739
|
+
/**
|
|
13740
|
+
* Opens a named write transaction (ADR-0039). Requires the stack to have been
|
|
13741
|
+
* opened with `transactions: true`.
|
|
13742
|
+
*
|
|
13743
|
+
* Writes through the handle validate at the call site and stage in memory;
|
|
13744
|
+
* reads through it see the staged state overlaid on committed state. Nothing
|
|
13745
|
+
* reaches the database - or replication, or any other reader - until
|
|
13746
|
+
* {@link commit}. `stack.db` stays live and unchanged next to open transactions:
|
|
13747
|
+
* direct writes land immediately, and only touch a transaction by making its
|
|
13748
|
+
* commit refuse when they advance a staged document's revision.
|
|
13749
|
+
*/
|
|
13750
|
+
this.beginTransaction = () => {
|
|
13751
|
+
return this.transactionEngine.begin();
|
|
13752
|
+
};
|
|
13753
|
+
/**
|
|
13754
|
+
* Flushes a transaction's staged writes as one batch through the authoring
|
|
13755
|
+
* pipeline. On refusal - validation, or a document changed underneath - nothing
|
|
13756
|
+
* is persisted and the transaction stays open. The report says what landed and
|
|
13757
|
+
* on what storage guarantee (`adapter.atomicBatch`).
|
|
13758
|
+
*/
|
|
13759
|
+
this.commit = (t) => {
|
|
13760
|
+
return this.transactionEngine.commit(t);
|
|
13761
|
+
};
|
|
13762
|
+
/** Drops a transaction's staged writes. Idempotent. */
|
|
13763
|
+
this.discardTransaction = (t) => {
|
|
13764
|
+
this.transactionEngine.discard(t);
|
|
13765
|
+
};
|
|
10481
13766
|
/**
|
|
10482
13767
|
* Retrieves a Class instance by name.
|
|
10483
13768
|
* Results are cached for 15 minutes to improve performance.
|
|
@@ -10598,11 +13883,33 @@ class ClientStack extends Stack {
|
|
|
10598
13883
|
* ```
|
|
10599
13884
|
*/
|
|
10600
13885
|
this.findDocuments = async (selector, fields, skip, limit, sort) => {
|
|
13886
|
+
return this.findDocumentsForView(undefined, selector, fields, skip, limit, sort);
|
|
13887
|
+
};
|
|
13888
|
+
/**
|
|
13889
|
+
* {@link findDocuments} with an optional transaction stage overlaid - the shared
|
|
13890
|
+
* implementation, so a transaction's reads and ordinary reads run the identical
|
|
13891
|
+
* pipeline (policy, decryption, field visibility) and cannot drift (ADR-0039).
|
|
13892
|
+
*
|
|
13893
|
+
* With a stage: the database's index cannot see staged documents, so the
|
|
13894
|
+
* committed query runs unwindowed, staged ids mask their committed rows, staged
|
|
13895
|
+
* matches join the set, and sort/skip/limit apply after the merge. A selector
|
|
13896
|
+
* over a class the stage never touched skips all of that.
|
|
13897
|
+
*
|
|
13898
|
+
* @internal
|
|
13899
|
+
*/
|
|
13900
|
+
this.findDocumentsForView = async (stage, selector, fields, skip, limit, sort) => {
|
|
10601
13901
|
const fnLogger = logger.child({ method: "findDocuments", args: { selector, fields, skip, limit, sort } });
|
|
10602
13902
|
// By default request for only active documents
|
|
10603
13903
|
if (!selector.hasOwnProperty("active")) {
|
|
10604
13904
|
selector["active"] = true;
|
|
10605
13905
|
}
|
|
13906
|
+
if (stage && stageCoversSelector(stage, selector)) {
|
|
13907
|
+
const { queryFields, extras } = widenProjection(fields, sort);
|
|
13908
|
+
const found = await this.db.find(Object.assign(Object.assign({ selector }, (queryFields ? { fields: queryFields } : {})), { limit: 2 ** 31 - 1 }));
|
|
13909
|
+
const merged = mergeStageIntoResults(stage, selector, found.docs, { sort, skip, limit, fields, extras });
|
|
13910
|
+
const readableDocs = await this.processFoundDocuments(merged, fields);
|
|
13911
|
+
return { docs: readableDocs, selector, skip, limit };
|
|
13912
|
+
}
|
|
10606
13913
|
let indexFields = Object.keys(selector);
|
|
10607
13914
|
fnLogger.info("Produced index fields from selector", { indexFields });
|
|
10608
13915
|
let result = {
|
|
@@ -11486,6 +14793,17 @@ class ClientStack extends Stack {
|
|
|
11486
14793
|
}
|
|
11487
14794
|
};
|
|
11488
14795
|
this.query = async (sql, ...params) => {
|
|
14796
|
+
return this.runQuery(sql, params, this);
|
|
14797
|
+
};
|
|
14798
|
+
/**
|
|
14799
|
+
* {@link query}'s implementation, with the executor's data source as a
|
|
14800
|
+
* parameter: the executor reaches documents only through stack APIs, so a
|
|
14801
|
+
* transaction hands in a facade that routes them at its overlay while everything
|
|
14802
|
+
* else - parsing, binding, planning - stays exactly this code (ADR-0039).
|
|
14803
|
+
*
|
|
14804
|
+
* @internal
|
|
14805
|
+
*/
|
|
14806
|
+
this.runQuery = async (sql, params, execStack) => {
|
|
11489
14807
|
const fnLogger = logger.child({ method: "query", args: { sql, params } });
|
|
11490
14808
|
fnLogger.info("Executing query");
|
|
11491
14809
|
let astList = [];
|
|
@@ -11502,7 +14820,7 @@ class ClientStack extends Stack {
|
|
|
11502
14820
|
if (astList.length > 0) {
|
|
11503
14821
|
try {
|
|
11504
14822
|
const plan = createPlan(astList);
|
|
11505
|
-
const rows = await executePlan(
|
|
14823
|
+
const rows = await executePlan(execStack, plan, params);
|
|
11506
14824
|
// The AST for the whole query (including unions) is the list
|
|
11507
14825
|
fnLogger.info("Query executed successfully", { rows, astList });
|
|
11508
14826
|
return { rows, ast: astList };
|
|
@@ -11605,6 +14923,9 @@ class ClientStack extends Stack {
|
|
|
11605
14923
|
this.jobScheduler = new JobScheduler(this);
|
|
11606
14924
|
this.policyEngine = new PolicyEngine(this);
|
|
11607
14925
|
this.cryptoEngine = new CryptoEngine(this);
|
|
14926
|
+
// Re-created, never carried over: `reset()` re-runs initialize, and a stage
|
|
14927
|
+
// surviving a reset would resurrect uncommitted writes (ADR-0039).
|
|
14928
|
+
this.transactionEngine = new TransactionEngine(this, Boolean(options === null || options === void 0 ? void 0 : options.transactions));
|
|
11608
14929
|
if (options === null || options === void 0 ? void 0 : options.documentKey) {
|
|
11609
14930
|
await this.cryptoEngine.setDocumentKey(options.documentKey);
|
|
11610
14931
|
}
|
|
@@ -11942,11 +15263,18 @@ class ClientStack extends Stack {
|
|
|
11942
15263
|
await stack.initialize(conn, options);
|
|
11943
15264
|
await stack.initdb();
|
|
11944
15265
|
if ((options === null || options === void 0 ? void 0 : options.patches) && options.patches.length) {
|
|
11945
|
-
|
|
11946
|
-
|
|
15266
|
+
// Raw find, not findDocuments: `active` carries the ledger's own meaning
|
|
15267
|
+
// (ADR-0041) - `true` is applied, `false` is deferred and must be
|
|
15268
|
+
// re-attempted, absent is a legacy entry from before the flag, treated as
|
|
15269
|
+
// applied. Read through the visibility filter, the dedupe saw nothing at
|
|
15270
|
+
// all and every open re-applied every consumer patch (ADR-0040).
|
|
15271
|
+
const ledger = await stack.db.find({
|
|
15272
|
+
selector: { "~class": "patch" },
|
|
15273
|
+
limit: 2 ** 31 - 1,
|
|
11947
15274
|
});
|
|
11948
|
-
await stack.applyConsumerPatches(options.patches.filter(p => !
|
|
11949
|
-
&& existing.target === p.target
|
|
15275
|
+
await stack.applyConsumerPatches(options.patches.filter(p => !ledger.docs.find(existing => existing.version === p.version
|
|
15276
|
+
&& existing.target === p.target
|
|
15277
|
+
&& existing.active !== false)));
|
|
11950
15278
|
}
|
|
11951
15279
|
if (options === null || options === void 0 ? void 0 : options.credentials) {
|
|
11952
15280
|
await stack.authenticate(options.credentials);
|
|
@@ -11967,21 +15295,419 @@ class ClientStack extends Stack {
|
|
|
11967
15295
|
*
|
|
11968
15296
|
* @param patches - Patches not yet present in this stack.
|
|
11969
15297
|
*/
|
|
15298
|
+
/**
|
|
15299
|
+
* The highest consumer patch version this device has applied, from the patch
|
|
15300
|
+
* ledger - `null` when no consumer patch has ever applied (or they are all
|
|
15301
|
+
* deferred, which for the schema gate is the same thing: the schema those
|
|
15302
|
+
* patches install is not here yet). The sync layer folds this into what it
|
|
15303
|
+
* publishes and compares, so consumer-schema skew between devices refuses at
|
|
15304
|
+
* the gate instead of pulling documents this device's schema cannot describe
|
|
15305
|
+
* (ADR-0040).
|
|
15306
|
+
*/
|
|
15307
|
+
async getConsumerSchemaVersion() {
|
|
15308
|
+
// Raw find: ledger documents carry no `active` flag (see the dedupe in
|
|
15309
|
+
// {@link create}).
|
|
15310
|
+
const ledger = await this.db.find({
|
|
15311
|
+
selector: { "~class": "patch" },
|
|
15312
|
+
limit: 2 ** 31 - 1,
|
|
15313
|
+
}).catch(() => ({ docs: [] }));
|
|
15314
|
+
let highest = null;
|
|
15315
|
+
for (const doc of ledger.docs) {
|
|
15316
|
+
if (doc.target === "system")
|
|
15317
|
+
continue;
|
|
15318
|
+
// A dormant entry is a deferral (ADR-0041): the patch is known, its
|
|
15319
|
+
// schema is not here - counting it would tell the sync gate this device
|
|
15320
|
+
// is current and let it pull documents it cannot describe.
|
|
15321
|
+
if (doc.active === false)
|
|
15322
|
+
continue;
|
|
15323
|
+
const version = doc.version;
|
|
15324
|
+
if (typeof version !== "string" || !semver.valid(version))
|
|
15325
|
+
continue;
|
|
15326
|
+
if (!highest || semver.gt(version, highest))
|
|
15327
|
+
highest = version;
|
|
15328
|
+
}
|
|
15329
|
+
return highest;
|
|
15330
|
+
}
|
|
11970
15331
|
async applyConsumerPatches(patches) {
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
15332
|
+
if (!patches.length) {
|
|
15333
|
+
this.deferredPatches = [];
|
|
15334
|
+
return;
|
|
15335
|
+
}
|
|
15336
|
+
// EVERY consumer chain - class models, data documents, jobs, mixed - stages
|
|
15337
|
+
// through the one internal transaction (ADR-0042). The original protocol
|
|
15338
|
+
// scoped itself to class-only chains and kept a sequential fallback for the
|
|
15339
|
+
// rest; the mixed extension it recorded turned out to be already built (the
|
|
15340
|
+
// sweep judges data docs by staged models since ADR-0044, and the pipeline
|
|
15341
|
+
// resolves batch-mates regardless of order since ADR-0043), so the fork was
|
|
15342
|
+
// deleted rather than extended. A data-only chain gains the same contract:
|
|
15343
|
+
// all-or-nothing, nothing armed unless the whole chain lands.
|
|
15344
|
+
return this.applyConsumerPatchChain(patches);
|
|
15345
|
+
}
|
|
15346
|
+
/**
|
|
15347
|
+
* The stack a patch job executes against (ADR-0044): reads see the chain
|
|
15348
|
+
* transaction's overlay, writes stage into it - so a migration's data
|
|
15349
|
+
* transformation lands in the same commit as the model it prepares, or not at
|
|
15350
|
+
* all. While the stack is locked, class-aware reads of an encrypting class
|
|
15351
|
+
* THROW instead of serving the null convention: a migration wants the refusal
|
|
15352
|
+
* (a `requiresKey: false` job that was declared wrongly must fail loudly, and
|
|
15353
|
+
* the chain converts that failure to a deferral). Raw reads (`db.find`) bypass
|
|
15354
|
+
* the class-aware path by design and stay the author's responsibility.
|
|
15355
|
+
*/
|
|
15356
|
+
createPatchJobStack(t) {
|
|
15357
|
+
const stack = this;
|
|
15358
|
+
const resolveClass = async (className) => {
|
|
15359
|
+
var _a;
|
|
15360
|
+
return (_a = classFromStage(stack, t.stage, className)) !== null && _a !== void 0 ? _a : await stack.getClassSnapshot(className).catch(() => null);
|
|
15361
|
+
};
|
|
15362
|
+
const assertReadableWhileLocked = async (className) => {
|
|
15363
|
+
if (!stack.isLocked() || typeof className !== "string" || !className)
|
|
11980
15364
|
return;
|
|
15365
|
+
const classObj = await resolveClass(className);
|
|
15366
|
+
if (classObj && classObj.getEncryptedAttributes().length) {
|
|
15367
|
+
throw new StackLockedError(className);
|
|
15368
|
+
}
|
|
15369
|
+
};
|
|
15370
|
+
const selectorClassNames = (selector) => {
|
|
15371
|
+
const constraint = selector === null || selector === void 0 ? void 0 : selector["~class"];
|
|
15372
|
+
if (typeof constraint === "string")
|
|
15373
|
+
return [constraint];
|
|
15374
|
+
if (constraint && typeof constraint === "object") {
|
|
15375
|
+
if (typeof constraint.$eq === "string")
|
|
15376
|
+
return [constraint.$eq];
|
|
15377
|
+
if (Array.isArray(constraint.$in))
|
|
15378
|
+
return constraint.$in.filter((name) => typeof name === "string");
|
|
11981
15379
|
}
|
|
11982
|
-
|
|
15380
|
+
return [];
|
|
15381
|
+
};
|
|
15382
|
+
// Reads are SYSTEM-LEVEL: raw overlay plus decrypt-when-keyed, never the
|
|
15383
|
+
// policy-filtered pipeline. A migration runs before any session exists,
|
|
15384
|
+
// and a policy-filtered massage would silently transform only the subset
|
|
15385
|
+
// an absent session could see - the patch path's own direct writes never
|
|
15386
|
+
// pass through policy either.
|
|
15387
|
+
const openWhenKeyed = async (doc) => {
|
|
15388
|
+
var _a;
|
|
15389
|
+
if (!doc || !((_a = stack.cryptoEngine) === null || _a === void 0 ? void 0 : _a.isEnabled()) || !stack.cryptoEngine.getDocumentKey())
|
|
15390
|
+
return doc;
|
|
15391
|
+
if (!stack.cryptoEngine.identifyEncryptedKeys(doc).length)
|
|
15392
|
+
return doc;
|
|
15393
|
+
const classObj = await stack.getClassSnapshot(doc["~class"]).catch(() => null);
|
|
15394
|
+
if (classObj)
|
|
15395
|
+
await stack.cryptoEngine.decryptDocument(doc, classObj);
|
|
15396
|
+
return doc;
|
|
15397
|
+
};
|
|
15398
|
+
const facade = Object.create(stack);
|
|
15399
|
+
facade.findDocuments = async (selector, fields, skip, limit, sort) => {
|
|
15400
|
+
for (const name of selectorClassNames(selector))
|
|
15401
|
+
await assertReadableWhileLocked(name);
|
|
15402
|
+
const withDefault = (selector === null || selector === void 0 ? void 0 : selector.hasOwnProperty("active")) ? selector : Object.assign(Object.assign({}, selector), { active: true });
|
|
15403
|
+
const { docs } = await t.db.find({ selector: withDefault, fields, skip, limit, sort });
|
|
15404
|
+
for (const doc of docs)
|
|
15405
|
+
await openWhenKeyed(doc);
|
|
15406
|
+
return { docs, selector: withDefault, skip, limit };
|
|
15407
|
+
};
|
|
15408
|
+
facade.findDocumentsIterator = function (selector_1) {
|
|
15409
|
+
return __asyncGenerator(this, arguments, function* (selector, options = {}) {
|
|
15410
|
+
const result = yield __await(facade.findDocuments(selector, options.fields));
|
|
15411
|
+
for (const doc of result.docs)
|
|
15412
|
+
yield yield __await(doc);
|
|
15413
|
+
});
|
|
15414
|
+
};
|
|
15415
|
+
facade.canApplyQueryLimitEarly = async () => false;
|
|
15416
|
+
facade.ensureSortIndex = async () => false;
|
|
15417
|
+
facade.query = (sql, ...params) => stack.runQuery(sql, params, facade);
|
|
15418
|
+
facade.getDocument = async (docId) => {
|
|
15419
|
+
const doc = await t.db.get(docId);
|
|
15420
|
+
await assertReadableWhileLocked(doc === null || doc === void 0 ? void 0 : doc["~class"]);
|
|
15421
|
+
return openWhenKeyed(doc);
|
|
15422
|
+
};
|
|
15423
|
+
facade.createDoc = (docId, type, _classObj, params) => t.createDoc(docId, type, params);
|
|
15424
|
+
facade.createDocs = (docs, type) => t.createDocs(docs, type);
|
|
15425
|
+
facade.deleteDocument = (docId) => t.deleteDocument(docId);
|
|
15426
|
+
facade.getClass = async (name, ...rest) => {
|
|
15427
|
+
var _a;
|
|
15428
|
+
// A class an earlier patch staged exists for this job even though it is
|
|
15429
|
+
// not committed yet - the ADR-0043 rule, applied to the chain.
|
|
15430
|
+
const real = (_a = classFromStage(stack, t.stage, name)) !== null && _a !== void 0 ? _a : await stack.getClass(name, ...rest);
|
|
15431
|
+
if (!real)
|
|
15432
|
+
return real;
|
|
15433
|
+
const wrapped = Object.create(real);
|
|
15434
|
+
wrapped.getCards = (selector, fields, skip, limit, sort) => facade.findDocuments(Object.assign(Object.assign({}, (selector || {})), { "~class": { $eq: real.name } }), fields, skip, limit, sort)
|
|
15435
|
+
.then((result) => result.docs);
|
|
15436
|
+
wrapped.addCard = (params) => t.createDoc(null, real.name, params);
|
|
15437
|
+
wrapped.addCards = (cards) => t.createDocs(cards.map((params) => ({ docId: null, params })), real.name);
|
|
15438
|
+
wrapped.deleteCard = (cardId) => t.deleteDocument(cardId);
|
|
15439
|
+
return wrapped;
|
|
15440
|
+
};
|
|
15441
|
+
facade.db = {
|
|
15442
|
+
get: async (docId, options) => {
|
|
15443
|
+
const doc = await t.db.get(docId, options);
|
|
15444
|
+
await assertReadableWhileLocked(doc === null || doc === void 0 ? void 0 : doc["~class"]);
|
|
15445
|
+
return openWhenKeyed(doc);
|
|
15446
|
+
},
|
|
15447
|
+
bulkGet: (request) => t.db.bulkGet(request),
|
|
15448
|
+
find: (query) => t.db.find(query),
|
|
15449
|
+
put: (doc, options) => t.db.put(doc, options),
|
|
15450
|
+
post: (doc) => t.db.post(doc),
|
|
15451
|
+
remove: (doc, rev) => t.db.remove(doc, rev),
|
|
15452
|
+
bulkDocs: (docs, options) => t.db.bulkDocs(docs, options),
|
|
15453
|
+
};
|
|
15454
|
+
return facade;
|
|
15455
|
+
}
|
|
15456
|
+
/**
|
|
15457
|
+
* Runs one of a patch's one-shot jobs against the transaction facade. The run
|
|
15458
|
+
* receipt is a `~JobRun` with NO `jobId` - patch jobs are deliberately never
|
|
15459
|
+
* persisted as `~Job` documents, so there is no row to point at (`~sys-0.0.17`
|
|
15460
|
+
* made the foreign key optional for exactly this) - carrying the patch
|
|
15461
|
+
* identity in `runtimeArgs`. It writes DIRECTLY, win or lose: a failed
|
|
15462
|
+
* migration's receipt is the troubleshooting trail and must survive the
|
|
15463
|
+
* discard that protects everything else (ADR-0044).
|
|
15464
|
+
*/
|
|
15465
|
+
async runPatchJob(t, patch, phase) {
|
|
15466
|
+
var _a, _b, _c;
|
|
15467
|
+
const job = patch[phase];
|
|
15468
|
+
const shortPhase = phase === "preApply" ? "pre" : "post";
|
|
15469
|
+
const startTime = Date.now();
|
|
15470
|
+
const receipt = {
|
|
15471
|
+
_id: `JobRun-${this.cryptoEngine.generateRandomString(12)}`,
|
|
15472
|
+
"~class": "~JobRun",
|
|
15473
|
+
status: "FAILURE",
|
|
15474
|
+
triggerType: "event",
|
|
15475
|
+
startTime,
|
|
15476
|
+
runtimeArgs: {
|
|
15477
|
+
patchVersion: patch.version,
|
|
15478
|
+
patchTarget: (_a = patch.target) !== null && _a !== void 0 ? _a : "app",
|
|
15479
|
+
phase: shortPhase,
|
|
15480
|
+
jobName: job.name,
|
|
15481
|
+
},
|
|
15482
|
+
};
|
|
15483
|
+
try {
|
|
15484
|
+
const facade = this.createPatchJobStack(t);
|
|
15485
|
+
// The ~Job convention, verbatim: content defines execute(stack, params, job).
|
|
15486
|
+
const fn = new Function("stack", "params", "job", `"use strict"; ${job.content}; return execute(stack, params, job);`);
|
|
15487
|
+
await fn(facade, (_b = job.params) !== null && _b !== void 0 ? _b : {}, { name: job.name, phase: shortPhase, version: patch.version });
|
|
15488
|
+
receipt.status = "SUCCESS";
|
|
15489
|
+
}
|
|
15490
|
+
catch (error) {
|
|
15491
|
+
receipt.errorMessage = String((_c = error === null || error === void 0 ? void 0 : error.message) !== null && _c !== void 0 ? _c : error);
|
|
15492
|
+
throw error;
|
|
15493
|
+
}
|
|
15494
|
+
finally {
|
|
15495
|
+
receipt.endTime = Date.now();
|
|
15496
|
+
receipt.durationMs = receipt.endTime - startTime;
|
|
15497
|
+
await this.db.bulkDocs([receipt]).catch(() => undefined);
|
|
11983
15498
|
}
|
|
15499
|
+
}
|
|
15500
|
+
/**
|
|
15501
|
+
* The ADR-0042 protocol: the whole pending chain stages through one internal
|
|
15502
|
+
* transaction - patch N+1 hydrates against the classes patch N staged, so the
|
|
15503
|
+
* ADR-0038 merge composes in memory before anything is real - propagation is
|
|
15504
|
+
* validated dry with nothing kept, and one commit lands every staged doc (class
|
|
15505
|
+
* models and data documents alike, since the mixed extension) as one batch
|
|
15506
|
+
* through the unchanged pipeline, where real propagation runs. The ledger
|
|
15507
|
+
* (ADR-0041) arms only after that commit; any refusal beforehand persists
|
|
15508
|
+
* nothing and names the patch at fault.
|
|
15509
|
+
*/
|
|
15510
|
+
async applyConsumerPatchChain(patches) {
|
|
15511
|
+
var _a, _b, _c;
|
|
15512
|
+
const fnLogger = logger.child({ method: "applyConsumerPatchChain" });
|
|
11984
15513
|
this.deferredPatches = [];
|
|
15514
|
+
const t = this.transactionEngine.beginInternal();
|
|
15515
|
+
// How the deferral barrier sees the chain: a class staged by an earlier
|
|
15516
|
+
// patch counts as existing, so patch N+1 defers exactly as it would have
|
|
15517
|
+
// when patch N had already committed - "unchanged" (ADR-0042 §2) means
|
|
15518
|
+
// unchanged decisions, not unchanged lookups.
|
|
15519
|
+
const stagedClassSchema = (className) => {
|
|
15520
|
+
var _a;
|
|
15521
|
+
for (const entry of t.stage.values()) {
|
|
15522
|
+
const doc = entry.doc;
|
|
15523
|
+
if (isClassModel(doc) && (doc._id === className || doc.name === className)) {
|
|
15524
|
+
return (_a = doc.schema) !== null && _a !== void 0 ? _a : null;
|
|
15525
|
+
}
|
|
15526
|
+
}
|
|
15527
|
+
return null;
|
|
15528
|
+
};
|
|
15529
|
+
// For the dry-run's error report: which patch last touched a class.
|
|
15530
|
+
const patchByClassId = new Map();
|
|
15531
|
+
const deferFrom = async (index) => {
|
|
15532
|
+
this.deferredPatches = patches.slice(index);
|
|
15533
|
+
fnLogger.warn("Deferred patches until the stack is unlocked", {
|
|
15534
|
+
from: patches[index].version,
|
|
15535
|
+
count: this.deferredPatches.length,
|
|
15536
|
+
});
|
|
15537
|
+
for (const deferred of this.deferredPatches) {
|
|
15538
|
+
await this.recordPatchDeferral(deferred);
|
|
15539
|
+
}
|
|
15540
|
+
};
|
|
15541
|
+
const stagedPatches = [];
|
|
15542
|
+
try {
|
|
15543
|
+
for (let index = 0; index < patches.length; index++) {
|
|
15544
|
+
const patch = patches[index];
|
|
15545
|
+
if (this.isLocked() && await this.patchNeedsDocumentKey(patch, stagedClassSchema)) {
|
|
15546
|
+
await deferFrom(index);
|
|
15547
|
+
break;
|
|
15548
|
+
}
|
|
15549
|
+
// One patch's whole staging - pre-apply job writes included - can be
|
|
15550
|
+
// unwound alone: a locked refusal converts this patch to a deferral
|
|
15551
|
+
// while the already-staged prefix goes on to commit.
|
|
15552
|
+
const before = t.stage.snapshot();
|
|
15553
|
+
try {
|
|
15554
|
+
if (patch.preApply) {
|
|
15555
|
+
await this.runPatchJob(t, patch, "preApply");
|
|
15556
|
+
}
|
|
15557
|
+
await this.stagePatch(t, patch);
|
|
15558
|
+
}
|
|
15559
|
+
catch (error) {
|
|
15560
|
+
if (this.isLocked() && (error === null || error === void 0 ? void 0 : error.name) === "StackLockedError") {
|
|
15561
|
+
// A `requiresKey: false` claimed wrongly (ADR-0044): degrade
|
|
15562
|
+
// to what a correct declaration would have done, instead of
|
|
15563
|
+
// failing the open.
|
|
15564
|
+
t.stage.restore(before);
|
|
15565
|
+
await deferFrom(index);
|
|
15566
|
+
break;
|
|
15567
|
+
}
|
|
15568
|
+
// Patch fault or init-state fault (ADR-0042): zero persisted.
|
|
15569
|
+
throw new Error(`Patch '${patch.version}' refused while ${patch.preApply ? "preparing" : "staging"}: ${(_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error}`);
|
|
15570
|
+
}
|
|
15571
|
+
for (const doc of (_b = patch.docs) !== null && _b !== void 0 ? _b : []) {
|
|
15572
|
+
if (isClassModel(doc))
|
|
15573
|
+
patchByClassId.set(doc._id, patch.version);
|
|
15574
|
+
}
|
|
15575
|
+
stagedPatches.push(patch);
|
|
15576
|
+
}
|
|
15577
|
+
if (!stagedPatches.length) {
|
|
15578
|
+
this.transactionEngine.discard(t);
|
|
15579
|
+
return;
|
|
15580
|
+
}
|
|
15581
|
+
await this.validateChainPropagation(t, patchByClassId);
|
|
15582
|
+
// Environment faults from here - a concurrent write between dry-run and
|
|
15583
|
+
// commit, a per-document conflict - reject as they are: nothing recorded,
|
|
15584
|
+
// the chain retries next open.
|
|
15585
|
+
await this.transactionEngine.commit(t);
|
|
15586
|
+
}
|
|
15587
|
+
catch (error) {
|
|
15588
|
+
this.transactionEngine.discard(t);
|
|
15589
|
+
throw error;
|
|
15590
|
+
}
|
|
15591
|
+
// Post-apply phase (ADR-0044): the models have landed; backfills run in a
|
|
15592
|
+
// second staged transaction, one commit of their own, and the ledger arms
|
|
15593
|
+
// only after. A post failure leaves the chain unarmed: the next open
|
|
15594
|
+
// re-stages to an empty diff and re-runs post - which is why jobs must be
|
|
15595
|
+
// idempotent.
|
|
15596
|
+
if (stagedPatches.some(patch => patch.postApply)) {
|
|
15597
|
+
const post = this.transactionEngine.beginInternal();
|
|
15598
|
+
try {
|
|
15599
|
+
for (const patch of stagedPatches) {
|
|
15600
|
+
if (!patch.postApply)
|
|
15601
|
+
continue;
|
|
15602
|
+
await this.runPatchJob(post, patch, "postApply");
|
|
15603
|
+
}
|
|
15604
|
+
await this.transactionEngine.commit(post);
|
|
15605
|
+
}
|
|
15606
|
+
catch (error) {
|
|
15607
|
+
this.transactionEngine.discard(post);
|
|
15608
|
+
if (this.isLocked() && (error === null || error === void 0 ? void 0 : error.name) === "StackLockedError") {
|
|
15609
|
+
// Models landed, backfill needs the key: hold the arming behind
|
|
15610
|
+
// it. Dormant entries keep the sync gate honest, and the unlock
|
|
15611
|
+
// replay re-runs the (idempotent) chain and arms.
|
|
15612
|
+
for (const patch of stagedPatches) {
|
|
15613
|
+
await this.recordPatchDeferral(patch);
|
|
15614
|
+
}
|
|
15615
|
+
this.deferredPatches = [...stagedPatches, ...this.deferredPatches];
|
|
15616
|
+
return;
|
|
15617
|
+
}
|
|
15618
|
+
throw new Error(`Patch chain post-apply failed: ${(_c = error === null || error === void 0 ? void 0 : error.message) !== null && _c !== void 0 ? _c : error}`);
|
|
15619
|
+
}
|
|
15620
|
+
}
|
|
15621
|
+
for (const patch of stagedPatches) {
|
|
15622
|
+
await this.recordPatchApplication(patch);
|
|
15623
|
+
}
|
|
15624
|
+
fnLogger.info("Applied patch chain", { count: stagedPatches.length });
|
|
15625
|
+
}
|
|
15626
|
+
/**
|
|
15627
|
+
* Stages one patch's documents into the chain transaction. Hydration reads
|
|
15628
|
+
* through the transaction's overlay, so an `_rev: "auto"` document merges onto
|
|
15629
|
+
* what an earlier patch staged - or onto committed state when the chain has not
|
|
15630
|
+
* touched it.
|
|
15631
|
+
*/
|
|
15632
|
+
async stagePatch(t, patch) {
|
|
15633
|
+
var _a;
|
|
15634
|
+
for (const sourceDoc of (_a = patch.docs) !== null && _a !== void 0 ? _a : []) {
|
|
15635
|
+
let doc = Object.assign({}, sourceDoc);
|
|
15636
|
+
if (doc._rev === "auto") {
|
|
15637
|
+
delete doc._rev;
|
|
15638
|
+
const existingDoc = await t.db.get(doc._id);
|
|
15639
|
+
doc = Object.assign(Object.assign({}, existingDoc), doc);
|
|
15640
|
+
ClientStack.mergePatchSchema(sourceDoc, existingDoc, doc);
|
|
15641
|
+
// The overlay hands staged docs back without a revision until one
|
|
15642
|
+
// exists; the stated revision must match what the write replaces.
|
|
15643
|
+
if (existingDoc._rev !== undefined)
|
|
15644
|
+
doc._rev = existingDoc._rev;
|
|
15645
|
+
else
|
|
15646
|
+
delete doc._rev;
|
|
15647
|
+
}
|
|
15648
|
+
await t.db.put(doc);
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
/**
|
|
15652
|
+
* ADR-0042 §3 - propagation, validated dry: for every class the chain staged
|
|
15653
|
+
* over a committed predecessor, run the schema delta across the class's
|
|
15654
|
+
* committed documents and keep nothing. The point is the refusal - a document
|
|
15655
|
+
* that cannot satisfy the new model fails here, before the first write, naming
|
|
15656
|
+
* the patch, the document and the attribute.
|
|
15657
|
+
*/
|
|
15658
|
+
async validateChainPropagation(t, patchByClassId) {
|
|
15659
|
+
var _a, _b;
|
|
15660
|
+
for (const entry of t.stage.values()) {
|
|
15661
|
+
const staged = entry.doc;
|
|
15662
|
+
if (!isClassModel(staged))
|
|
15663
|
+
continue;
|
|
15664
|
+
const previous = await this.db.get(staged._id).catch((error) => {
|
|
15665
|
+
if ((error === null || error === void 0 ? void 0 : error.name) === "not_found" || (error === null || error === void 0 ? void 0 : error.status) === 404)
|
|
15666
|
+
return null;
|
|
15667
|
+
throw error;
|
|
15668
|
+
});
|
|
15669
|
+
// A class the chain creates propagates to nothing.
|
|
15670
|
+
if (!previous)
|
|
15671
|
+
continue;
|
|
15672
|
+
const schemaDelta = diff(previous.schema, staged.schema);
|
|
15673
|
+
if (!schemaDelta)
|
|
15674
|
+
continue;
|
|
15675
|
+
const classObj = await Class.buildFromModel(this, previous, { subscribe: false });
|
|
15676
|
+
// Raw read, not getCards: patches apply during `create()`, before any
|
|
15677
|
+
// session exists, and the policy-checked read path refuses without one.
|
|
15678
|
+
// The dry-run wants the stored documents; encrypted attributes are
|
|
15679
|
+
// opened explicitly when a key is present (a keyless encrypting class
|
|
15680
|
+
// never reaches here - the deferral barrier held its patch back).
|
|
15681
|
+
const found = await this.db.find({
|
|
15682
|
+
selector: { "~class": classObj.name, active: true },
|
|
15683
|
+
limit: 2 ** 31 - 1,
|
|
15684
|
+
});
|
|
15685
|
+
const decrypts = ((_a = this.cryptoEngine) === null || _a === void 0 ? void 0 : _a.isEnabled())
|
|
15686
|
+
&& this.cryptoEngine.getDocumentKey()
|
|
15687
|
+
&& classObj.getEncryptedAttributes().length > 0;
|
|
15688
|
+
for (let document of found.docs) {
|
|
15689
|
+
// A committed document superseded in the stage - a pre-apply job's
|
|
15690
|
+
// massage (ADR-0044) - is judged as its staged version: that is what
|
|
15691
|
+
// the commit will actually write. A staged delete leaves nothing to
|
|
15692
|
+
// propagate onto.
|
|
15693
|
+
const supersededBy = t.stage.get(document._id);
|
|
15694
|
+
if (supersededBy) {
|
|
15695
|
+
if (supersededBy.op === "delete")
|
|
15696
|
+
continue;
|
|
15697
|
+
document = structuredClone(supersededBy.doc);
|
|
15698
|
+
}
|
|
15699
|
+
else if (decrypts) {
|
|
15700
|
+
await this.cryptoEngine.decryptDocument(document, classObj);
|
|
15701
|
+
}
|
|
15702
|
+
try {
|
|
15703
|
+
await applySchemaDelta(document, schemaDelta, classObj, staged.schema);
|
|
15704
|
+
}
|
|
15705
|
+
catch (error) {
|
|
15706
|
+
const version = patchByClassId.get(staged._id);
|
|
15707
|
+
throw new Error(`Patch '${version !== null && version !== void 0 ? version : "?"}' cannot apply to class '${classObj.name}': ${(_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error}`);
|
|
15708
|
+
}
|
|
15709
|
+
}
|
|
15710
|
+
}
|
|
11985
15711
|
}
|
|
11986
15712
|
/**
|
|
11987
15713
|
* Decides whether applying a patch would write an encrypted attribute.
|
|
@@ -11993,26 +15719,54 @@ class ClientStack extends Stack {
|
|
|
11993
15719
|
* simulate schema evolution ahead of time.
|
|
11994
15720
|
*
|
|
11995
15721
|
* @param patch - The patch about to be applied.
|
|
15722
|
+
* @param stagedSchema - Chain staging only (ADR-0042): resolves a class the
|
|
15723
|
+
* current transaction already staged, so patch N+1 defers exactly as it would
|
|
15724
|
+
* have when patch N had committed.
|
|
11996
15725
|
* @returns `true` if any document in it belongs to a class with encrypted attributes.
|
|
11997
15726
|
*/
|
|
11998
|
-
async patchNeedsDocumentKey(patch) {
|
|
15727
|
+
async patchNeedsDocumentKey(patch, stagedSchema) {
|
|
15728
|
+
var _a;
|
|
15729
|
+
// A one-shot job's touch-set cannot be inspected, so the author answers the
|
|
15730
|
+
// barrier's question: unstated defaults to "needs the key" - forgetting the
|
|
15731
|
+
// flag costs latency (the job runs at unlock), never correctness. An
|
|
15732
|
+
// explicit `requiresKey: false` opts into locked execution (ADR-0044).
|
|
15733
|
+
const jobs = [patch.preApply, patch.postApply].filter(Boolean);
|
|
15734
|
+
if (jobs.some(job => job.requiresKey !== false))
|
|
15735
|
+
return true;
|
|
11999
15736
|
const hasEncryptedAttribute = (schema) => !!schema && Object.values(schema).some((attribute) => { var _a; return ((_a = attribute === null || attribute === void 0 ? void 0 : attribute.config) === null || _a === void 0 ? void 0 : _a.encrypted) === true; });
|
|
15737
|
+
const knownSchema = async (className) => {
|
|
15738
|
+
const staged = stagedSchema === null || stagedSchema === void 0 ? void 0 : stagedSchema(className);
|
|
15739
|
+
if (staged)
|
|
15740
|
+
return staged;
|
|
15741
|
+
const stored = await this.getClassModel(className).catch(() => null);
|
|
15742
|
+
return stored === null || stored === void 0 ? void 0 : stored.schema;
|
|
15743
|
+
};
|
|
12000
15744
|
const schemasInPatch = new Map();
|
|
12001
15745
|
for (const doc of patch.docs) {
|
|
12002
15746
|
if (isClassModel(doc) && doc.schema)
|
|
12003
15747
|
schemasInPatch.set(doc._id, doc.schema);
|
|
12004
15748
|
}
|
|
12005
15749
|
for (const doc of patch.docs) {
|
|
12006
|
-
//
|
|
12007
|
-
|
|
15750
|
+
// A class model carries no encrypted value itself - but *updating* one
|
|
15751
|
+
// triggers propagation over the class's existing documents (ADR-0038),
|
|
15752
|
+
// and when the class encrypts, that propagation both decrypts (getCards)
|
|
15753
|
+
// and re-encrypts (the rewrite): key work on both sides. A locked stack
|
|
15754
|
+
// must defer it or the replay fails on the first document (ADR-0040).
|
|
15755
|
+
// A class that does not exist yet propagates to nothing and stays safe
|
|
15756
|
+
// to apply locked - that is how the schema itself can arrive pre-unlock.
|
|
15757
|
+
if (isClassModel(doc)) {
|
|
15758
|
+
const predecessor = await knownSchema((_a = doc.name) !== null && _a !== void 0 ? _a : doc._id);
|
|
15759
|
+
if (predecessor && (hasEncryptedAttribute(predecessor) || hasEncryptedAttribute(doc.schema))) {
|
|
15760
|
+
return true;
|
|
15761
|
+
}
|
|
12008
15762
|
continue;
|
|
15763
|
+
}
|
|
12009
15764
|
const className = doc["~class"];
|
|
12010
15765
|
if (!className)
|
|
12011
15766
|
continue;
|
|
12012
15767
|
if (hasEncryptedAttribute(schemasInPatch.get(className)))
|
|
12013
15768
|
return true;
|
|
12014
|
-
|
|
12015
|
-
if (hasEncryptedAttribute(stored === null || stored === void 0 ? void 0 : stored.schema))
|
|
15769
|
+
if (hasEncryptedAttribute(await knownSchema(className)))
|
|
12016
15770
|
return true;
|
|
12017
15771
|
}
|
|
12018
15772
|
return false;
|
|
@@ -12197,6 +15951,68 @@ class ClientStack extends Stack {
|
|
|
12197
15951
|
throw new Error(e);
|
|
12198
15952
|
}
|
|
12199
15953
|
}
|
|
15954
|
+
/**
|
|
15955
|
+
* Finds a patch's ledger entry, raw - ledger documents are read outside the
|
|
15956
|
+
* `active: true` visibility convention because `active` carries the ledger's own
|
|
15957
|
+
* meaning here: `true` is applied, `false` is deferred, absent is a legacy entry
|
|
15958
|
+
* from before the flag (treated as applied). See ADR-0041.
|
|
15959
|
+
*/
|
|
15960
|
+
async findPatchLedgerEntry(patch) {
|
|
15961
|
+
var _a;
|
|
15962
|
+
const selector = { "~class": "patch", version: patch.version };
|
|
15963
|
+
// A targetless patch is legal; `undefined` in a Mango selector is not.
|
|
15964
|
+
if (patch.target !== undefined)
|
|
15965
|
+
selector.target = patch.target;
|
|
15966
|
+
const found = await this.db.find({ selector, limit: 10 }).catch(() => ({ docs: [] }));
|
|
15967
|
+
return (_a = found.docs[0]) !== null && _a !== void 0 ? _a : null;
|
|
15968
|
+
}
|
|
15969
|
+
/**
|
|
15970
|
+
* Records a successful application: the ledger entry arms with `active: true` -
|
|
15971
|
+
* flipping the deferral entry in place when one exists, so a replayed patch does
|
|
15972
|
+
* not duplicate its record.
|
|
15973
|
+
*/
|
|
15974
|
+
async recordPatchApplication(patch) {
|
|
15975
|
+
const existing = await this.findPatchLedgerEntry(patch);
|
|
15976
|
+
if (existing) {
|
|
15977
|
+
if (existing.active === true)
|
|
15978
|
+
return;
|
|
15979
|
+
await this.db.put(Object.assign(Object.assign({}, existing), { active: true, appliedTimestamp: Date.now() }));
|
|
15980
|
+
return;
|
|
15981
|
+
}
|
|
15982
|
+
await this.db.post(Object.assign(Object.assign({ createTimestamp: Date.now() }, patch), { active: true }));
|
|
15983
|
+
}
|
|
15984
|
+
/**
|
|
15985
|
+
* Records a deferral: the patch is known but dormant (`active: false`), waiting
|
|
15986
|
+
* on the document key. The entry is what makes a deferred device honest at the
|
|
15987
|
+
* sync gate - {@link getConsumerSchemaVersion} does not count it.
|
|
15988
|
+
*/
|
|
15989
|
+
async recordPatchDeferral(patch) {
|
|
15990
|
+
const existing = await this.findPatchLedgerEntry(patch);
|
|
15991
|
+
if (existing)
|
|
15992
|
+
return;
|
|
15993
|
+
await this.db.post(Object.assign(Object.assign({ createTimestamp: Date.now() }, patch), { active: false }));
|
|
15994
|
+
}
|
|
15995
|
+
/**
|
|
15996
|
+
* The ADR-0038 half of patch hydration: `schema` does not ride the shallow
|
|
15997
|
+
* merge, which would replace it wholesale - it merges attribute by attribute.
|
|
15998
|
+
* A patch states only the attributes it changes, an absent attribute stays as
|
|
15999
|
+
* stored, and an explicit `null` entry drops the attribute - from the model
|
|
16000
|
+
* here, and from the documents when the write propagates. Shared by
|
|
16001
|
+
* {@link applyPatch} and the chain staging of ADR-0042 so the two cannot drift.
|
|
16002
|
+
*/
|
|
16003
|
+
static mergePatchSchema(sourceDoc, existingDoc, doc) {
|
|
16004
|
+
const incoming = sourceDoc === null || sourceDoc === void 0 ? void 0 : sourceDoc.schema;
|
|
16005
|
+
const stored = existingDoc === null || existingDoc === void 0 ? void 0 : existingDoc.schema;
|
|
16006
|
+
if (incoming && typeof incoming === "object" && !Array.isArray(incoming)
|
|
16007
|
+
&& stored && typeof stored === "object" && !Array.isArray(stored)) {
|
|
16008
|
+
const merged = Object.assign(Object.assign({}, stored), incoming);
|
|
16009
|
+
for (const name of Object.keys(incoming)) {
|
|
16010
|
+
if (incoming[name] === null)
|
|
16011
|
+
delete merged[name];
|
|
16012
|
+
}
|
|
16013
|
+
doc.schema = merged;
|
|
16014
|
+
}
|
|
16015
|
+
}
|
|
12200
16016
|
async applyPatches(schemaVersion) {
|
|
12201
16017
|
const fnLogger = logger.child({ method: "applyPatches", args: { schemaVersion } });
|
|
12202
16018
|
let _schemaVersion = schemaVersion;
|
|
@@ -13515,4 +17331,4 @@ class DocStack extends EventTarget {
|
|
|
13515
17331
|
}
|
|
13516
17332
|
}
|
|
13517
17333
|
|
|
13518
|
-
export { Attribute, CONTENT_EXPORT_FORMAT, Class, ClientStack, DATA_MODEL_CLASSES, DocStack, DocStackSyncHandle, Domain, INTERNAL_DOC_CLASSES, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, JOB_SCHEDULE_DOC_ID, JobEngine, JobScheduler, META_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, SYNC_META_DOC_ID, SYSTEM_SEEDED_DOC_IDS, StackLockedError, StackSyncHandle, StackWriteGuardError, SyncSchemaMismatchError, Trigger, classTenants, collectQueryClasses, createClassFilter, createReplicationFilter, DocStack as default, deriveKeyId, deriveTenantScope, describeFilter, hasClassRules, isContentClassName, isContentDocument, isContentRelation, isEncryptedPayload, isInternalDoc, nextOccurrence, parseSchedule, publishSchemaVersion, readRemoteSchemaVersion, resolveInternalClasses, withFilterIdentity };
|
|
17334
|
+
export { Attribute, CONTENT_EXPORT_FORMAT, Class, ClientStack, DATA_MODEL_CLASSES, DocStack, DocStackSyncHandle, Domain, INTERNAL_DOC_CLASSES, INTERNAL_DOC_IDS, INTERNAL_DOC_ID_PREFIXES, JOB_SCHEDULE_DOC_ID, JobEngine, JobScheduler, META_CLASSES, OPTIONAL_INTERNAL_DOC_CLASSES, SYNC_META_DOC_ID, SYSTEM_SEEDED_DOC_IDS, StackLockedError, StackSyncHandle, StackWriteGuardError, SyncSchemaMismatchError, TransactionConflictError, TransactionDb, TransactionEngine, TransactionHandle, TransactionStateError, TransactionUnsupportedDocError, TransactionValidationError, TransactionsDisabledError, Trigger, classTenants, collectQueryClasses, createClassFilter, createReplicationFilter, DocStack as default, deriveKeyId, deriveTenantScope, describeFilter, hasClassRules, isContentClassName, isContentDocument, isContentRelation, isEncryptedPayload, isInternalDoc, nextOccurrence, parseSchedule, publishSchemaVersion, readRemoteConsumerSchemaVersion, readRemoteSchemaVersion, resolveInternalClasses, withFilterIdentity };
|