@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/lib/index.umd.js CHANGED
@@ -1,8 +1,8 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('pouchdb-browser'), require('zod'), require('pouchdb-find'), require('semver'), require('jsondiffpatch')) :
3
- typeof define === 'function' && define.amd ? define(['exports', 'pouchdb-browser', 'zod', 'pouchdb-find', 'semver', 'jsondiffpatch'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.docstack = {}, global.PouchDB, global.z, global.PouchDBFind, global.semver, global.jsondiffpatch));
5
- })(this, (function (exports, PouchDB, z, PouchDBFind, semver, jsondiffpatch) { 'use strict';
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('pouchdb-browser'), require('semver'), require('jsondiffpatch'), require('zod'), require('pouchdb-find')) :
3
+ typeof define === 'function' && define.amd ? define(['exports', 'pouchdb-browser', 'semver', 'jsondiffpatch', 'zod', 'pouchdb-find'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.docstack = {}, global.PouchDB, global.semver, global.jsondiffpatch, global.z, global.PouchDBFind));
5
+ })(this, (function (exports, PouchDB, semver, jsondiffpatch, z, PouchDBFind) { 'use strict';
6
6
 
7
7
  /******************************************************************************
8
8
  Copyright (c) Microsoft Corporation.
@@ -925,6 +925,80 @@
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.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 @@ async function execute(_stack, params) {
3564
3638
  "name": "class",
3565
3639
  "description": "A class document representing a data model class",
3566
3640
  "~class": "~self",
3567
- // Patch hydration is a shallow merge: this `schema` REPLACES the stored one,
3568
- // it does not add to it. So a patch to the `class` document must carry the
3569
- // full attribute set - dropping `ephemeral`/`simple` here silently strips
3570
- // those flags from every class model on the next validated write.
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 @@ async function execute(_stack, params) {
3602
3678
  }
3603
3679
  ]
3604
3680
  };
3605
- syspatches.push(sys_011, sys_012, sys_013, sys_014, sys_015, sys_016);
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 execute(_stack, params) {
3642
3743
  .map((patch) => JSON.parse(JSON.stringify(patch)));
3643
3744
  }
3644
3745
 
3645
- const attributeEffect = async (operation, model, classObj, doc) => {
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.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 @@ async function execute(_stack, params) {
3768
3812
  return Promise.resolve("pong");
3769
3813
  },
3770
3814
  bulkDocs: async function (docs, options, callback) {
3771
- const fnLogger = logger$3.child({ method: "bulkDocs" });
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 @@ async function execute(_stack, params) {
3831
3875
  const triggerQueue = Object.create(null);
3832
3876
  const classCache = new Map();
3833
3877
  const postOperations = async (error, result) => {
3834
- if (error) {
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 @@ async function execute(_stack, params) {
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
- // Get the previous version of the class model
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 @@ async function execute(_stack, params) {
3961
3999
  fnLogger.info(`Class '${className}' has no changes on schema.`);
3962
4000
  continue;
3963
4001
  }
3964
- const documents = await classObj.getCards();
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
- await stack.db.bulkDocs(updates);
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 @@ async function execute(_stack, params) {
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
- stack.db.get(doc.sourceId).catch(() => null),
3986
- stack.db.get(doc.targetId).catch(() => null),
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 @@ async function execute(_stack, params) {
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
- classObj = classCache.get(className) || await stack.getClassSnapshot(className);
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
4932
5060
  return scope;
4933
5061
  };
4934
5062
 
4935
- const logger$2 = createLogger().child({ module: "sync" });
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 @@ async function execute(_stack, params) {
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 schema version ` +
4947
- `${remoteVersion}, this device has ${localVersion || "no schema version"}. Update the ` +
4948
- `application so its patches reach ${remoteVersion} before syncing again.`);
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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
- const recorded = existing === null || existing === void 0 ? void 0 : existing.schemaVersion;
5004
- if (recorded && semver.valid(recorded) && semver.valid(schemaVersion) && !semver.gt(schemaVersion, recorded)) {
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
- appVersion, updatedAt: Date.now() });
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 @@ async function execute(_stack, params) {
5097
5250
  * @throws {SyncSchemaMismatchError} When the remote is ahead of this device.
5098
5251
  */
5099
5252
  async start() {
5100
- const fnLogger = logger$2.child({ method: "start", stack: this.stack.name });
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 @@ async function execute(_stack, params) {
5136
5289
  this.replication.cancel();
5137
5290
  }
5138
5291
  catch (error) {
5139
- logger$2.warn("Error while cancelling replication", { error, stack: this.stack.name });
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
8171
8340
  *
8172
8341
  * @module
8173
8342
  */
8174
- const logger$1 = createLogger().child({ module: "job-scheduler" });
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 @@ async function execute(_stack, params) {
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$1.warn("tick - job hash does not match the pinned value; not running", { jobId });
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 @@ async function execute(_stack, params) {
8331
8500
  }
8332
8501
  const schedule = parseSchedule(job.schedule);
8333
8502
  if (!schedule) {
8334
- logger$1.warn("tick - job carries a schedule this client cannot read", {
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 @@ async function execute(_stack, params) {
8354
8523
  continue;
8355
8524
  }
8356
8525
  if (isImplausible(state.nextRunAt, schedule, at)) {
8357
- logger$1.warn("tick - stored nextRunAt is further out than the schedule allows; recomputing", {
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 @@ async function execute(_stack, params) {
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$1.warn("dispatch - scheduled job did not run", { jobId, error: (error === null || error === void 0 ? void 0 : error.message) || String(error) });
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 @@ async function execute(_stack, params) {
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$1.info("sweepAbandonedRuns - reaped abandoned runs", { count: stale.length });
8621
+ logger$2.info("sweepAbandonedRuns - reaped abandoned runs", { count: stale.length });
8453
8622
  return stale.length;
8454
8623
  }
8455
8624
  catch (error) {
8456
- logger$1.warn("sweepAbandonedRuns - could not sweep", { error: (error === null || error === void 0 ? void 0 : error.message) || String(error) });
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 @@ async function execute(_stack, params) {
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$1.warn("writeState - could not persist schedule state", {
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,115 +9436,3191 @@ async function execute(_stack, params) {
9267
9436
  }
9268
9437
 
9269
9438
  /**
9270
- * Moving a stack's *content* between databases, without its datamodel.
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
- * @module
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
- /** The envelope format identifier, so an importer can refuse what it does not understand. */
9284
- const CONTENT_EXPORT_FORMAT = "docstack/content-export@1";
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
- * `~class` values that describe the stack rather than hold its content.
9541
+ * Errors of the transaction engine (ADR-0039).
9287
9542
  *
9288
- * `~self` is the bootstrap class-of-classes; `class` and `superclass` are class models;
9289
- * `domain` is a relation definition; `patch` is the local ledger of applied patches.
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
- const META_CLASSES = ["~self", "class", "superclass", "domain", "patch"];
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
- * Reports whether a class name belongs to application content.
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
- const isContentClassName = (className) => {
9310
- if (typeof className !== "string" || !className.length)
9311
- return false;
9312
- if (className.startsWith("~"))
9313
- return false;
9314
- return !META_CLASSES.includes(className);
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
- * Reports whether a document is application content rather than part of the stack.
9318
- *
9319
- * @param doc - Any stored document.
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
- const isContentDocument = (doc) => {
9322
- if (!doc || typeof doc._id !== "string")
9323
- return false;
9324
- // A design document has no `~class` and would fall through the class test.
9325
- if (doc._id.startsWith("_"))
9326
- return false;
9327
- return isContentClassName(doc["~class"]);
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
- * Reports whether a document is an application relation.
9331
- *
9332
- * Relations carry `~domain` and no `~class` (see `isRelation` in `@docstack/shared`), so
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
- const isContentRelation = (doc) => {
9338
- if (!doc || typeof doc._id !== "string")
9339
- return false;
9340
- if (doc["~class"] !== undefined)
9341
- return false;
9342
- return isContentClassName(doc["~domain"]);
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
- * Strips the fields that belong to the source database rather than to the document.
9348
- *
9349
- * `_rev` above all: a revision from one database means nothing in another, and carrying
9350
- * it into an import turns every write into a conflict.
9351
- *
9352
- * @param doc - The document to clean.
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 stripTransientFields = (doc) => {
9356
- const clone = Object.assign({}, doc);
9357
- for (const field of TRANSIENT_FIELDS)
9358
- delete clone[field];
9359
- return clone;
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
- * Validates an import payload's envelope before anything is written.
9621
+ * The validation sweep - the transaction's atomicity boundary in practice.
9363
9622
  *
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).
9367
- *
9368
- * @param payload - The value handed to {@link ClientStack.importContent}.
9369
- * @throws Error when it is not a content export this version understands.
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 assertContentExport = (payload) => {
9372
- const value = payload;
9373
- if (!value || typeof value !== "object") {
9374
- throw new Error("importContent - payload is not a content export.");
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
+ }
9375
9641
  }
9376
- if (value.format !== CONTENT_EXPORT_FORMAT) {
9377
- throw new Error(`importContent - unsupported format '${String(value.format)}'. ` +
9378
- `This build reads '${CONTENT_EXPORT_FORMAT}'.`);
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) {
12622
+ throw new Error(`importContent - unsupported format '${String(value.format)}'. ` +
12623
+ `This build reads '${CONTENT_EXPORT_FORMAT}'.`);
9379
12624
  }
9380
12625
  if (!Array.isArray(value.documents) || !Array.isArray(value.relations)) {
9381
12626
  throw new Error("importContent - payload is missing its 'documents' or 'relations' array.");
@@ -9397,6 +12642,8 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
9947
13194
  };
9948
13195
  this.applyPatch = async (patch) => {
9949
13196
  const fnLogger = logger.child({ method: "applyPatch", args: { patch } });
9950
- return new Promise(async (resolve, reject) => {
9951
- try {
9952
- fnLogger.info("Attempting to apply patch", { patch });
9953
- fnLogger.info("applyPatch - starting to hydrate patch docs", { docCount: patch.docs.length });
9954
- const hydratedDocs = await Promise.all(patch.docs.map(async (sourceDoc) => {
9955
- // Work on a copy: the patch definition must survive intact. System
9956
- // patches are module-level objects shared by every stack in the
9957
- // process, so deleting `_rev` from one would strip the "auto" marker
9958
- // permanently the next ClientStack created in the same context
9959
- // would then rewrite those documents as fresh inserts, hit a 409,
9960
- // and end up without its `class` class model.
9961
- let doc = Object.assign({}, sourceDoc);
9962
- if (doc._rev === "auto") {
9963
- delete doc._rev;
9964
- const existingDoc = await this.db.get(doc._id);
9965
- if (existingDoc) {
9966
- doc = Object.assign(Object.assign({}, existingDoc), doc);
9967
- }
9968
- }
9969
- return doc;
9970
- }));
9971
- fnLogger.info("applyPatch - hydration complete, calling bulkDocs", { docCount: hydratedDocs.length });
9972
- await this.db.bulkDocs(hydratedDocs, { isPatch: true }).then((result) => {
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 @@ async function execute(_stack, params) {
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
- (_a = this.jobScheduler) === null || _a === void 0 ? void 0 : _a.stop();
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
11502
14820
  if (astList.length > 0) {
11503
14821
  try {
11504
14822
  const plan = createPlan(astList);
11505
- const rows = await executePlan(this, plan, params);
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 @@ async function execute(_stack, params) {
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 @@ async function execute(_stack, params) {
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
- const patches = await stack.findDocuments({
11946
- "~class": { $eq: "patch" }
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 => !patches.docs.find(existing => existing.version === p.version
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 @@ async function execute(_stack, params) {
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
- const fnLogger = logger.child({ method: "applyConsumerPatches" });
11972
- for (let index = 0; index < patches.length; index++) {
11973
- const patch = patches[index];
11974
- if (this.isLocked() && await this.patchNeedsDocumentKey(patch)) {
11975
- this.deferredPatches = patches.slice(index);
11976
- fnLogger.warn("Deferred patches that write encrypted attributes until the stack is unlocked", {
11977
- from: patch.version,
11978
- count: this.deferredPatches.length,
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
- await this.applyPatch(patch);
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 = jsondiffpatch.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 @@ async function execute(_stack, params) {
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
- // Class models describe encryption; they never carry an encrypted value.
12007
- if (isClassModel(doc))
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
- const stored = await this.getClassModel(className).catch(() => null);
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 @@ async function execute(_stack, params) {
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;
@@ -13537,6 +17353,14 @@ async function execute(_stack, params) {
13537
17353
  exports.StackSyncHandle = StackSyncHandle;
13538
17354
  exports.StackWriteGuardError = StackWriteGuardError;
13539
17355
  exports.SyncSchemaMismatchError = SyncSchemaMismatchError;
17356
+ exports.TransactionConflictError = TransactionConflictError;
17357
+ exports.TransactionDb = TransactionDb;
17358
+ exports.TransactionEngine = TransactionEngine;
17359
+ exports.TransactionHandle = TransactionHandle;
17360
+ exports.TransactionStateError = TransactionStateError;
17361
+ exports.TransactionUnsupportedDocError = TransactionUnsupportedDocError;
17362
+ exports.TransactionValidationError = TransactionValidationError;
17363
+ exports.TransactionsDisabledError = TransactionsDisabledError;
13540
17364
  exports.Trigger = Trigger;
13541
17365
  exports.classTenants = classTenants;
13542
17366
  exports.collectQueryClasses = collectQueryClasses;
@@ -13555,6 +17379,7 @@ async function execute(_stack, params) {
13555
17379
  exports.nextOccurrence = nextOccurrence;
13556
17380
  exports.parseSchedule = parseSchedule;
13557
17381
  exports.publishSchemaVersion = publishSchemaVersion;
17382
+ exports.readRemoteConsumerSchemaVersion = readRemoteConsumerSchemaVersion;
13558
17383
  exports.readRemoteSchemaVersion = readRemoteSchemaVersion;
13559
17384
  exports.resolveInternalClasses = resolveInternalClasses;
13560
17385
  exports.withFilterIdentity = withFilterIdentity;