diff --git a/.changeset/odd-wolves-sit.md b/.changeset/odd-wolves-sit.md new file mode 100644 index 00000000000..fc63acc005e --- /dev/null +++ b/.changeset/odd-wolves-sit.md @@ -0,0 +1,5 @@ +--- +"@firebase/firestore": patch +--- + +Add unique IDs and state information into fatal error messages instead of the generic "unexpected state" message. diff --git a/packages/firestore/package.json b/packages/firestore/package.json index d9cb6c263b5..0c9ddeee843 100644 --- a/packages/firestore/package.json +++ b/packages/firestore/package.json @@ -20,14 +20,14 @@ "dev": "rollup -c -w", "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "prettier": "prettier --write '*.js' '@(lite|src|test)/**/*.ts' 'test/unit/remote/bloom_filter_golden_test_data/*.json'", + "prettier": "prettier --write '*.js' '@(lite|src|test|scripts)/**/*.ts' 'test/unit/remote/bloom_filter_golden_test_data/*.json'", "test:lite": "ts-node ./scripts/run-tests.ts --emulator --platform node_lite --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:prod": "ts-node ./scripts/run-tests.ts --platform node_lite --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:prod:nameddb": "ts-node ./scripts/run-tests.ts --platform node_lite --databaseId=test-db --main=lite/index.ts 'test/lite/**/*.test.ts'", "test:lite:browser": "karma start --lite", "test:lite:browser:nameddb": "karma start --lite --databaseId=test-db", "test:lite:browser:debug": "karma start --browsers=Chrome --lite --auto-watch", - "test": "run-s --npm-path npm lint test:all", + "test": "run-s --npm-path npm lint assertion-id:check test:all", "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:all:ci", "test:all:ci": "run-s --npm-path npm test:browser test:travis test:lite:browser test:browser:prod:nameddb test:lite:browser:nameddb", "test:all": "run-p --npm-path npm test:browser test:lite:browser test:travis test:minified test:browser:prod:nameddb test:lite:browser:nameddb", @@ -52,7 +52,11 @@ "api-report:api-json": "rm -rf temp && api-extractor run --local --verbose", "api-report": "run-s --npm-path npm api-report:main api-report:lite && yarn api-report:api-json", "doc": "api-documenter markdown --input temp --output docs", - "typings:public": "node ../../scripts/build/use_typings.js ./dist/index.d.ts" + "typings:public": "node ../../scripts/build/use_typings.js ./dist/index.d.ts", + "assertion-id:check": "ts-node scripts/assertion-id-tool.ts --dir=src --check", + "assertion-id:new": "ts-node scripts/assertion-id-tool.ts --dir=src --new", + "assertion-id:list": "ts-node scripts/assertion-id-tool.ts --dir=src --list", + "assertion-id:find": "ts-node scripts/assertion-id-tool.ts --dir=src --find" }, "exports": { ".": { diff --git a/packages/firestore/scripts/assertion-id-tool.js b/packages/firestore/scripts/assertion-id-tool.js new file mode 100644 index 00000000000..f29d88a1bf2 --- /dev/null +++ b/packages/firestore/scripts/assertion-id-tool.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(g&&(g=0,op[0]&&(_=0)),_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0){node.arguments.forEach((function(arg){argsText_1.push(arg.getText(sourceFile_1));if(ts.isStringLiteral(arg)){errorMessage_1=arg.getText(sourceFile_1)}else if(ts.isNumericLiteral(arg)){assertionId_1=parseInt(arg.getText(sourceFile_1),10)}}))}foundCalls.push({fileName:filePath,functionName:functionName,line:line+1,character:character+1,argumentsText:argsText_1,errorMessage:errorMessage_1,assertionId:assertionId_1!==null&&assertionId_1!==void 0?assertionId_1:-1})}}ts.forEachChild(node,visit_1)};visit_1(sourceFile_1)}catch(error){console.error("Error processing file ".concat(filePath,": ").concat(error.message))}};for(var _i=0,filePaths_1=filePaths;_i1){duplicatesFound=true;console.error('\nDuplicate assertion id "'.concat(code,'" found at ').concat(locations.length," locations:"));locations.forEach((function(loc){var relativePath=path.relative(process.cwd(),loc.fileName);console.error("- ".concat(relativePath,":").concat(loc.line,":").concat(loc.character))}))}}));if(!duplicatesFound){log("No duplicate assertion ids found.")}else{process.exit(1)}}function handleNew(occurrences){var maxCode=0;occurrences.forEach((function(occ){if(occ.assertionId>maxCode){maxCode=occ.assertionId}}));if(occurrences.length===0){log("0");return}var newCode=maxCode+1;console.log(newCode)}function main(){return __awaiter(this,void 0,void 0,(function(){var argv,targetDirectory,stats,filesToScan,allOccurrences;return __generator(this,(function(_a){argv=(0,yargs_1.default)((0,helpers_1.hideBin)(process.argv)).usage("Usage: $0 [options]").option("dir",{alias:"D",describe:"Directory to scan recursively for TS files",type:"string",demandOption:true}).option("verbose",{alias:"V",describe:"verbose",type:"boolean"}).option("find",{alias:"F",describe:"Find locations of a specific {assertionId}",type:"string",nargs:1}).option("list",{alias:"L",describe:"List all unique assertion ids found (default action)",type:"boolean"}).option("new",{alias:"N",describe:"Suggest a new assertion id based on existing ones",type:"boolean"}).option("check",{alias:"C",describe:"Check for duplicate usage of assertion ids",type:"boolean"}).check((function(argv){var options=[argv.F,argv.L,argv.N,argv.C].filter(Boolean).length;if(options>1){throw new Error("Options -F, -L, -N, -C are mutually exclusive.")}return true})).help().alias("help","h").strict().parse();targetDirectory=path.resolve(argv["dir"]);isVerbose=!!argv["verbose"];try{stats=fs.statSync(targetDirectory);if(!stats.isDirectory()){console.error("Error: Provided path is not a directory: ".concat(targetDirectory));process.exit(1)}}catch(error){console.error("Error accessing directory ".concat(targetDirectory,": ").concat(error.message));process.exit(1)}log("Scanning directory: ".concat(targetDirectory));filesToScan=getTsFilesRecursive(targetDirectory);if(filesToScan.length===0){log("No relevant .ts or .tsx files found.");process.exit(0)}log("Found ".concat(filesToScan.length," files. Analyzing for assertion ids..."));allOccurrences=findFunctionCalls(filesToScan);log("Scan complete. Found ".concat(allOccurrences.length," potential assertion id occurrences."));if(argv["find"]){handleFind(allOccurrences,argv["find"])}else if(argv["new"]){handleNew(allOccurrences)}else if(argv["check"]){handleCheck(allOccurrences)}else{handleList(allOccurrences)}return[2]}))}))}main().catch((function(error){console.error("\nAn unexpected error occurred:");console.error(error);process.exit(1)})); \ No newline at end of file diff --git a/packages/firestore/scripts/assertion-id-tool.ts b/packages/firestore/scripts/assertion-id-tool.ts new file mode 100644 index 00000000000..8bde791cc6d --- /dev/null +++ b/packages/firestore/scripts/assertion-id-tool.ts @@ -0,0 +1,381 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console */ + +import * as fs from 'fs'; +import { getRandomValues } from 'node:crypto'; +import * as path from 'path'; + +import * as ts from 'typescript'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +let isVerbose: boolean = false; + +function log(message: any): void { + if (isVerbose) { + console.log(message); + } +} + +// Define the names of the functions we are looking for +const targetFunctionNames: Set = new Set(['fail', 'hardAssert']); + +// Interface to store information about found call sites +interface CallSiteInfo { + fileName: string; + functionName: string; + line: number; + character: number; + argumentsText: string[]; // Added to store argument text + errorMessage: string | undefined; + assertionId: string; +} + +/** + * Recursively finds all files with .ts extensions in a directory. + * @param dirPath The absolute path to the directory to scan. + * @returns An array of absolute paths to the found TypeScript files. + */ +function getTsFilesRecursive(dirPath: string): string[] { + let tsFiles: string[] = []; + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + // Ignore node_modules for performance and relevance + if (entry.name === 'node_modules') { + continue; + } + // Recursively scan subdirectories + tsFiles = tsFiles.concat(getTsFilesRecursive(fullPath)); + } else if (entry.isFile() && entry.name.endsWith('.ts')) { + // Exclude declaration files (.d.ts) as they usually don't contain implementation + if (!entry.name.endsWith('.d.ts')) { + tsFiles.push(fullPath); + } + } + } + } catch (error: any) { + console.error(`Error reading directory ${dirPath}: ${error.message}`); + throw error; + } + return tsFiles; +} + +/** + * Analyzes TypeScript source files to find calls to specific functions. + * @param filePaths An array of absolute paths to the TypeScript files to scan. + * @returns An array of objects detailing the found call sites. + */ +function findFunctionCalls(filePaths: string[]): CallSiteInfo[] { + const foundCalls: CallSiteInfo[] = []; + + for (const filePath of filePaths) { + // Read the file content + const sourceText = fs.readFileSync(filePath, 'utf8'); + + // Create the SourceFile AST node + const sourceFile = ts.createSourceFile( + path.basename(filePath), // Use basename for AST node name + sourceText, + ts.ScriptTarget.ESNext, + true, // Set parent pointers + ts.ScriptKind.Unknown // Detect TS vs TSX automatically + ); + + // Define the visitor function + const visit = (node: ts.Node): void => { + // Check if the node is a CallExpression (e.g., myFunction(...)) + if (ts.isCallExpression(node)) { + let functionName: string | null = null; + const expression = node.expression; + + // Check if the call is directly to an identifier (e.g., fail()) + if (ts.isIdentifier(expression)) { + functionName = expression.text; + } + + // If we found a function name, and it's one we're looking for + if (functionName && targetFunctionNames.has(functionName)) { + // Get line and character number + const { line, character } = ts.getLineAndCharacterOfPosition( + sourceFile, + node.getStart() // Get start position of the call expression + ); + + // --- Extract Arguments --- + const argsText: string[] = []; + let errorMessage: string | undefined; + let assertionId: string | undefined; + if (node.arguments && node.arguments.length > 0) { + node.arguments.forEach((arg: ts.Expression) => { + // Get the source text of the argument node + argsText.push(arg.getText(sourceFile)); + + if (ts.isStringLiteral(arg)) { + errorMessage = arg.getText(sourceFile); + } else if (ts.isNumericLiteral(arg)) { + assertionId = arg.getText(sourceFile); + } + }); + } + + // Store the information (add 1 to line/char for 1-based indexing) + foundCalls.push({ + fileName: filePath, // Store the full path + functionName, + line: line + 1, + character: character + 1, + argumentsText: argsText, // Store the extracted arguments, + errorMessage, + assertionId: assertionId ?? 'INVALID' + }); + } + } + + // Continue traversing down the AST + ts.forEachChild(node, visit); + }; + + // Start traversal from the root SourceFile node + visit(sourceFile); + } // End loop through filePaths + + return foundCalls; +} + +// --- Action Handlers --- + +function handleList(occurrences: CallSiteInfo[]): void { + if (occurrences.length === 0) { + log('No assertion ids found.'); + return; + } + + occurrences + .sort((a, b) => a.assertionId.localeCompare(b.assertionId)) + .forEach(call => { + console.log( + `ID: ${call.assertionId}; MESSAGE: ${call.errorMessage}; SOURCE: '${ + call.functionName + }' call at ${path.relative(process.cwd(), call.fileName)}:${ + call.line + }:${call.character}` + ); + }); +} + +function find( + occurrences: CallSiteInfo[], + targetId: string | number +): CallSiteInfo[] { + const target = + typeof targetId === 'number' ? targetId.toString(16) : targetId; + return occurrences.filter(o => String(o.assertionId) === String(target)); +} + +function handleFind( + occurrences: CallSiteInfo[], + targetId: string | number +): void { + const foundLocations = find(occurrences, targetId); + + if (foundLocations.length === 0) { + log(`Assertion id "${targetId}" not found.`); + process.exit(1); + } + + handleList(foundLocations); +} + +function handleCheck(occurrences: CallSiteInfo[]): void { + if (occurrences.length === 0) { + log('No assertion ids found to check for duplicates.'); + return; + } + const idCounts: { [id: string]: CallSiteInfo[] } = {}; + + occurrences.forEach(occ => { + // Count ID occurrences + const codeStr = String(occ.assertionId); // Use string representation as key + if (!idCounts[codeStr]) { + idCounts[codeStr] = []; + } + idCounts[codeStr].push(occ); + + // validate formats + if (!/^0x[0-9a-f]{4}$/.test(occ.assertionId)) { + console.error( + `Invalid assertion ID '${occ.assertionId}'. Must match /^0x[0-9a-f]{4}$/` + ); + + const relativePath = path.relative(process.cwd(), occ.fileName); + console.error(`- at '${relativePath}:${occ.line}:${occ.character}`); + } + }); + + let duplicatesFound = false; + log('Checking for duplicate assertion id usage:'); + Object.entries(idCounts).forEach(([code, locations]) => { + if (locations.length > 1) { + duplicatesFound = true; + console.error( + `\nDuplicate assertion id "${code}" found at ${locations.length} locations:` + ); + locations.forEach(loc => { + const relativePath = path.relative(process.cwd(), loc.fileName); + console.error(`- ${relativePath}:${loc.line}:${loc.character}`); + }); + } + }); + + if (!duplicatesFound) { + log('No duplicate assertion ids found.'); + } else { + process.exit(1); + } +} + +function randomId(): string { + const randomBytes = new Uint8Array(2); + getRandomValues(randomBytes); + + return ( + '0x' + + Array.from(randomBytes) + .map(byte => byte.toString(16).padStart(2, '0')) + .join('') + ); +} + +function handleNew(occurrences: CallSiteInfo[]): void { + let newCode: string = randomId(); + + // If we find this code already is used, regenerate it. + while (find(occurrences, newCode).length > 0) { + newCode = randomId(); + } + + console.log(newCode); +} + +// --- Main Execution --- +async function main(): Promise { + const argv = yargs(hideBin(process.argv)) + .usage('Usage: $0 [options]') + .option('dir', { + alias: 'D', + describe: 'Directory to scan recursively for TS files', + type: 'string', + demandOption: true + }) + .option('verbose', { + alias: 'V', + describe: 'verbose', + type: 'boolean' + }) + .option('find', { + alias: 'F', + describe: 'Find locations of a specific {assertionId}', + type: 'string', + nargs: 1 + }) + .option('list', { + alias: 'L', + describe: 'List all unique assertion ids found (default action)', + type: 'boolean' + }) + .option('new', { + alias: 'N', + describe: 'Suggest a new assertion id based on existing ones', + type: 'boolean' + }) + .option('check', { + alias: 'C', + describe: 'Check for duplicate usage of assertion ids', + type: 'boolean' + }) + .check(argv => { + // Enforce mutual exclusivity among options *within* the scan command + const options = [argv.F, argv.L, argv.N, argv.C].filter(Boolean).length; + if (options > 1) { + throw new Error('Options -F, -L, -N, -C are mutually exclusive.'); + } + return true; + }) + .help() + .alias('help', 'h') + .strict() + .parse(); // Execute parsing + + // Extract directory path (safe due to demandOption) + const targetDirectory = path.resolve(argv['dir'] as string); + + // set verbosity + isVerbose = !!argv['verbose']; + + // Validate directory + try { + const stats = fs.statSync(targetDirectory); + if (!stats.isDirectory()) { + console.error( + `Error: Provided path is not a directory: ${targetDirectory}` + ); + process.exit(1); + } + } catch (error: any) { + console.error( + `Error accessing directory ${targetDirectory}: ${error.message}` + ); + process.exit(1); + } + + log(`Scanning directory: ${targetDirectory}`); + const filesToScan = getTsFilesRecursive(targetDirectory); + + if (filesToScan.length === 0) { + log('No relevant .ts or .tsx files found.'); + process.exit(0); + } + log(`Found ${filesToScan.length} files. Analyzing for assertion ids...`); + + const allOccurrences = findFunctionCalls(filesToScan); + log( + `Scan complete. Found ${allOccurrences.length} potential assertion id occurrences.` + ); + + // Determine action based on flags + if (argv['find']) { + handleFind(allOccurrences, argv['find']); + } else if (argv['new']) { + handleNew(allOccurrences); + } else if (argv['check']) { + handleCheck(allOccurrences); + } else { + // Default action is List (-L or no flag) + handleList(allOccurrences); + } +} + +// Run the main function +void main(); diff --git a/packages/firestore/scripts/build-bundle.js b/packages/firestore/scripts/build-bundle.js index b44311571e3..f8ba283a5a8 100644 --- a/packages/firestore/scripts/build-bundle.js +++ b/packages/firestore/scripts/build-bundle.js @@ -14,4 +14,4 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){function adopt(value){return value instanceof P?value:new P((function(resolve){resolve(value)}))}return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator["throw"](value))}catch(e){reject(e)}}function step(result){result.done?resolve(result.value):adopt(result.value).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};var __generator=this&&this.__generator||function(thisArg,body){var _={label:0,sent:function(){if(t[0]&1)throw t[1];return t[1]},trys:[],ops:[]},f,y,t,g;return g={next:verb(0),throw:verb(1),return:verb(2)},typeof Symbol==="function"&&(g[Symbol.iterator]=function(){return this}),g;function verb(n){return function(v){return step([n,v])}}function step(op){if(f)throw new TypeError("Generator is already executing.");while(g&&(g=0,op[0]&&(_=0)),_)try{if(f=1,y&&(t=op[0]&2?y["return"]:op[0]?y["throw"]||((t=y["return"])&&t.call(y),0):y.next)&&!(t=t.call(y,op[1])).done)return t;if(y=0,t)op=[op[0]&2,t.value];switch(op[0]){case 0:case 1:t=op;break;case 4:_.label++;return{value:op[1],done:false};case 5:_.label++;y=op[1];op=[0];continue;case 7:op=_.ops.pop();_.trys.pop();continue;default:if(!(t=_.trys,t=t.length>0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]0&&t[t.length-1])&&(op[0]===6||op[0]===2)){_=0;continue}if(op[0]===3&&(!t||op[1]>t[0]&&op[1]=0){var method=declaration.name.text;if(method==="debugAssert"){updatedNode=ts.factory.createOmittedExpression()}else if(method==="hardAssert"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,[node.arguments[0]])}else if(method==="fail"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,[])}}}}if(updatedNode){ts.setSourceMapRange(updatedNode,ts.getSourceMapRange(node));return updatedNode}else{return node}};return RemoveAsserts}(); \ No newline at end of file + */Object.defineProperty(exports,"__esModule",{value:true});exports.removeAsserts=removeAsserts;var ts=require("typescript");var ASSERT_LOCATION="packages/firestore/src/util/assert.ts";function removeAsserts(program){var removeAsserts=new RemoveAsserts(program.getTypeChecker());return function(context){return function(file){return removeAsserts.visitNodeAndChildren(file,context)}}}var RemoveAsserts=function(){function RemoveAsserts(typeChecker){this.typeChecker=typeChecker}RemoveAsserts.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RemoveAsserts.prototype.visitNode=function(node){var updatedNode=null;if(ts.isCallExpression(node)){var signature=this.typeChecker.getResolvedSignature(node);if(signature&&signature.declaration&&signature.declaration.kind===ts.SyntaxKind.FunctionDeclaration){var declaration=signature.declaration;if(declaration&&declaration.getSourceFile().fileName.indexOf(ASSERT_LOCATION)>=0){var method=declaration.name.text;if(method==="debugAssert"){updatedNode=ts.factory.createOmittedExpression()}else if(method==="hardAssert"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,node.arguments.filter((function(value){return!ts.isStringLiteral(value)})))}else if(method==="fail"){updatedNode=ts.factory.createCallExpression(declaration.name,undefined,node.arguments.filter((function(value){return!ts.isStringLiteral(value)})))}}}}if(updatedNode){ts.setSourceMapRange(updatedNode,ts.getSourceMapRange(node));return updatedNode}else{return node}};return RemoveAsserts}(); \ No newline at end of file diff --git a/packages/firestore/scripts/remove-asserts.ts b/packages/firestore/scripts/remove-asserts.ts index fb3e2af6a34..f55907ec152 100644 --- a/packages/firestore/scripts/remove-asserts.ts +++ b/packages/firestore/scripts/remove-asserts.ts @@ -70,14 +70,14 @@ class RemoveAsserts { updatedNode = ts.factory.createCallExpression( declaration.name!, /*typeArgs*/ undefined, - [node.arguments[0]] + node.arguments.filter(value => !ts.isStringLiteral(value)) ); } else if (method === 'fail') { // Remove the log message updatedNode = ts.factory.createCallExpression( declaration.name!, /*typeArgs*/ undefined, - [] + node.arguments.filter(value => !ts.isStringLiteral(value)) ); } } diff --git a/packages/firestore/scripts/rename-internals.js b/packages/firestore/scripts/rename-internals.js index fa8394a2261..4d5ebf1a838 100644 --- a/packages/firestore/scripts/rename-internals.js +++ b/packages/firestore/scripts/rename-internals.js @@ -14,4 +14,4 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - */exports.__esModule=true;exports.renameInternals=void 0;var ts=require("typescript");var ignoredIdentifiers=["undefined"];var RenameInternals=function(){function RenameInternals(publicApi,prefix){this.publicApi=publicApi;this.prefix=prefix}RenameInternals.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RenameInternals.prototype.visitNode=function(node){if(ts.isIdentifier(node)){var name_1=node.escapedText.toString();if(!this.publicApi.has(name_1)&&ignoredIdentifiers.indexOf(node.escapedText.toString())===-1){var newIdentifier=ts.factory.createIdentifier(this.prefix+name_1);ts.setSourceMapRange(newIdentifier,ts.getSourceMapRange(node));return newIdentifier}}return node};return RenameInternals}();var DEFAULT_PREFIX="_";function renameInternals(program,config){var _a;var prefix=(_a=config.prefix)!==null&&_a!==void 0?_a:DEFAULT_PREFIX;var renamer=new RenameInternals(config.publicIdentifiers,prefix);return function(context){return function(file){return renamer.visitNodeAndChildren(file,context)}}}exports.renameInternals=renameInternals; \ No newline at end of file + */Object.defineProperty(exports,"__esModule",{value:true});exports.renameInternals=renameInternals;var ts=require("typescript");var ignoredIdentifiers=["undefined"];var RenameInternals=function(){function RenameInternals(publicApi,prefix){this.publicApi=publicApi;this.prefix=prefix}RenameInternals.prototype.visitNodeAndChildren=function(node,context){var _this=this;return ts.visitEachChild(this.visitNode(node),(function(childNode){return _this.visitNodeAndChildren(childNode,context)}),context)};RenameInternals.prototype.visitNode=function(node){if(ts.isIdentifier(node)){var name_1=node.escapedText.toString();if(!this.publicApi.has(name_1)&&ignoredIdentifiers.indexOf(node.escapedText.toString())===-1){var newIdentifier=ts.factory.createIdentifier(this.prefix+name_1);ts.setSourceMapRange(newIdentifier,ts.getSourceMapRange(node));return newIdentifier}}return node};return RenameInternals}();var DEFAULT_PREFIX="_";function renameInternals(program,config){var _a;var prefix=(_a=config.prefix)!==null&&_a!==void 0?_a:DEFAULT_PREFIX;var renamer=new RenameInternals(config.publicIdentifiers,prefix);return function(context){return function(file){return renamer.visitNodeAndChildren(file,context)}}} \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.js b/packages/firestore/scripts/run-tests.js new file mode 100644 index 00000000000..3a2e171b649 --- /dev/null +++ b/packages/firestore/scripts/run-tests.js @@ -0,0 +1,17 @@ +"use strict"; +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */Object.defineProperty(exports,"__esModule",{value:true});var path_1=require("path");var child_process_promise_1=require("child-process-promise");var yargs=require("yargs");var argv=yargs.options({main:{type:"string",demandOption:true},platform:{type:"string",default:"node"},emulator:{type:"boolean"},persistence:{type:"boolean"},databaseId:{type:"string"}}).parseSync();var nyc=(0,path_1.resolve)(__dirname,"../../../node_modules/.bin/nyc");var mocha=(0,path_1.resolve)(__dirname,"../../../node_modules/.bin/mocha");var babel=(0,path_1.resolve)(__dirname,"../babel-register.js");process.env.NO_TS_NODE="true";process.env.TEST_PLATFORM=argv.platform;var args=["--reporter","lcovonly",mocha,"--require",babel,"--require",argv.main,"--config","../../config/mocharc.node.js"];if(argv.emulator){process.env.FIRESTORE_TARGET_BACKEND="emulator"}if(argv.persistence){process.env.USE_MOCK_PERSISTENCE="YES";args.push("--require","test/util/node_persistence.ts")}if(argv.databaseId){process.env.FIRESTORE_TARGET_DB_ID=argv.databaseId}args=args.concat(argv._);var childProcess=(0,child_process_promise_1.spawn)(nyc,args,{stdio:"inherit",cwd:process.cwd()}).childProcess;process.once("exit",(function(){return childProcess.kill()}));process.once("SIGINT",(function(){return childProcess.kill("SIGINT")}));process.once("SIGTERM",(function(){return childProcess.kill("SIGTERM")})); \ No newline at end of file diff --git a/packages/firestore/scripts/run-tests.ts b/packages/firestore/scripts/run-tests.ts index 7e5cdf8fc80..bd721944647 100644 --- a/packages/firestore/scripts/run-tests.ts +++ b/packages/firestore/scripts/run-tests.ts @@ -20,32 +20,34 @@ import { resolve } from 'path'; import { spawn } from 'child-process-promise'; import * as yargs from 'yargs'; -const argv = yargs.options({ - main: { - type: 'string', - demandOption: true - }, - platform: { - type: 'string', - default: 'node' - }, - emulator: { - type: 'boolean' - }, - persistence: { - type: 'boolean' - }, - databaseId: { - type: 'string' - } -}).parseSync(); +const argv = yargs + .options({ + main: { + type: 'string', + demandOption: true + }, + platform: { + type: 'string', + default: 'node' + }, + emulator: { + type: 'boolean' + }, + persistence: { + type: 'boolean' + }, + databaseId: { + type: 'string' + } + }) + .parseSync(); const nyc = resolve(__dirname, '../../../node_modules/.bin/nyc'); const mocha = resolve(__dirname, '../../../node_modules/.bin/mocha'); const babel = resolve(__dirname, '../babel-register.js'); // used in '../../config/mocharc.node.js' to disable ts-node -process.env.NO_TS_NODE = "true"; +process.env.NO_TS_NODE = 'true'; process.env.TEST_PLATFORM = argv.platform; let args = [ diff --git a/packages/firestore/src/api/credentials.ts b/packages/firestore/src/api/credentials.ts index b542ec80b91..c1b10d61057 100644 --- a/packages/firestore/src/api/credentials.ts +++ b/packages/firestore/src/api/credentials.ts @@ -207,7 +207,9 @@ export class LiteAuthCredentialsProvider implements CredentialsProvider { if (tokenData) { hardAssert( typeof tokenData.accessToken === 'string', - 'Invalid tokenData returned from getToken():' + tokenData + 0xa539, + 'Invalid tokenData returned from getToken()', + { tokenData } ); return new OAuthToken( tokenData.accessToken, @@ -259,6 +261,7 @@ export class FirebaseAuthCredentialsProvider ): void { hardAssert( this.tokenListener === undefined, + 0xa540, 'Token listener already added' ); let lastTokenId = this.tokenCounter; @@ -357,7 +360,9 @@ export class FirebaseAuthCredentialsProvider if (tokenData) { hardAssert( typeof tokenData.accessToken === 'string', - 'Invalid tokenData returned from getToken():' + tokenData + 0x7c5d, + 'Invalid tokenData returned from getToken()', + { tokenData } ); return new OAuthToken(tokenData.accessToken, this.currentUser); } else { @@ -386,7 +391,9 @@ export class FirebaseAuthCredentialsProvider const currentUid = this.auth && this.auth.getUid(); hardAssert( currentUid === null || typeof currentUid === 'string', - 'Received invalid UID: ' + currentUid + 0x0807, + 'Received invalid UID', + { currentUid } ); return new User(currentUid); } @@ -513,6 +520,7 @@ export class FirebaseAppCheckTokenProvider ): void { hardAssert( this.tokenListener === undefined, + 0x0db8, 'Token listener already added' ); @@ -588,7 +596,9 @@ export class FirebaseAppCheckTokenProvider if (tokenResult) { hardAssert( typeof tokenResult.token === 'string', - 'Invalid tokenResult returned from getToken():' + tokenResult + 0xae0e, + 'Invalid tokenResult returned from getToken()', + { tokenResult } ); this.latestAppCheckToken = tokenResult.token; return new AppCheckToken(tokenResult.token); @@ -659,7 +669,9 @@ export class LiteAppCheckTokenProvider implements CredentialsProvider { if (tokenResult) { hardAssert( typeof tokenResult.token === 'string', - 'Invalid tokenResult returned from getToken():' + tokenResult + 0x0d8e, + 'Invalid tokenResult returned from getToken()', + { tokenResult } ); return new AppCheckToken(tokenResult.token); } else { diff --git a/packages/firestore/src/api/snapshot.ts b/packages/firestore/src/api/snapshot.ts index 29e1616b61c..669ac26cafe 100644 --- a/packages/firestore/src/api/snapshot.ts +++ b/packages/firestore/src/api/snapshot.ts @@ -749,7 +749,7 @@ export function resultChangeType(type: ChangeType): DocumentChangeType { case ChangeType.Removed: return 'removed'; default: - return fail('Unknown change type: ' + type); + return fail(0xf03d, 'Unknown change type', { type }); } } diff --git a/packages/firestore/src/core/component_provider.ts b/packages/firestore/src/core/component_provider.ts index 8a63509232c..183a1046cc9 100644 --- a/packages/firestore/src/core/component_provider.ts +++ b/packages/firestore/src/core/component_provider.ts @@ -197,6 +197,7 @@ export class LruGcMemoryOfflineComponentProvider extends MemoryOfflineComponentP ): Scheduler | null { hardAssert( this.persistence.referenceDelegate instanceof MemoryLruDelegate, + 0xb743, 'referenceDelegate is expected to be an instance of MemoryLruDelegate.' ); diff --git a/packages/firestore/src/core/filter.ts b/packages/firestore/src/core/filter.ts index 06e2740c315..d8907387057 100644 --- a/packages/firestore/src/core/filter.ts +++ b/packages/firestore/src/core/filter.ts @@ -168,7 +168,9 @@ export class FieldFilter extends Filter { case Operator.GREATER_THAN_OR_EQUAL: return comparison >= 0; default: - return fail('Unknown FieldFilter operator: ' + this.op); + return fail(0xb8a2, 'Unknown FieldFilter operator', { + operator: this.op + }); } } @@ -315,7 +317,7 @@ export function filterEquals(f1: Filter, f2: Filter): boolean { } else if (f1 instanceof CompositeFilter) { return compositeFilterEquals(f1, f2); } else { - fail('Only FieldFilters and CompositeFilters can be compared'); + fail(0x4bef, 'Only FieldFilters and CompositeFilters can be compared'); } } diff --git a/packages/firestore/src/core/query.ts b/packages/firestore/src/core/query.ts index b13296ad7ee..ca875bbb14d 100644 --- a/packages/firestore/src/core/query.ts +++ b/packages/firestore/src/core/query.ts @@ -578,6 +578,6 @@ export function compareDocs( case Direction.DESCENDING: return -1 * comparison; default: - return fail('Unknown direction: ' + orderBy.dir); + return fail(0x4d4e, 'Unknown direction', { direction: orderBy.dir }); } } diff --git a/packages/firestore/src/core/sync_engine_impl.ts b/packages/firestore/src/core/sync_engine_impl.ts index f96cbea0f00..404d4663a47 100644 --- a/packages/firestore/src/core/sync_engine_impl.ts +++ b/packages/firestore/src/core/sync_engine_impl.ts @@ -579,6 +579,7 @@ export async function syncEngineApplyRemoteEvent( targetChange.modifiedDocuments.size + targetChange.removedDocuments.size <= 1, + 0x5858, 'Limbo resolution for single document contains multiple changes.' ); if (targetChange.addedDocuments.size > 0) { @@ -586,11 +587,13 @@ export async function syncEngineApplyRemoteEvent( } else if (targetChange.modifiedDocuments.size > 0) { hardAssert( limboResolution.receivedDocument, + 0x390f, 'Received change for limbo target document without add.' ); } else if (targetChange.removedDocuments.size > 0) { hardAssert( limboResolution.receivedDocument, + 0xa4f3, 'Received remove for limbo target document without add.' ); limboResolution.receivedDocument = false; @@ -994,7 +997,7 @@ function updateTrackedLimbos( removeLimboTarget(syncEngineImpl, limboChange.key); } } else { - fail('Unknown limbo change: ' + JSON.stringify(limboChange)); + fail(0x4d4f, 'Unknown limbo change', { limboChange }); } } } @@ -1317,7 +1320,7 @@ export async function syncEngineApplyBatchState( batchId ); } else { - fail(`Unknown batchState: ${batchState}`); + fail(0x1a40, `Unknown batchState`, { batchState }); } await syncEngineEmitNewSnapsAndNotifyLocalStore(syncEngineImpl, documents); @@ -1560,7 +1563,7 @@ export async function syncEngineApplyTargetState( break; } default: - fail('Unexpected target state: ' + state); + fail(0xfa9b, 'Unexpected target state', state); } } } diff --git a/packages/firestore/src/core/transaction.ts b/packages/firestore/src/core/transaction.ts index 471c64e13bd..d3dc6ee8a89 100644 --- a/packages/firestore/src/core/transaction.ts +++ b/packages/firestore/src/core/transaction.ts @@ -124,7 +124,9 @@ export class Transaction { // Represent a deleted doc using SnapshotVersion.min(). docVersion = SnapshotVersion.min(); } else { - throw fail('Document in a transaction was a ' + doc.constructor.name); + throw fail(0xc542, 'Document in a transaction was a ', { + documentName: doc.constructor.name + }); } const existingVersion = this.readVersions.get(doc.key.toString()); diff --git a/packages/firestore/src/core/view.ts b/packages/firestore/src/core/view.ts index b0a07bd783c..e0909de938f 100644 --- a/packages/firestore/src/core/view.ts +++ b/packages/firestore/src/core/view.ts @@ -505,7 +505,7 @@ function compareChangeType(c1: ChangeType, c2: ChangeType): number { case ChangeType.Removed: return 0; default: - return fail('Unknown ChangeType: ' + change); + return fail(0x4f35, 'Unknown ChangeType', { change }); } }; diff --git a/packages/firestore/src/core/view_snapshot.ts b/packages/firestore/src/core/view_snapshot.ts index f15c5ccb409..3bb6e8ae134 100644 --- a/packages/firestore/src/core/view_snapshot.ts +++ b/packages/firestore/src/core/view_snapshot.ts @@ -118,10 +118,12 @@ export class DocumentChangeSet { // Metadata->Added // Removed->Metadata fail( - 'unsupported combination of changes: ' + - JSON.stringify(change) + - ' after ' + - JSON.stringify(oldChange) + 0xf76d, + 'unsupported combination of changes: `change` after `oldChange`', + { + change, + oldChange + } ); } } diff --git a/packages/firestore/src/index/firestore_index_value_writer.ts b/packages/firestore/src/index/firestore_index_value_writer.ts index dfdb3836578..b76ca7a930a 100644 --- a/packages/firestore/src/index/firestore_index_value_writer.ts +++ b/packages/firestore/src/index/firestore_index_value_writer.ts @@ -136,7 +136,7 @@ export class FirestoreIndexValueWriter { this.writeIndexArray(indexValue.arrayValue!, encoder); this.writeTruncationMarker(encoder); } else { - fail('unknown index value type ' + indexValue); + fail(0x4a4e, 'unknown index value type', { indexValue }); } } diff --git a/packages/firestore/src/lite-api/reference_impl.ts b/packages/firestore/src/lite-api/reference_impl.ts index 6876ed0a877..6d92ccab479 100644 --- a/packages/firestore/src/lite-api/reference_impl.ts +++ b/packages/firestore/src/lite-api/reference_impl.ts @@ -133,7 +133,11 @@ export function getDoc( return invokeBatchGetDocumentsRpc(datastore, [reference._key]).then( result => { - hardAssert(result.length === 1, 'Expected a single document result'); + hardAssert( + result.length === 1, + 0x3d02, + 'Expected a single document result' + ); const document = result[0]; return new DocumentSnapshot( reference.firestore, diff --git a/packages/firestore/src/lite-api/transaction.ts b/packages/firestore/src/lite-api/transaction.ts index 10a07ef252c..9a405feebea 100644 --- a/packages/firestore/src/lite-api/transaction.ts +++ b/packages/firestore/src/lite-api/transaction.ts @@ -94,7 +94,7 @@ export class Transaction { const userDataWriter = new LiteUserDataWriter(this._firestore); return this._transaction.lookup([ref._key]).then(docs => { if (!docs || docs.length !== 1) { - return fail('Mismatch in docs returned from document lookup.'); + return fail(0x5de9, 'Mismatch in docs returned from document lookup.'); } const doc = docs[0]; if (doc.isFoundDocument()) { @@ -115,7 +115,11 @@ export class Transaction { ); } else { throw fail( - `BatchGetDocumentsRequest returned unexpected document: ${doc}` + 0x4801, + 'BatchGetDocumentsRequest returned unexpected document', + { + doc + } ); } }); diff --git a/packages/firestore/src/lite-api/user_data_reader.ts b/packages/firestore/src/lite-api/user_data_reader.ts index ebd4b49085f..aa5f9eeb5bf 100644 --- a/packages/firestore/src/lite-api/user_data_reader.ts +++ b/packages/firestore/src/lite-api/user_data_reader.ts @@ -175,7 +175,9 @@ function isWrite(dataSource: UserDataSource): boolean { case UserDataSource.ArrayArgument: return false; default: - throw fail(`Unexpected case for UserDataSource: ${dataSource}`); + throw fail(0x9c4b, 'Unexpected case for UserDataSource', { + dataSource + }); } } diff --git a/packages/firestore/src/lite-api/user_data_writer.ts b/packages/firestore/src/lite-api/user_data_writer.ts index e903991cb58..070c71c7832 100644 --- a/packages/firestore/src/lite-api/user_data_writer.ts +++ b/packages/firestore/src/lite-api/user_data_writer.ts @@ -89,7 +89,9 @@ export abstract class AbstractUserDataWriter { case TypeOrder.VectorValue: return this.convertVectorValue(value.mapValue!); default: - throw fail('Invalid value type: ' + JSON.stringify(value)); + throw fail(0xf2a2, 'Invalid value type', { + value + }); } } @@ -173,7 +175,9 @@ export abstract class AbstractUserDataWriter { const resourcePath = ResourcePath.fromString(name); hardAssert( isValidResourceName(resourcePath), - 'ReferenceValue is not valid ' + name + 0x25d8, + 'ReferenceValue is not valid', + { name } ); const databaseId = new DatabaseId(resourcePath.get(1), resourcePath.get(3)); const key = new DocumentKey(resourcePath.popFirst(5)); diff --git a/packages/firestore/src/local/encoded_resource_path.ts b/packages/firestore/src/local/encoded_resource_path.ts index b52b8bd6805..497a65fdf8c 100644 --- a/packages/firestore/src/local/encoded_resource_path.ts +++ b/packages/firestore/src/local/encoded_resource_path.ts @@ -118,11 +118,13 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { // Event the empty path must encode as a path of at least length 2. A path // with exactly 2 must be the empty path. const length = path.length; - hardAssert(length >= 2, 'Invalid path ' + path); + hardAssert(length >= 2, 0xfb98, 'Invalid path', { path }); if (length === 2) { hardAssert( path.charAt(0) === escapeChar && path.charAt(1) === encodedSeparatorChar, - 'Non-empty path ' + path + ' had length 2' + 0xdb51, + 'Non-empty path had length 2', + { path } ); return ResourcePath.emptyPath(); } @@ -139,7 +141,7 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { // there must be an end to this segment. const end = path.indexOf(escapeChar, start); if (end < 0 || end > lastReasonableEscapeIndex) { - fail('Invalid encoded resource path: "' + path + '"'); + fail(0xc553, 'Invalid encoded resource path', { path }); } const next = path.charAt(end + 1); @@ -167,7 +169,7 @@ export function decodeResourcePath(path: EncodedResourcePath): ResourcePath { segmentBuilder += path.substring(start, end + 1); break; default: - fail('Invalid encoded resource path: "' + path + '"'); + fail(0xeeef, 'Invalid encoded resource path', { path }); } start = end + 2; diff --git a/packages/firestore/src/local/indexeddb_index_manager.ts b/packages/firestore/src/local/indexeddb_index_manager.ts index 04a380601b3..d2b8bc47163 100644 --- a/packages/firestore/src/local/indexeddb_index_manager.ts +++ b/packages/firestore/src/local/indexeddb_index_manager.ts @@ -1066,7 +1066,7 @@ export class IndexedDbIndexManager implements IndexManager { this.getSubTargets(target), (subTarget: Target) => this.getFieldIndex(transaction, subTarget).next(index => - index ? index : fail('Target cannot be served from index') + index ? index : fail(0xad8a, 'Target cannot be served from index') ) ).next(getMinOffsetFromFieldIndexes); } @@ -1118,6 +1118,7 @@ function indexStateStore( function getMinOffsetFromFieldIndexes(fieldIndexes: FieldIndex[]): IndexOffset { hardAssert( fieldIndexes.length !== 0, + 0x7099, 'Found empty index group when looking for least recent index offset.' ); diff --git a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts index 16b157accb2..64b6df20b7e 100644 --- a/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts +++ b/packages/firestore/src/local/indexeddb_mutation_batch_impl.ts @@ -64,8 +64,9 @@ export function removeMutationBatch( removePromise.next(() => { hardAssert( numDeleted === 1, - 'Dangling document-mutation reference found: Missing batch ' + - batch.batchId + 0xb7de, + 'Dangling document-mutation reference found: Missing batch', + { batchId: batch.batchId } ); }) ); @@ -100,7 +101,7 @@ export function dbDocumentSize( } else if (doc.noDocument) { value = doc.noDocument; } else { - throw fail('Unknown remote document type'); + throw fail(0x398b, 'Unknown remote document type'); } return JSON.stringify(value).length; } diff --git a/packages/firestore/src/local/indexeddb_mutation_queue.ts b/packages/firestore/src/local/indexeddb_mutation_queue.ts index 0aedf650769..bcdafa6aa36 100644 --- a/packages/firestore/src/local/indexeddb_mutation_queue.ts +++ b/packages/firestore/src/local/indexeddb_mutation_queue.ts @@ -105,7 +105,7 @@ export class IndexedDbMutationQueue implements MutationQueue { // In particular, are there any reserved characters? are empty ids allowed? // For the moment store these together in the same mutations table assuming // that empty userIDs aren't allowed. - hardAssert(user.uid !== '', 'UserID must not be an empty string.'); + hardAssert(user.uid !== '', 0xfb83, 'UserID must not be an empty string.'); const userId = user.isAuthenticated() ? user.uid! : ''; return new IndexedDbMutationQueue( userId, @@ -154,6 +154,7 @@ export class IndexedDbMutationQueue implements MutationQueue { return mutationStore.add({} as any).next(batchId => { hardAssert( typeof batchId === 'number', + 0xbf7b, 'Auto-generated key is not a number' ); @@ -206,7 +207,12 @@ export class IndexedDbMutationQueue implements MutationQueue { if (dbBatch) { hardAssert( dbBatch.userId === this.userId, - `Unexpected user '${dbBatch.userId}' for mutation batch ${batchId}` + 0x0030, + `Unexpected user for mutation batch`, + { + userId: dbBatch.userId, + batchId + } ); return fromDbMutationBatch(this.serializer, dbBatch); } @@ -257,7 +263,9 @@ export class IndexedDbMutationQueue implements MutationQueue { if (dbBatch.userId === this.userId) { hardAssert( dbBatch.batchId >= nextBatchId, - 'Should have found mutation after ' + nextBatchId + 0xb9a4, + 'Should have found mutation after `nextBatchId`', + { nextBatchId } ); foundBatch = fromDbMutationBatch(this.serializer, dbBatch); } @@ -336,15 +344,22 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(mutation => { if (!mutation) { throw fail( - 'Dangling document-mutation reference found: ' + - indexKey + - ' which points to ' + + 0xf028, + 'Dangling document-mutation reference found: `indexKey` which points to `batchId`', + { + indexKey, batchId + } ); } hardAssert( mutation.userId === this.userId, - `Unexpected user '${mutation.userId}' for mutation batch ${batchId}` + 0x2907, + `Unexpected user for mutation batch`, + { + userId: mutation.userId, + batchId + } ); results.push(fromDbMutationBatch(this.serializer, mutation)); }); @@ -468,14 +483,18 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(mutation => { if (mutation === null) { throw fail( - 'Dangling document-mutation reference found, ' + - 'which points to ' + + 0x89ca, + 'Dangling document-mutation reference found, which points to `batchId`', + { batchId + } ); } hardAssert( mutation.userId === this.userId, - `Unexpected user '${mutation.userId}' for mutation batch ${batchId}` + 0x2614, + `Unexpected user for mutation batch`, + { userId: mutation.userId, batchId } ); results.push(fromDbMutationBatch(this.serializer, mutation)); }) @@ -549,9 +568,13 @@ export class IndexedDbMutationQueue implements MutationQueue { .next(() => { hardAssert( danglingMutationReferences.length === 0, - 'Document leak -- detected dangling mutation references when queue is empty. ' + - 'Dangling keys: ' + - danglingMutationReferences.map(p => p.canonicalString()) + 0xdd90, + 'Document leak -- detected dangling mutation references when queue is empty.', + { + danglingKeys: danglingMutationReferences.map(p => + p.canonicalString() + ) + } ); }); }); diff --git a/packages/firestore/src/local/indexeddb_remote_document_cache.ts b/packages/firestore/src/local/indexeddb_remote_document_cache.ts index 9b23c64fcf5..fffe935c4f9 100644 --- a/packages/firestore/src/local/indexeddb_remote_document_cache.ts +++ b/packages/firestore/src/local/indexeddb_remote_document_cache.ts @@ -381,7 +381,7 @@ class IndexedDbRemoteDocumentCacheImpl implements IndexedDbRemoteDocumentCache { return documentGlobalStore(txn) .get(DbRemoteDocumentGlobalKey) .next(metadata => { - hardAssert(!!metadata, 'Missing document cache metadata'); + hardAssert(!!metadata, 0x4e35, 'Missing document cache metadata'); return metadata!; }); } diff --git a/packages/firestore/src/local/indexeddb_schema_converter.ts b/packages/firestore/src/local/indexeddb_schema_converter.ts index 9d7485f4a92..7446ae7ae20 100644 --- a/packages/firestore/src/local/indexeddb_schema_converter.ts +++ b/packages/firestore/src/local/indexeddb_schema_converter.ts @@ -326,7 +326,9 @@ export class SchemaConverter implements SimpleDbSchemaConverter { (dbBatch: DbMutationBatch) => { hardAssert( dbBatch.userId === queue.userId, - `Cannot process batch ${dbBatch.batchId} from unexpected user` + 0x48da, + `Cannot process batch from unexpected user`, + { batchId: dbBatch.batchId } ); const batch = fromDbMutationBatch(this.serializer, dbBatch); @@ -772,6 +774,6 @@ function extractKey(remoteDoc: DbRemoteDocumentLegacy): DocumentKey { } else if (remoteDoc.unknownDocument) { return DocumentKey.fromSegments(remoteDoc.unknownDocument.path); } else { - return fail('Unexpected DbRemoteDocument'); + return fail(0x8faf, 'Unexpected DbRemoteDocument'); } } diff --git a/packages/firestore/src/local/indexeddb_sentinels.ts b/packages/firestore/src/local/indexeddb_sentinels.ts index e1e3ead3aa2..cb6ebcb664a 100644 --- a/packages/firestore/src/local/indexeddb_sentinels.ts +++ b/packages/firestore/src/local/indexeddb_sentinels.ts @@ -450,6 +450,6 @@ export function getObjectStores(schemaVersion: number): string[] { } else if (schemaVersion === 11) { return V11_STORES; } else { - fail('Only schema version 11 and 12 and 13 are supported'); + fail(0xeb55, 'Only schema version 11 and 12 and 13 are supported'); } } diff --git a/packages/firestore/src/local/indexeddb_target_cache.ts b/packages/firestore/src/local/indexeddb_target_cache.ts index 9e93cc68838..1d5ed8f0c8b 100644 --- a/packages/firestore/src/local/indexeddb_target_cache.ts +++ b/packages/firestore/src/local/indexeddb_target_cache.ts @@ -144,6 +144,7 @@ export class IndexedDbTargetCache implements TargetCache { .next(metadata => { hardAssert( metadata.targetCount > 0, + 0x1f81, 'Removing from an empty target cache' ); metadata.targetCount -= 1; @@ -197,7 +198,7 @@ export class IndexedDbTargetCache implements TargetCache { return globalTargetStore(transaction) .get(DbTargetGlobalKey) .next(metadata => { - hardAssert(metadata !== null, 'Missing metadata row.'); + hardAssert(metadata !== null, 0x0b48, 'Missing metadata row.'); return metadata; }); } diff --git a/packages/firestore/src/local/local_serializer.ts b/packages/firestore/src/local/local_serializer.ts index b8916608711..bb1658caa52 100644 --- a/packages/firestore/src/local/local_serializer.ts +++ b/packages/firestore/src/local/local_serializer.ts @@ -101,7 +101,7 @@ export function fromDbRemoteDocument( const version = fromDbTimestamp(remoteDoc.unknownDocument.version); doc = MutableDocument.newUnknownDocument(key, version); } else { - return fail('Unexpected DbRemoteDocument'); + return fail(0xdd85, 'Unexpected DbRemoteDocument'); } if (remoteDoc.readTime) { @@ -138,7 +138,7 @@ export function toDbRemoteDocument( version: toDbTimestamp(document.version) }; } else { - return fail('Unexpected Document ' + document); + return fail(0xe230, 'Unexpected Document', { document }); } return remoteDoc; } diff --git a/packages/firestore/src/local/local_store_impl.ts b/packages/firestore/src/local/local_store_impl.ts index 56f2b96f8d1..31d2a46c326 100644 --- a/packages/firestore/src/local/local_store_impl.ts +++ b/packages/firestore/src/local/local_store_impl.ts @@ -494,7 +494,11 @@ export function localStoreRejectBatch( return localStoreImpl.mutationQueue .lookupMutationBatch(txn, batchId) .next((batch: MutationBatch | null) => { - hardAssert(batch !== null, 'Attempt to reject nonexistent batch!'); + hardAssert( + batch !== null, + 0x90f9, + 'Attempt to reject nonexistent batch!' + ); affectedKeys = batch.keys(); return localStoreImpl.mutationQueue.removeMutationBatch(txn, batch); }) @@ -1137,6 +1141,7 @@ function applyWriteToRemoteDocuments( const ackVersion = batchResult.docVersions.get(docKey); hardAssert( ackVersion !== null, + 0xbd9d, 'ackVersions should contain every doc in the write.' ); if (doc.version.compareTo(ackVersion!) < 0) { diff --git a/packages/firestore/src/local/memory_mutation_queue.ts b/packages/firestore/src/local/memory_mutation_queue.ts index e3902cc96fc..f136fb7ad15 100644 --- a/packages/firestore/src/local/memory_mutation_queue.ts +++ b/packages/firestore/src/local/memory_mutation_queue.ts @@ -246,6 +246,7 @@ export class MemoryMutationQueue implements MutationQueue { const batchIndex = this.indexOfExistingBatchId(batch.batchId, 'removed'); hardAssert( batchIndex === 0, + 0xd6db, 'Can only remove the first entry of the mutation queue' ); this.mutationQueue.shift(); diff --git a/packages/firestore/src/local/memory_persistence.ts b/packages/firestore/src/local/memory_persistence.ts index 30d4f2bd19a..fcb6db42059 100644 --- a/packages/firestore/src/local/memory_persistence.ts +++ b/packages/firestore/src/local/memory_persistence.ts @@ -235,7 +235,10 @@ export class MemoryEagerDelegate implements MemoryReferenceDelegate { private get orphanedDocuments(): Set { if (!this._orphanedDocuments) { - throw fail('orphanedDocuments is only valid during a transaction.'); + throw fail( + 0xee44, + 'orphanedDocuments is only valid during a transaction.' + ); } else { return this._orphanedDocuments; } diff --git a/packages/firestore/src/local/memory_remote_document_cache.ts b/packages/firestore/src/local/memory_remote_document_cache.ts index 42a0010d4ac..0daf80b6a19 100644 --- a/packages/firestore/src/local/memory_remote_document_cache.ts +++ b/packages/firestore/src/local/memory_remote_document_cache.ts @@ -219,7 +219,7 @@ class MemoryRemoteDocumentCacheImpl implements MemoryRemoteDocumentCache { ): PersistencePromise { // This method should only be called from the IndexBackfiller if persistence // is enabled. - fail('getAllFromCollectionGroup() is not supported.'); + fail(0x251c, 'getAllFromCollectionGroup() is not supported.'); } forEachDocumentKey( diff --git a/packages/firestore/src/local/persistence_promise.ts b/packages/firestore/src/local/persistence_promise.ts index 4678650fa84..812cc0fca85 100644 --- a/packages/firestore/src/local/persistence_promise.ts +++ b/packages/firestore/src/local/persistence_promise.ts @@ -86,7 +86,7 @@ export class PersistencePromise { catchFn?: RejectedHandler ): PersistencePromise { if (this.callbackAttached) { - fail('Called next() or catch() twice for PersistencePromise'); + fail(0xe830, 'Called next() or catch() twice for PersistencePromise'); } this.callbackAttached = true; if (this.isDone) { diff --git a/packages/firestore/src/local/shared_client_state.ts b/packages/firestore/src/local/shared_client_state.ts index 7c033cedb41..1000e63a0f6 100644 --- a/packages/firestore/src/local/shared_client_state.ts +++ b/packages/firestore/src/local/shared_client_state.ts @@ -1085,7 +1085,9 @@ function fromWebStorageSequenceNumber( const parsed = JSON.parse(seqString); hardAssert( typeof parsed === 'number', - 'Found non-numeric sequence number' + 0x77ac, + 'Found non-numeric sequence number', + { seqString } ); sequenceNumber = parsed; } catch (e) { diff --git a/packages/firestore/src/model/document.ts b/packages/firestore/src/model/document.ts index 830983aec43..ac454704776 100644 --- a/packages/firestore/src/model/document.ts +++ b/packages/firestore/src/model/document.ts @@ -397,6 +397,9 @@ export function compareDocumentsByField( if (v1 !== null && v2 !== null) { return valueCompare(v1, v2); } else { - return fail("Trying to compare documents on fields that don't exist"); + return fail( + 0xa786, + "Trying to compare documents on fields that don't exist" + ); } } diff --git a/packages/firestore/src/model/mutation.ts b/packages/firestore/src/model/mutation.ts index 119e9b9731b..0bcd1345b01 100644 --- a/packages/firestore/src/model/mutation.ts +++ b/packages/firestore/src/model/mutation.ts @@ -623,8 +623,12 @@ function serverTransformResults( const transformResults = new Map(); hardAssert( fieldTransforms.length === serverTransformResults.length, - `server transform result count (${serverTransformResults.length}) ` + - `should match field transform count (${fieldTransforms.length})` + 0x7f90, + 'server transform result count should match field transform count', + { + serverTransformResultCount: serverTransformResults.length, + fieldTransformCount: fieldTransforms.length + } ); for (let i = 0; i < serverTransformResults.length; i++) { diff --git a/packages/firestore/src/model/mutation_batch.ts b/packages/firestore/src/model/mutation_batch.ts index 56d5f4d2cd3..703623da01a 100644 --- a/packages/firestore/src/model/mutation_batch.ts +++ b/packages/firestore/src/model/mutation_batch.ts @@ -219,10 +219,12 @@ export class MutationBatchResult { ): MutationBatchResult { hardAssert( batch.mutations.length === results.length, - 'Mutations sent ' + - batch.mutations.length + - ' must equal results received ' + - results.length + 0xe5da, + 'Mutations sent must equal results received', + { + mutationsSent: batch.mutations.length, + resultsReceived: results.length + } ); let versionMap = documentVersionMap(); diff --git a/packages/firestore/src/model/normalize.ts b/packages/firestore/src/model/normalize.ts index 2061601d23e..986eeed1e48 100644 --- a/packages/firestore/src/model/normalize.ts +++ b/packages/firestore/src/model/normalize.ts @@ -32,7 +32,7 @@ export function normalizeTimestamp(date: Timestamp): { seconds: number; nanos: number; } { - hardAssert(!!date, 'Cannot normalize null or undefined timestamp.'); + hardAssert(!!date, 0x986a, 'Cannot normalize null or undefined timestamp.'); // The json interface (for the browser) will return an iso timestamp string, // while the proto js library (for node) will return a @@ -44,7 +44,9 @@ export function normalizeTimestamp(date: Timestamp): { // Parse the nanos right out of the string. let nanos = 0; const fraction = ISO_TIMESTAMP_REG_EXP.exec(date); - hardAssert(!!fraction, 'invalid timestamp: ' + date); + hardAssert(!!fraction, 0xb5de, 'invalid timestamp', { + timestamp: date + }); if (fraction[1]) { // Pad the fraction out to 9 digits (nanos). let nanoStr = fraction[1]; diff --git a/packages/firestore/src/model/path.ts b/packages/firestore/src/model/path.ts index c375b4c56d2..0f4581da8d8 100644 --- a/packages/firestore/src/model/path.ts +++ b/packages/firestore/src/model/path.ts @@ -35,13 +35,19 @@ abstract class BasePath> { if (offset === undefined) { offset = 0; } else if (offset > segments.length) { - fail('offset ' + offset + ' out of range ' + segments.length); + fail(0x027d, 'offset out of range', { + offset, + range: segments.length + }); } if (length === undefined) { length = segments.length - offset; } else if (length > segments.length - offset) { - fail('length ' + length + ' out of range ' + (segments.length - offset)); + fail(0x06d2, 'length out of range', { + length, + range: segments.length - offset + }); } this.segments = segments; this.offset = offset; diff --git a/packages/firestore/src/model/target_index_matcher.ts b/packages/firestore/src/model/target_index_matcher.ts index df80ffa419a..407eae337c7 100644 --- a/packages/firestore/src/model/target_index_matcher.ts +++ b/packages/firestore/src/model/target_index_matcher.ts @@ -111,6 +111,7 @@ export class TargetIndexMatcher { servedByIndex(index: FieldIndex): boolean { hardAssert( index.collectionGroup === this.collectionId, + 0xc07f, 'Collection IDs do not match' ); diff --git a/packages/firestore/src/model/values.ts b/packages/firestore/src/model/values.ts index 30d8688b776..1ef54a98ad6 100644 --- a/packages/firestore/src/model/values.ts +++ b/packages/firestore/src/model/values.ts @@ -93,7 +93,7 @@ export function typeOrder(value: Value): TypeOrder { } return TypeOrder.ObjectValue; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0x6e87, 'Invalid value type', { value }); } } @@ -140,7 +140,7 @@ export function valueEquals(left: Value, right: Value): boolean { case TypeOrder.MaxValue: return true; default: - return fail('Unexpected value type: ' + JSON.stringify(left)); + return fail(0xcbf8, 'Unexpected value type', { left }); } } @@ -269,7 +269,7 @@ export function valueCompare(left: Value, right: Value): number { case TypeOrder.ObjectValue: return compareMaps(left.mapValue!, right.mapValue!); default: - throw fail('Invalid value type: ' + leftType); + throw fail(0x5ae0, 'Invalid value type', { leftType }); } } @@ -449,7 +449,7 @@ function canonifyValue(value: Value): string { } else if ('mapValue' in value) { return canonifyMap(value.mapValue!); } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0xee4d, 'Invalid value type', { value }); } } @@ -541,7 +541,7 @@ export function estimateByteSize(value: Value): number { case TypeOrder.ObjectValue: return estimateMapByteSize(value.mapValue!); default: - throw fail('Invalid value type: ' + JSON.stringify(value)); + throw fail(0x34ae, 'Invalid value type', { value }); } } @@ -701,7 +701,7 @@ export function valuesGetLowerBound(value: Value): Value { } return { mapValue: {} }; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0x8c66, 'Invalid value type', { value }); } } @@ -731,7 +731,7 @@ export function valuesGetUpperBound(value: Value): Value { } return MAX_VALUE; } else { - return fail('Invalid value type: ' + JSON.stringify(value)); + return fail(0xf207, 'Invalid value type', { value }); } } diff --git a/packages/firestore/src/platform/browser/webchannel_connection.ts b/packages/firestore/src/platform/browser/webchannel_connection.ts index 6813b88f65a..ce20eef2049 100644 --- a/packages/firestore/src/platform/browser/webchannel_connection.ts +++ b/packages/firestore/src/platform/browser/webchannel_connection.ts @@ -142,12 +142,14 @@ export class WebChannelConnection extends RestConnection { break; default: fail( - `RPC '${rpcName}' ${streamId} ` + - 'failed with unanticipated webchannel error: ' + - xhr.getLastErrorCode() + - ': ' + - xhr.getLastError() + - ', giving up.' + 0x235f, + 'RPC failed with unanticipated webchannel error. Giving up.', + { + rpcName, + streamId, + lastErrorCode: xhr.getLastErrorCode(), + lastError: xhr.getLastError() + } ); } } finally { @@ -351,7 +353,11 @@ export class WebChannelConnection extends RestConnection { msg => { if (!closed) { const msgData = msg.data[0]; - hardAssert(!!msgData, 'Got a webchannel message without data.'); + hardAssert( + !!msgData, + 0x3fdd, + 'Got a webchannel message without data.' + ); // TODO(b/35143891): There is a bug in One Platform that caused errors // (and only errors) to be wrapped in an extra array. To be forward // compatible with the bug we need to check either condition. The latter diff --git a/packages/firestore/src/platform/node/grpc_connection.ts b/packages/firestore/src/platform/node/grpc_connection.ts index dec3137af76..d50a3149416 100644 --- a/packages/firestore/src/platform/node/grpc_connection.ts +++ b/packages/firestore/src/platform/node/grpc_connection.ts @@ -48,6 +48,7 @@ function createMetadata( ): grpc.Metadata { hardAssert( authToken === null || authToken.type === 'OAuth', + 0x9048, 'If provided, token must be OAuth' ); const metadata = new grpc.Metadata(); diff --git a/packages/firestore/src/remote/datastore.ts b/packages/firestore/src/remote/datastore.ts index ac47f0cb931..f790ede0d5c 100644 --- a/packages/firestore/src/remote/datastore.ts +++ b/packages/firestore/src/remote/datastore.ts @@ -228,7 +228,9 @@ export async function invokeBatchGetDocumentsRpc( const result: Document[] = []; keys.forEach(key => { const doc = docs.get(key.toString()); - hardAssert(!!doc, 'Missing entity in write response for ' + key); + hardAssert(!!doc, 0xd7c2, 'Missing entity in write response for `key`', { + key + }); result.push(doc); }); return result; @@ -290,6 +292,7 @@ export async function invokeRunAggregationQueryRpc( hardAssert( filteredResult.length === 1, + 0xfcd7, 'Aggregation fields are missing from result.' ); debugAssert( diff --git a/packages/firestore/src/remote/persistent_stream.ts b/packages/firestore/src/remote/persistent_stream.ts index f9f64bec7f6..4f3b91652ad 100644 --- a/packages/firestore/src/remote/persistent_stream.ts +++ b/packages/firestore/src/remote/persistent_stream.ts @@ -809,6 +809,7 @@ export class PersistentWriteStream extends PersistentStream< // Always capture the last stream token. hardAssert( !!responseProto.streamToken, + 0x7a5a, 'Got a write handshake response without a stream token' ); this.lastStreamToken = responseProto.streamToken; @@ -816,6 +817,7 @@ export class PersistentWriteStream extends PersistentStream< // The first response is always the handshake response hardAssert( !responseProto.writeResults || responseProto.writeResults.length === 0, + 0xda08, 'Got mutation results for handshake' ); return this.listener!.onHandshakeComplete(); @@ -825,6 +827,7 @@ export class PersistentWriteStream extends PersistentStream< // Always capture the last stream token. hardAssert( !!responseProto.streamToken, + 0x3186, 'Got a write response without a stream token' ); this.lastStreamToken = responseProto.streamToken; diff --git a/packages/firestore/src/remote/rpc_error.ts b/packages/firestore/src/remote/rpc_error.ts index 479ceea36c9..2efee40f223 100644 --- a/packages/firestore/src/remote/rpc_error.ts +++ b/packages/firestore/src/remote/rpc_error.ts @@ -58,7 +58,7 @@ enum RpcCode { export function isPermanentError(code: Code): boolean { switch (code) { case Code.OK: - return fail('Treated status OK as error'); + return fail(0xfdaa, 'Treated status OK as error'); case Code.CANCELLED: case Code.UNKNOWN: case Code.DEADLINE_EXCEEDED: @@ -83,7 +83,7 @@ export function isPermanentError(code: Code): boolean { case Code.DATA_LOSS: return true; default: - return fail('Unknown status code: ' + code); + return fail(0x3c6b, 'Unknown status code', { code }); } } @@ -171,7 +171,7 @@ export function mapCodeFromRpcCode(code: number | undefined): Code { case RpcCode.DATA_LOSS: return Code.DATA_LOSS; default: - return fail('Unknown status code: ' + code); + return fail(0x999b, 'Unknown status code', { code }); } } @@ -220,7 +220,7 @@ export function mapRpcCodeFromCode(code: Code | undefined): number { case Code.DATA_LOSS: return RpcCode.DATA_LOSS; default: - return fail('Unknown status code: ' + code); + return fail(0x3019, 'Unknown status code', { code }); } } diff --git a/packages/firestore/src/remote/serializer.ts b/packages/firestore/src/remote/serializer.ts index 811c2ac4df6..aabdb263c1a 100644 --- a/packages/firestore/src/remote/serializer.ts +++ b/packages/firestore/src/remote/serializer.ts @@ -257,6 +257,7 @@ export function fromBytes( if (serializer.useProto3Json) { hardAssert( value === undefined || typeof value === 'string', + 0xe30b, 'value must be undefined or a string when using proto3 Json' ); return ByteString.fromBase64String(value ? value : ''); @@ -269,6 +270,7 @@ export function fromBytes( // does not indicate that it extends Uint8Array. value instanceof Buffer || value instanceof Uint8Array, + 0x3f41, 'value must be undefined, Buffer, or Uint8Array' ); return ByteString.fromUint8Array(value ? value : new Uint8Array()); @@ -283,7 +285,7 @@ export function toVersion( } export function fromVersion(version: ProtoTimestamp): SnapshotVersion { - hardAssert(!!version, "Trying to deserialize version that isn't set"); + hardAssert(!!version, 0xc050, "Trying to deserialize version that isn't set"); return SnapshotVersion.fromTimestamp(fromTimestamp(version)); } @@ -306,7 +308,9 @@ function fromResourceName(name: string): ResourcePath { const resource = ResourcePath.fromString(name); hardAssert( isValidResourceName(resource), - 'Tried to deserialize invalid key ' + resource.toString() + 0x27ce, + 'Tried to deserialize invalid key', + { key: resource.toString() } ); return resource; } @@ -389,7 +393,9 @@ function extractLocalPathFromResourceName( ): ResourcePath { hardAssert( resourceName.length > 4 && resourceName.get(4) === 'documents', - 'tried to deserialize invalid key ' + resourceName.toString() + 0x71a3, + 'tried to deserialize invalid key', + { key: resourceName.toString() } ); return resourceName.popFirst(5); } @@ -454,6 +460,7 @@ function fromFound( ): MutableDocument { hardAssert( !!doc.found, + 0xaa33, 'Tried to deserialize a found document from a missing document.' ); assertPresent(doc.found.name, 'doc.found.name'); @@ -473,10 +480,12 @@ function fromMissing( ): MutableDocument { hardAssert( !!result.missing, + 0x0f36, 'Tried to deserialize a missing document from a found document.' ); hardAssert( !!result.readTime, + 0x5995, 'Tried to deserialize a missing document without a read time.' ); const key = fromName(serializer, result.missing); @@ -493,7 +502,7 @@ export function fromBatchGetDocumentsResponse( } else if ('missing' in result) { return fromMissing(serializer, result); } - return fail('invalid batch get response: ' + JSON.stringify(result)); + return fail(0x1c42, 'invalid batch get response', { result }); } export function fromWatchChange( @@ -578,7 +587,7 @@ export function fromWatchChange( const targetId = filter.targetId; watchChange = new ExistenceFilterChange(targetId, existenceFilter); } else { - return fail('Unknown change type ' + JSON.stringify(change)); + return fail(0x2d51, 'Unknown change type', { change }); } return watchChange; } @@ -597,7 +606,7 @@ function fromWatchTargetChangeState( } else if (state === 'RESET') { return WatchTargetChangeState.Reset; } else { - return fail('Got unexpected TargetChange.state: ' + state); + return fail(0x9991, 'Got unexpected TargetChange.state', { state }); } } @@ -641,7 +650,9 @@ export function toMutation( verify: toName(serializer, mutation.key) }; } else { - return fail('Unknown mutation type ' + mutation.type); + return fail(0x40d7, 'Unknown mutation type', { + mutationType: mutation.type + }); } if (mutation.fieldTransforms.length > 0) { @@ -697,7 +708,7 @@ export function fromMutation( const key = fromName(serializer, proto.verify); return new VerifyMutation(key, precondition); } else { - return fail('unknown mutation proto: ' + JSON.stringify(proto)); + return fail(0x05b7, 'unknown mutation proto', { proto }); } } @@ -713,7 +724,7 @@ function toPrecondition( } else if (precondition.exists !== undefined) { return { exists: precondition.exists }; } else { - return fail('Unknown precondition'); + return fail(0x6b69, 'Unknown precondition'); } } @@ -755,6 +766,7 @@ export function fromWriteResults( if (protos && protos.length > 0) { hardAssert( commitTime !== undefined, + 0x3811, 'Received a write result without a commit time' ); return protos.map(proto => fromWriteResult(proto, commitTime)); @@ -793,7 +805,9 @@ function toFieldTransform( increment: transform.operand }; } else { - throw fail('Unknown transform: ' + fieldTransform.transform); + throw fail(0x51c2, 'Unknown transform', { + transform: fieldTransform.transform + }); } } @@ -805,7 +819,9 @@ function fromFieldTransform( if ('setToServerValue' in proto) { hardAssert( proto.setToServerValue === 'REQUEST_TIME', - 'Unknown server value transform proto: ' + JSON.stringify(proto) + 0x40f6, + 'Unknown server value transform proto', + { proto } ); transform = new ServerTimestampTransform(); } else if ('appendMissingElements' in proto) { @@ -820,7 +836,7 @@ function fromFieldTransform( proto.increment! ); } else { - fail('Unknown transform proto: ' + JSON.stringify(proto)); + fail(0x40c8, 'Unknown transform proto', { proto }); } const fieldPath = FieldPath.fromServerFormat(proto.fieldPath!); return new FieldTransform(fieldPath, transform!); @@ -839,7 +855,11 @@ export function fromDocumentsTarget( const count = documentsTarget.documents!.length; hardAssert( count === 1, - 'DocumentsTarget contained other than 1 document: ' + count + 0x07ae, + 'DocumentsTarget contained other than 1 document', + { + count + } ); const name = documentsTarget.documents![0]; return queryToTarget(newQueryForPath(fromQueryPath(name))); @@ -969,6 +989,7 @@ export function convertQueryTargetToQuery(target: ProtoQueryTarget): Query { if (fromCount > 0) { hardAssert( fromCount === 1, + 0xfe26, 'StructuredQuery.from with more than one collection is not supported.' ); const from = query.from![0]; @@ -1045,7 +1066,7 @@ export function toLabel(purpose: TargetPurpose): string | null { case TargetPurpose.LimboResolution: return 'limbo-document'; default: - return fail('Unrecognized query purpose: ' + purpose); + return fail(0x713b, 'Unrecognized query purpose', { purpose }); } } @@ -1116,7 +1137,7 @@ function fromFilter(filter: ProtoFilter): Filter { } else if (filter.compositeFilter !== undefined) { return fromCompositeFilter(filter); } else { - return fail('Unknown filter: ' + JSON.stringify(filter)); + return fail(0x7591, 'Unknown filter', { filter }); } } @@ -1210,9 +1231,9 @@ export function fromOperatorName(op: ProtoFieldFilterOp): Operator { case 'ARRAY_CONTAINS_ANY': return Operator.ARRAY_CONTAINS_ANY; case 'OPERATOR_UNSPECIFIED': - return fail('Unspecified operator'); + return fail(0xe2fe, 'Unspecified operator'); default: - return fail('Unknown operator'); + return fail(0xc54a, 'Unknown operator'); } } @@ -1225,7 +1246,7 @@ export function fromCompositeOperatorName( case 'OR': return CompositeOperator.OR; default: - return fail('Unknown operator'); + return fail(0x0402, 'Unknown operator'); } } @@ -1261,7 +1282,7 @@ export function toFilter(filter: Filter): ProtoFilter { } else if (filter instanceof CompositeFilter) { return toCompositeFilter(filter); } else { - return fail('Unrecognized filter type ' + JSON.stringify(filter)); + return fail(0xd65d, 'Unrecognized filter type', { filter }); } } @@ -1346,9 +1367,9 @@ export function fromUnaryFilter(filter: ProtoFilter): Filter { nullValue: 'NULL_VALUE' }); case 'OPERATOR_UNSPECIFIED': - return fail('Unspecified filter'); + return fail(0xef81, 'Unspecified filter'); default: - return fail('Unknown filter'); + return fail(0xed36, 'Unknown filter'); } } diff --git a/packages/firestore/src/remote/watch_change.ts b/packages/firestore/src/remote/watch_change.ts index 0c69163095f..a656d8fdf6e 100644 --- a/packages/firestore/src/remote/watch_change.ts +++ b/packages/firestore/src/remote/watch_change.ts @@ -203,7 +203,7 @@ class TargetState { removedDocuments = removedDocuments.add(key); break; default: - fail('Encountered invalid change type: ' + changeType); + fail(0x9481, 'Encountered invalid change type', { changeType }); } }); @@ -242,10 +242,9 @@ class TargetState { this.pendingResponses -= 1; hardAssert( this.pendingResponses >= 0, - '`pendingResponses` is less than 0. Actual value: ' + - this.pendingResponses + - '. This indicates that the SDK received more target acks from the ' + - 'server than expected. The SDK should not continue to operate.' + 0x0ca9, + '`pendingResponses` is less than 0. This indicates that the SDK received more target acks from the server than expected. The SDK should not continue to operate.', + { pendingResponses: this.pendingResponses } ); } @@ -378,7 +377,9 @@ export class WatchChangeAggregator { } break; default: - fail('Unknown target watch change state: ' + targetChange.state); + fail(0xddd6, 'Unknown target watch change state', { + state: targetChange.state + }); } }); } @@ -432,7 +433,9 @@ export class WatchChangeAggregator { } else { hardAssert( expectedCount === 1, - 'Single document existence filter with count: ' + expectedCount + 0x4e2d, + 'Single document existence filter with count', + { expectedCount } ); } } else { diff --git a/packages/firestore/src/util/assert.ts b/packages/firestore/src/util/assert.ts index 6d65e6cd19b..07ebe775e9c 100644 --- a/packages/firestore/src/util/assert.ts +++ b/packages/firestore/src/util/assert.ts @@ -26,12 +26,61 @@ import { logError } from './log'; * Returns `never` and can be used in expressions: * @example * let futureVar = fail('not implemented yet'); + * + * @param code generate a new unique value with `yarn assertion-id:generate` + * Search for an existing value using `yarn assertion-id:find X` + */ +export function fail( + code: number, + message: string, + context?: Record +): never; + +/** + * Unconditionally fails, throwing an Error with the given message. + * Messages are stripped in production builds. + * + * Returns `never` and can be used in expressions: + * @example + * let futureVar = fail('not implemented yet'); + * + * @param id generate a new unique value with `yarn assertion-id:generate` + * Search for an existing value using `yarn assertion-id:find X` */ -export function fail(failure: string = 'Unexpected state'): never { +export function fail(id: number, context?: Record): never; + +export function fail( + id: number, + messageOrContext?: string | Record, + context?: Record +): never { + let message = 'Unexpected state'; + if (typeof messageOrContext === 'string') { + message = messageOrContext; + } else { + context = messageOrContext; + } + _fail(id, message, context); +} + +function _fail( + id: number, + failure: string, + context?: Record +): never { // Log the failure in addition to throw an exception, just in case the // exception is swallowed. - const message = - `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ` + failure; + let message = `FIRESTORE (${SDK_VERSION}) INTERNAL ASSERTION FAILED: ${failure} (ID: ${id.toString( + 16 + )})`; + if (context !== undefined) { + try { + const stringContext = JSON.stringify(context); + message += ' CONTEXT: ' + stringContext; + } catch (e) { + message += ' CONTEXT: ' + context; + } + } logError(message); // NOTE: We don't use FirestoreError here because these are internal failures @@ -45,13 +94,47 @@ export function fail(failure: string = 'Unexpected state'): never { * given message if it did. * * Messages are stripped in production builds. + * + * @param id generate a new unique value with `yarn assertion-idgenerate`. + * Search for an existing value using `yarn assertion-id:find X` + */ +export function hardAssert( + assertion: boolean, + id: number, + message: string, + context?: Record +): asserts assertion; + +/** + * Fails if the given assertion condition is false, throwing an Error with the + * given message if it did. + * + * Messages are stripped in production builds. + * + * @param id generate a new unique value with `yarn assertion-id:generate`. + * Search for an existing value using `yarn assertion-id:find X` */ export function hardAssert( assertion: boolean, - message?: string + id: number, + context?: Record +): asserts assertion; + +export function hardAssert( + assertion: boolean, + id: number, + messageOrContext?: string | Record, + context?: Record ): asserts assertion { + let message = 'Unexpected state'; + if (typeof messageOrContext === 'string') { + message = messageOrContext; + } else { + context = messageOrContext; + } + if (!assertion) { - fail(message); + _fail(id, message, context); } } @@ -70,7 +153,7 @@ export function debugAssert( message: string ): asserts assertion { if (!assertion) { - fail(message); + fail(0xdeb6, message); } } diff --git a/packages/firestore/src/util/async_queue_impl.ts b/packages/firestore/src/util/async_queue_impl.ts index 79eb6c23850..f8c7a995761 100644 --- a/packages/firestore/src/util/async_queue_impl.ts +++ b/packages/firestore/src/util/async_queue_impl.ts @@ -236,7 +236,9 @@ export class AsyncQueueImpl implements AsyncQueue { private verifyNotFailed(): void { if (this.failure) { - fail('AsyncQueue is already failed: ' + getMessageOrStack(this.failure)); + fail(0xb815, 'AsyncQueue is already failed', { + messageOrStack: getMessageOrStack(this.failure) + }); } } diff --git a/packages/firestore/src/util/input_validation.ts b/packages/firestore/src/util/input_validation.ts index 37e349ce910..7fd9967b5a0 100644 --- a/packages/firestore/src/util/input_validation.ts +++ b/packages/firestore/src/util/input_validation.ts @@ -128,7 +128,7 @@ export function valueDescription(input: unknown): string { } else if (typeof input === 'function') { return 'a function'; } else { - return fail('Unknown wrong type: ' + typeof input); + return fail(0x3029, 'Unknown wrong type', { type: typeof input }); } } diff --git a/packages/firestore/src/util/logic_utils.ts b/packages/firestore/src/util/logic_utils.ts index 3c3a6b19fd8..b2167c385e9 100644 --- a/packages/firestore/src/util/logic_utils.ts +++ b/packages/firestore/src/util/logic_utils.ts @@ -44,6 +44,7 @@ import { hardAssert } from './assert'; export function computeInExpansion(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x4e2c, 'Only field filters and composite filters are accepted.' ); @@ -90,6 +91,7 @@ export function getDnfTerms(filter: CompositeFilter): Filter[] { hardAssert( isDisjunctiveNormalForm(result), + 0x1cdf, 'computeDistributedNormalForm did not result in disjunctive normal form' ); @@ -157,6 +159,7 @@ function isDisjunctionOfFieldFiltersAndFlatConjunctions( export function computeDistributedNormalForm(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x84e2, 'Only field filters and composite filters are accepted.' ); @@ -182,14 +185,17 @@ export function computeDistributedNormalForm(filter: Filter): Filter { hardAssert( newFilter instanceof CompositeFilter, + 0xfbf2, 'field filters are already in DNF form' ); hardAssert( compositeFilterIsConjunction(newFilter), + 0x9d3b, 'Disjunction of filters all of which are already in DNF form is itself in DNF form.' ); hardAssert( newFilter.filters.length > 1, + 0xe247, 'Single-filter composite filters are already in DNF form.' ); @@ -201,10 +207,12 @@ export function computeDistributedNormalForm(filter: Filter): Filter { export function applyDistribution(lhs: Filter, rhs: Filter): Filter { hardAssert( lhs instanceof FieldFilter || lhs instanceof CompositeFilter, + 0x95f4, 'Only field filters and composite filters are accepted.' ); hardAssert( rhs instanceof FieldFilter || rhs instanceof CompositeFilter, + 0x6381, 'Only field filters and composite filters are accepted.' ); @@ -245,6 +253,7 @@ function applyDistributionCompositeFilters( ): Filter { hardAssert( lhs.filters.length > 0 && rhs.filters.length > 0, + 0xbb85, 'Found an empty composite filter' ); @@ -306,6 +315,7 @@ function applyDistributionFieldAndCompositeFilters( export function applyAssociation(filter: Filter): Filter { hardAssert( filter instanceof FieldFilter || filter instanceof CompositeFilter, + 0x2e4a, 'Only field filters and composite filters are accepted.' ); diff --git a/packages/firestore/src/util/sorted_map.ts b/packages/firestore/src/util/sorted_map.ts index a24cf8802ca..023354173d3 100644 --- a/packages/firestore/src/util/sorted_map.ts +++ b/packages/firestore/src/util/sorted_map.ts @@ -511,14 +511,20 @@ export class LLRBNode { // leaves is equal on both sides. This function verifies that or asserts. protected check(): number { if (this.isRed() && this.left.isRed()) { - throw fail('Red node has red child(' + this.key + ',' + this.value + ')'); + throw fail(0xaad2, 'Red node has red child', { + key: this.key, + value: this.value + }); } if (this.right.isRed()) { - throw fail('Right child of (' + this.key + ',' + this.value + ') is red'); + throw fail(0x3721, 'Right child of (`key`, `value`) is red', { + key: this.key, + value: this.value + }); } const blackDepth = (this.left as LLRBNode).check(); if (blackDepth !== (this.right as LLRBNode).check()) { - throw fail('Black depths differ'); + throw fail(0x6d2d, 'Black depths differ'); } else { return blackDepth + (this.isRed() ? 0 : 1); } @@ -528,19 +534,19 @@ export class LLRBNode { // Represents an empty node (a leaf node in the Red-Black Tree). export class LLRBEmptyNode { get key(): never { - throw fail('LLRBEmptyNode has no key.'); + throw fail(0xe1a6, 'LLRBEmptyNode has no key.'); } get value(): never { - throw fail('LLRBEmptyNode has no value.'); + throw fail(0x3f0d, 'LLRBEmptyNode has no value.'); } get color(): never { - throw fail('LLRBEmptyNode has no color.'); + throw fail(0x4157, 'LLRBEmptyNode has no color.'); } get left(): never { - throw fail('LLRBEmptyNode has no left child.'); + throw fail(0x741e, 'LLRBEmptyNode has no left child.'); } get right(): never { - throw fail('LLRBEmptyNode has no right child.'); + throw fail(0x901e, 'LLRBEmptyNode has no right child.'); } size = 0; diff --git a/packages/firestore/test/unit/index/ordered_code_writer.test.ts b/packages/firestore/test/unit/index/ordered_code_writer.test.ts index 6d87ddb4849..27956c730ee 100644 --- a/packages/firestore/test/unit/index/ordered_code_writer.test.ts +++ b/packages/firestore/test/unit/index/ordered_code_writer.test.ts @@ -248,7 +248,14 @@ function getBytes(val: unknown): { asc: Uint8Array; desc: Uint8Array } { ascWriter.writeUtf8Ascending(val); descWriter.writeUtf8Descending(val); } else { - hardAssert(val instanceof Uint8Array); + hardAssert( + val instanceof Uint8Array, + 0xa10f, + 'val is not instance of Uint8Array', + { + val + } + ); ascWriter.writeBytesAscending(ByteString.fromUint8Array(val)); descWriter.writeBytesDescending(ByteString.fromUint8Array(val)); } diff --git a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts index e44bb73e47b..1240c977cee 100644 --- a/packages/firestore/test/unit/local/indexeddb_persistence.test.ts +++ b/packages/firestore/test/unit/local/indexeddb_persistence.test.ts @@ -1609,6 +1609,6 @@ function toLegacyDbRemoteDocument( parentPath }; } else { - return fail('Unexpected Document ' + document); + return fail(0x6bb7, 'Unexpected Document ', { document }); } } diff --git a/packages/firestore/test/unit/local/simple_db.test.ts b/packages/firestore/test/unit/local/simple_db.test.ts index b2b7ed3f95a..207e454fb5b 100644 --- a/packages/firestore/test/unit/local/simple_db.test.ts +++ b/packages/firestore/test/unit/local/simple_db.test.ts @@ -363,7 +363,7 @@ describe('SimpleDb', () => { iterated.push(value); return PersistencePromise.reject(new Error('Expected error')); }) - .next(() => fail('Promise not rejected')) + .next(() => fail(0xb9b3, 'Promise not rejected')) .catch(err => { expect(err.message).to.eq('Expected error'); expect(iterated).to.deep.equal([testData[0]]); diff --git a/packages/firestore/test/unit/specs/spec_builder.ts b/packages/firestore/test/unit/specs/spec_builder.ts index 80dcd6519de..3e52c5873b9 100644 --- a/packages/firestore/test/unit/specs/spec_builder.ts +++ b/packages/firestore/test/unit/specs/spec_builder.ts @@ -674,7 +674,7 @@ export class SpecBuilder { } else if (doc.isNoDocument()) { // Don't send any updates } else { - fail('Unknown parameter: ' + doc); + fail(0xd71c, 'Unknown parameter', { doc }); } this.watchCurrents(query, 'resume-token-' + version); this.watchSnapshots(version); @@ -1149,7 +1149,7 @@ export class SpecBuilder { userDataWriter.convertValue(filter.value) ] as SpecQueryFilter; } else { - return fail('Unknown filter: ' + filter); + return fail(0x0e51, 'Unknown filter', { filter }); } }); } @@ -1210,7 +1210,10 @@ export class SpecBuilder { targetPurpose?: TargetPurpose ): void { if (!(resume?.resumeToken || resume?.readTime) && resume?.expectedCount) { - fail('Expected count is present without a resume token or read time.'); + fail( + 0xc9a1, + 'Expected count is present without a resume token or read time.' + ); } if (this.activeTargets[targetId]) { @@ -1273,7 +1276,10 @@ export class SpecBuilder { if (queryTargetId && limboTargetId) { // TODO(dimond): add support for query for doc and limbo doc at the same // time? - fail('Found both query and limbo doc with target ID, not supported yet'); + fail( + 0x6e17, + 'Found both query and limbo doc with target ID, not supported yet' + ); } const targetId = queryTargetId || limboTargetId; debugAssert( diff --git a/packages/firestore/test/unit/specs/spec_test_components.ts b/packages/firestore/test/unit/specs/spec_test_components.ts index 2a2e480de63..017afe1d924 100644 --- a/packages/firestore/test/unit/specs/spec_test_components.ts +++ b/packages/firestore/test/unit/specs/spec_test_components.ts @@ -415,7 +415,7 @@ export class MockConnection implements Connection { } else if (request.removeTarget) { delete this.activeTargets[request.removeTarget]; } else { - fail('Invalid listen request'); + fail(0x782d, 'Invalid listen request'); } }, closeFn: () => { diff --git a/packages/firestore/test/unit/specs/spec_test_runner.ts b/packages/firestore/test/unit/specs/spec_test_runner.ts index b34421d9e0a..ee0af0b8bf8 100644 --- a/packages/firestore/test/unit/specs/spec_test_runner.ts +++ b/packages/firestore/test/unit/specs/spec_test_runner.ts @@ -477,7 +477,7 @@ abstract class TestRunner { ? this.doFailDatabase(step.failDatabase!) : this.doRecoverDatabase(); } else { - return fail('Unknown step: ' + JSON.stringify(step)); + return fail(0x6bb3, 'Unknown step: ' + JSON.stringify(step)); } } @@ -724,7 +724,7 @@ abstract class TestRunner { ); return this.doWatchEvent(change); } else { - return fail('Either doc or docs must be set'); + return fail(0xdcc3, 'Either doc or docs must be set'); } } diff --git a/packages/firestore/test/unit/util/assert.test.ts b/packages/firestore/test/unit/util/assert.test.ts new file mode 100644 index 00000000000..e865337a3a3 --- /dev/null +++ b/packages/firestore/test/unit/util/assert.test.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import { fail, hardAssert } from '../../../src/util/assert'; + +describe('hardAssert', () => { + it('includes the error code as hex', () => { + expect(() => hardAssert(false, 0x1234, 'a message here')).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => + hardAssert(false, 0x1234, 'a message here', { foo: 'bar baz' }) + ).to.throw('bar baz'); + }); + + it('includes the message', () => { + expect(() => + hardAssert(false, 0x1234, 'a message here', { foo: 'bar baz' }) + ).to.throw('a message here'); + }); + + describe('without message', () => { + it('includes the error code as hex', () => { + expect(() => hardAssert(false, 0x1234)).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => hardAssert(false, 0x1234, { foo: 'bar baz' })).to.throw( + 'bar baz' + ); + }); + it('includes a default message', () => { + expect(() => hardAssert(false, 0x1234, { foo: 'bar baz' })).to.throw( + 'Unexpected state' + ); + }); + }); +}); + +describe('fail', () => { + it('includes the error code as hex', () => { + expect(() => fail(0x1234, 'a message here')).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => fail(0x1234, 'a message here', { foo: 'bar baz' })).to.throw( + 'bar baz' + ); + }); + + it('includes the message', () => { + expect(() => fail(0x1234, 'a message here', { foo: 'bar baz' })).to.throw( + 'a message here' + ); + }); + + describe('without message', () => { + it('includes the error code as hex', () => { + expect(() => fail(0x1234)).to.throw('1234'); + }); + + it('includes the context', () => { + expect(() => fail(0x1234, { foo: 'bar baz' })).to.throw('bar baz'); + }); + it('includes a default message', () => { + expect(() => fail(0x1234, { foo: 'bar baz' })).to.throw( + 'Unexpected state' + ); + }); + }); +}); diff --git a/packages/firestore/test/unit/util/async_queue.test.ts b/packages/firestore/test/unit/util/async_queue.test.ts index cc55879c88b..48aa02fe298 100644 --- a/packages/firestore/test/unit/util/async_queue.test.ts +++ b/packages/firestore/test/unit/util/async_queue.test.ts @@ -139,7 +139,7 @@ describe('AsyncQueue', () => { Promise.reject('dummyOp should not be run'); expect(() => { queue.enqueueAndForget(dummyOp); - }).to.throw(/already failed:.*Simulated Error/); + }).to.throw(/already failed.*Simulated Error/); // Finally, restore log level. setLogLevel(oldLogLevel as unknown as LogLevelString); @@ -247,7 +247,7 @@ describe('AsyncQueue', () => { const deferred = new Deferred(); queue.enqueueRetryable(async () => { deferred.resolve(); - throw fail('Simulated test failure'); + throw fail(0x1576, 'Simulated test failure'); }); await deferred.promise; await expect( diff --git a/packages/firestore/test/util/helpers.ts b/packages/firestore/test/util/helpers.ts index c5865c3e0f7..1ddaf174b19 100644 --- a/packages/firestore/test/util/helpers.ts +++ b/packages/firestore/test/util/helpers.ts @@ -867,11 +867,13 @@ export function expectEqual(left: any, right: any, message?: string): void { message = message || ''; if (typeof left.isEqual !== 'function') { return fail( + 0x8004, JSON.stringify(left) + ' does not support isEqual (left) ' + message ); } if (typeof right.isEqual !== 'function') { return fail( + 0xebc9, JSON.stringify(right) + ' does not support isEqual (right) ' + message ); } diff --git a/packages/firestore/test/util/spec_test_helpers.ts b/packages/firestore/test/util/spec_test_helpers.ts index 7e2f77da438..73378a361a3 100644 --- a/packages/firestore/test/util/spec_test_helpers.ts +++ b/packages/firestore/test/util/spec_test_helpers.ts @@ -100,7 +100,10 @@ export function encodeWatchChange( } }; } - return fail('Unrecognized watch change: ' + JSON.stringify(watchChange)); + return fail( + 0xf8e5, + 'Unrecognized watch change: ' + JSON.stringify(watchChange) + ); } function encodeTargetChangeTargetChangeType( @@ -118,6 +121,6 @@ function encodeTargetChangeTargetChangeType( case WatchTargetChangeState.Reset: return 'RESET'; default: - return fail('Unknown WatchTargetChangeState: ' + state); + return fail(0x368b, 'Unknown WatchTargetChangeState: ' + state); } } diff --git a/packages/firestore/test/util/test_platform.ts b/packages/firestore/test/util/test_platform.ts index 7803a2fb3c6..a0eac93cbc9 100644 --- a/packages/firestore/test/util/test_platform.ts +++ b/packages/firestore/test/util/test_platform.ts @@ -64,7 +64,7 @@ export class FakeWindow implements WindowLike { // listeners. break; default: - fail(`MockWindow doesn't support events of type '${type}'`); + fail(0xe53d, `MockWindow doesn't support events of type '${type}'`); } }