pyric 0.0.1 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/dist/app/index.d.ts +81 -0
- package/dist/app/index.d.ts.map +1 -0
- package/dist/app/index.js +78 -0
- package/dist/app/index.js.map +1 -0
- package/dist/auth/index.d.ts +346 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +701 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/prod-backend.d.ts +39 -0
- package/dist/auth/prod-backend.d.ts.map +1 -0
- package/dist/auth/prod-backend.js +179 -0
- package/dist/auth/prod-backend.js.map +1 -0
- package/dist/auth/providers.d.ts +107 -0
- package/dist/auth/providers.d.ts.map +1 -0
- package/dist/auth/providers.js +138 -0
- package/dist/auth/providers.js.map +1 -0
- package/dist/auth/sandbox-backend.d.ts +769 -0
- package/dist/auth/sandbox-backend.d.ts.map +1 -0
- package/dist/auth/sandbox-backend.js +1598 -0
- package/dist/auth/sandbox-backend.js.map +1 -0
- package/dist/auth/target.d.ts +40 -0
- package/dist/auth/target.d.ts.map +1 -0
- package/dist/auth/target.js +28 -0
- package/dist/auth/target.js.map +1 -0
- package/dist/auth/types.d.ts +252 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +26 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/database/constraints/atoms.d.ts +29 -0
- package/dist/database/constraints/atoms.d.ts.map +1 -0
- package/dist/database/constraints/atoms.js +43 -0
- package/dist/database/constraints/atoms.js.map +1 -0
- package/dist/database/constraints/compose.d.ts +16 -0
- package/dist/database/constraints/compose.d.ts.map +1 -0
- package/dist/database/constraints/compose.js +15 -0
- package/dist/database/constraints/compose.js.map +1 -0
- package/dist/database/constraints/data.d.ts +30 -0
- package/dist/database/constraints/data.d.ts.map +1 -0
- package/dist/database/constraints/data.js +46 -0
- package/dist/database/constraints/data.js.map +1 -0
- package/dist/database/constraints/document.d.ts +42 -0
- package/dist/database/constraints/document.d.ts.map +1 -0
- package/dist/database/constraints/document.js +86 -0
- package/dist/database/constraints/document.js.map +1 -0
- package/dist/database/constraints/game.d.ts +30 -0
- package/dist/database/constraints/game.d.ts.map +1 -0
- package/dist/database/constraints/game.js +49 -0
- package/dist/database/constraints/game.js.map +1 -0
- package/dist/database/constraints/index.d.ts +12 -0
- package/dist/database/constraints/index.d.ts.map +1 -0
- package/dist/database/constraints/index.js +9 -0
- package/dist/database/constraints/index.js.map +1 -0
- package/dist/database/constraints/policies.d.ts +16 -0
- package/dist/database/constraints/policies.d.ts.map +1 -0
- package/dist/database/constraints/policies.js +17 -0
- package/dist/database/constraints/policies.js.map +1 -0
- package/dist/database/constraints/ruleset.d.ts +5 -0
- package/dist/database/constraints/ruleset.d.ts.map +1 -0
- package/dist/database/constraints/ruleset.js +132 -0
- package/dist/database/constraints/ruleset.js.map +1 -0
- package/dist/database/constraints/schema.d.ts +17 -0
- package/dist/database/constraints/schema.d.ts.map +1 -0
- package/dist/database/constraints/schema.js +74 -0
- package/dist/database/constraints/schema.js.map +1 -0
- package/dist/database/constraints/types.d.ts +22 -0
- package/dist/database/constraints/types.d.ts.map +1 -0
- package/dist/database/constraints/types.js +2 -0
- package/dist/database/constraints/types.js.map +1 -0
- package/dist/database/crawl/handler.d.ts +7 -0
- package/dist/database/crawl/handler.d.ts.map +1 -0
- package/dist/database/crawl/handler.js +91 -0
- package/dist/database/crawl/handler.js.map +1 -0
- package/dist/database/crawl/semaphore.d.ts +9 -0
- package/dist/database/crawl/semaphore.d.ts.map +1 -0
- package/dist/database/crawl/semaphore.js +27 -0
- package/dist/database/crawl/semaphore.js.map +1 -0
- package/dist/database/crawl/spec.d.ts +33 -0
- package/dist/database/crawl/spec.d.ts.map +1 -0
- package/dist/database/crawl/spec.js +12 -0
- package/dist/database/crawl/spec.js.map +1 -0
- package/dist/database/data/handler.d.ts +9 -0
- package/dist/database/data/handler.d.ts.map +1 -0
- package/dist/database/data/handler.js +96 -0
- package/dist/database/data/handler.js.map +1 -0
- package/dist/database/data/spec.d.ts +77 -0
- package/dist/database/data/spec.d.ts.map +1 -0
- package/dist/database/data/spec.js +17 -0
- package/dist/database/data/spec.js.map +1 -0
- package/dist/database/data/validated.d.ts +8 -0
- package/dist/database/data/validated.d.ts.map +1 -0
- package/dist/database/data/validated.js +99 -0
- package/dist/database/data/validated.js.map +1 -0
- package/dist/database/grammar/RtdbExpr.ohm +98 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts +2 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.js +103 -0
- package/dist/database/grammar/RtdbExpr.ohm.generated.js.map +1 -0
- package/dist/database/grammar/RtdbExprParser.d.ts +5 -0
- package/dist/database/grammar/RtdbExprParser.d.ts.map +1 -0
- package/dist/database/grammar/RtdbExprParser.js +53 -0
- package/dist/database/grammar/RtdbExprParser.js.map +1 -0
- package/dist/database/grammar/linter.d.ts +3 -0
- package/dist/database/grammar/linter.d.ts.map +1 -0
- package/dist/database/grammar/linter.js +66 -0
- package/dist/database/grammar/linter.js.map +1 -0
- package/dist/database/grammar/simulator.d.ts +31 -0
- package/dist/database/grammar/simulator.d.ts.map +1 -0
- package/dist/database/grammar/simulator.js +255 -0
- package/dist/database/grammar/simulator.js.map +1 -0
- package/dist/database/grammar/validator.d.ts +3 -0
- package/dist/database/grammar/validator.d.ts.map +1 -0
- package/dist/database/grammar/validator.js +84 -0
- package/dist/database/grammar/validator.js.map +1 -0
- package/dist/database/host.d.ts +50 -0
- package/dist/database/host.d.ts.map +1 -0
- package/dist/database/host.js +41 -0
- package/dist/database/host.js.map +1 -0
- package/dist/database/index.d.ts +40 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +47 -0
- package/dist/database/index.js.map +1 -0
- package/dist/database/initialize-from-app.d.ts +28 -0
- package/dist/database/initialize-from-app.d.ts.map +1 -0
- package/dist/database/initialize-from-app.js +23 -0
- package/dist/database/initialize-from-app.js.map +1 -0
- package/dist/database/ir/handler.d.ts +6 -0
- package/dist/database/ir/handler.d.ts.map +1 -0
- package/dist/database/ir/handler.js +52 -0
- package/dist/database/ir/handler.js.map +1 -0
- package/dist/database/ir/spec.d.ts +25 -0
- package/dist/database/ir/spec.d.ts.map +1 -0
- package/dist/database/ir/spec.js +11 -0
- package/dist/database/ir/spec.js.map +1 -0
- package/dist/database/mapper.d.ts +9 -0
- package/dist/database/mapper.d.ts.map +1 -0
- package/dist/database/mapper.js +115 -0
- package/dist/database/mapper.js.map +1 -0
- package/dist/database/modular.d.ts +604 -0
- package/dist/database/modular.d.ts.map +1 -0
- package/dist/database/modular.js +1226 -0
- package/dist/database/modular.js.map +1 -0
- package/dist/database/replay.d.ts +33 -0
- package/dist/database/replay.d.ts.map +1 -0
- package/dist/database/replay.js +124 -0
- package/dist/database/replay.js.map +1 -0
- package/dist/database/resolver.d.ts +4 -0
- package/dist/database/resolver.d.ts.map +1 -0
- package/dist/database/resolver.js +66 -0
- package/dist/database/resolver.js.map +1 -0
- package/dist/database/sandbox/backend.d.ts +403 -0
- package/dist/database/sandbox/backend.d.ts.map +1 -0
- package/dist/database/sandbox/backend.js +1467 -0
- package/dist/database/sandbox/backend.js.map +1 -0
- package/dist/database/sandbox/data-tree.d.ts +123 -0
- package/dist/database/sandbox/data-tree.d.ts.map +1 -0
- package/dist/database/sandbox/data-tree.js +310 -0
- package/dist/database/sandbox/data-tree.js.map +1 -0
- package/dist/database/sandbox/normalize.d.ts +77 -0
- package/dist/database/sandbox/normalize.d.ts.map +1 -0
- package/dist/database/sandbox/normalize.js +165 -0
- package/dist/database/sandbox/normalize.js.map +1 -0
- package/dist/database/sandbox/push-id.d.ts +32 -0
- package/dist/database/sandbox/push-id.d.ts.map +1 -0
- package/dist/database/sandbox/push-id.js +82 -0
- package/dist/database/sandbox/push-id.js.map +1 -0
- package/dist/database/sandbox/query.d.ts +175 -0
- package/dist/database/sandbox/query.d.ts.map +1 -0
- package/dist/database/sandbox/query.js +269 -0
- package/dist/database/sandbox/query.js.map +1 -0
- package/dist/database/sandbox/rules-eval.d.ts +69 -0
- package/dist/database/sandbox/rules-eval.d.ts.map +1 -0
- package/dist/database/sandbox/rules-eval.js +124 -0
- package/dist/database/sandbox/rules-eval.js.map +1 -0
- package/dist/database/sandbox/sentinels.d.ts +71 -0
- package/dist/database/sandbox/sentinels.d.ts.map +1 -0
- package/dist/database/sandbox/sentinels.js +92 -0
- package/dist/database/sandbox/sentinels.js.map +1 -0
- package/dist/database/simulation/handler.d.ts +6 -0
- package/dist/database/simulation/handler.d.ts.map +1 -0
- package/dist/database/simulation/handler.js +236 -0
- package/dist/database/simulation/handler.js.map +1 -0
- package/dist/database/simulation/spec.d.ts +69 -0
- package/dist/database/simulation/spec.d.ts.map +1 -0
- package/dist/database/simulation/spec.js +25 -0
- package/dist/database/simulation/spec.js.map +1 -0
- package/dist/database/tools.d.ts +22 -0
- package/dist/database/tools.d.ts.map +1 -0
- package/dist/database/tools.js +272 -0
- package/dist/database/tools.js.map +1 -0
- package/dist/database/types.d.ts +210 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +26 -0
- package/dist/database/types.js.map +1 -0
- package/dist/database/write/handler.d.ts +7 -0
- package/dist/database/write/handler.d.ts.map +1 -0
- package/dist/database/write/handler.js +42 -0
- package/dist/database/write/handler.js.map +1 -0
- package/dist/database/write/spec.d.ts +18 -0
- package/dist/database/write/spec.d.ts.map +1 -0
- package/dist/database/write/spec.js +7 -0
- package/dist/database/write/spec.js.map +1 -0
- package/dist/firestore/index.d.ts +533 -0
- package/dist/firestore/index.d.ts.map +1 -0
- package/dist/firestore/index.js +1536 -0
- package/dist/firestore/index.js.map +1 -0
- package/dist/firestore/tools.d.ts +67 -0
- package/dist/firestore/tools.d.ts.map +1 -0
- package/dist/firestore/tools.js +318 -0
- package/dist/firestore/tools.js.map +1 -0
- package/dist/firestore-values/index.d.ts +63 -0
- package/dist/firestore-values/index.d.ts.map +1 -0
- package/dist/firestore-values/index.js +148 -0
- package/dist/firestore-values/index.js.map +1 -0
- package/dist/project-scope.d.ts +5 -0
- package/dist/project-scope.d.ts.map +1 -0
- package/dist/project-scope.js +2 -0
- package/dist/project-scope.js.map +1 -0
- package/dist/rules/extract.d.ts +28 -0
- package/dist/rules/extract.d.ts.map +1 -0
- package/dist/rules/extract.js +25 -0
- package/dist/rules/extract.js.map +1 -0
- package/dist/rules/generators/assembler.d.ts +19 -0
- package/dist/rules/generators/assembler.d.ts.map +1 -0
- package/dist/rules/generators/assembler.js +292 -0
- package/dist/rules/generators/assembler.js.map +1 -0
- package/dist/rules/generators/expressions.d.ts +52 -0
- package/dist/rules/generators/expressions.d.ts.map +1 -0
- package/dist/rules/generators/expressions.js +117 -0
- package/dist/rules/generators/expressions.js.map +1 -0
- package/dist/rules/generators/grid.d.ts +22 -0
- package/dist/rules/generators/grid.d.ts.map +1 -0
- package/dist/rules/generators/grid.js +52 -0
- package/dist/rules/generators/grid.js.map +1 -0
- package/dist/rules/grammar/FirestoreAST.d.ts +129 -0
- package/dist/rules/grammar/FirestoreAST.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreAST.js +3 -0
- package/dist/rules/grammar/FirestoreAST.js.map +1 -0
- package/dist/rules/grammar/FirestoreAssembler.d.ts +5 -0
- package/dist/rules/grammar/FirestoreAssembler.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreAssembler.js +158 -0
- package/dist/rules/grammar/FirestoreAssembler.js.map +1 -0
- package/dist/rules/grammar/FirestoreParser.d.ts +46 -0
- package/dist/rules/grammar/FirestoreParser.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreParser.js +409 -0
- package/dist/rules/grammar/FirestoreParser.js.map +1 -0
- package/dist/rules/grammar/FirestoreRules.ohm +229 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts +2 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.js +234 -0
- package/dist/rules/grammar/FirestoreRules.ohm.generated.js.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts +10 -0
- package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -0
- package/dist/rules/grammar/FirestoreValidator.js +482 -0
- package/dist/rules/grammar/FirestoreValidator.js.map +1 -0
- package/dist/rules/index.d.ts +67 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +80 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/indexes/extract/annotation-collect.d.ts +42 -0
- package/dist/rules/indexes/extract/annotation-collect.d.ts.map +1 -0
- package/dist/rules/indexes/extract/annotation-collect.js +69 -0
- package/dist/rules/indexes/extract/annotation-collect.js.map +1 -0
- package/dist/rules/indexes/extract/annotations.d.ts +50 -0
- package/dist/rules/indexes/extract/annotations.d.ts.map +1 -0
- package/dist/rules/indexes/extract/annotations.js +144 -0
- package/dist/rules/indexes/extract/annotations.js.map +1 -0
- package/dist/rules/indexes/extract/ast.d.ts +54 -0
- package/dist/rules/indexes/extract/ast.d.ts.map +1 -0
- package/dist/rules/indexes/extract/ast.js +119 -0
- package/dist/rules/indexes/extract/ast.js.map +1 -0
- package/dist/rules/indexes/extract/classify.d.ts +41 -0
- package/dist/rules/indexes/extract/classify.d.ts.map +1 -0
- package/dist/rules/indexes/extract/classify.js +138 -0
- package/dist/rules/indexes/extract/classify.js.map +1 -0
- package/dist/rules/indexes/extract/composite.d.ts +38 -0
- package/dist/rules/indexes/extract/composite.d.ts.map +1 -0
- package/dist/rules/indexes/extract/composite.js +85 -0
- package/dist/rules/indexes/extract/composite.js.map +1 -0
- package/dist/rules/indexes/extract/dataflow.d.ts +45 -0
- package/dist/rules/indexes/extract/dataflow.d.ts.map +1 -0
- package/dist/rules/indexes/extract/dataflow.js +200 -0
- package/dist/rules/indexes/extract/dataflow.js.map +1 -0
- package/dist/rules/indexes/extract/enumerate.d.ts +41 -0
- package/dist/rules/indexes/extract/enumerate.d.ts.map +1 -0
- package/dist/rules/indexes/extract/enumerate.js +163 -0
- package/dist/rules/indexes/extract/enumerate.js.map +1 -0
- package/dist/rules/indexes/extract/extractor.d.ts +3 -0
- package/dist/rules/indexes/extract/extractor.d.ts.map +1 -0
- package/dist/rules/indexes/extract/extractor.js +274 -0
- package/dist/rules/indexes/extract/extractor.js.map +1 -0
- package/dist/rules/indexes/extract/types.d.ts +211 -0
- package/dist/rules/indexes/extract/types.d.ts.map +1 -0
- package/dist/rules/indexes/extract/types.js +14 -0
- package/dist/rules/indexes/extract/types.js.map +1 -0
- package/dist/rules/indexes/extractHandler.d.ts +16 -0
- package/dist/rules/indexes/extractHandler.d.ts.map +1 -0
- package/dist/rules/indexes/extractHandler.js +100 -0
- package/dist/rules/indexes/extractHandler.js.map +1 -0
- package/dist/rules/indexes/extractTool.d.ts +15 -0
- package/dist/rules/indexes/extractTool.d.ts.map +1 -0
- package/dist/rules/indexes/extractTool.js +55 -0
- package/dist/rules/indexes/extractTool.js.map +1 -0
- package/dist/rules/indexes/types.d.ts +71 -0
- package/dist/rules/indexes/types.d.ts.map +1 -0
- package/dist/rules/indexes/types.js +15 -0
- package/dist/rules/indexes/types.js.map +1 -0
- package/dist/rules/inspect/handler.d.ts +6 -0
- package/dist/rules/inspect/handler.d.ts.map +1 -0
- package/dist/rules/inspect/handler.js +144 -0
- package/dist/rules/inspect/handler.js.map +1 -0
- package/dist/rules/inspect/spec.d.ts +37 -0
- package/dist/rules/inspect/spec.d.ts.map +1 -0
- package/dist/rules/inspect/spec.js +2 -0
- package/dist/rules/inspect/spec.js.map +1 -0
- package/dist/rules/inspect/tools.d.ts +21 -0
- package/dist/rules/inspect/tools.d.ts.map +1 -0
- package/dist/rules/inspect/tools.js +21 -0
- package/dist/rules/inspect/tools.js.map +1 -0
- package/dist/rules/linter/ast-utils.d.ts +97 -0
- package/dist/rules/linter/ast-utils.d.ts.map +1 -0
- package/dist/rules/linter/ast-utils.js +491 -0
- package/dist/rules/linter/ast-utils.js.map +1 -0
- package/dist/rules/linter/hallucinations.d.ts +34 -0
- package/dist/rules/linter/hallucinations.d.ts.map +1 -0
- package/dist/rules/linter/hallucinations.js +334 -0
- package/dist/rules/linter/hallucinations.js.map +1 -0
- package/dist/rules/linter/linter.d.ts +84 -0
- package/dist/rules/linter/linter.d.ts.map +1 -0
- package/dist/rules/linter/linter.js +784 -0
- package/dist/rules/linter/linter.js.map +1 -0
- package/dist/rules/modules/resolver-browser.d.ts +41 -0
- package/dist/rules/modules/resolver-browser.d.ts.map +1 -0
- package/dist/rules/modules/resolver-browser.js +63 -0
- package/dist/rules/modules/resolver-browser.js.map +1 -0
- package/dist/rules/modules/resolver-core.d.ts +51 -0
- package/dist/rules/modules/resolver-core.d.ts.map +1 -0
- package/dist/rules/modules/resolver-core.js +338 -0
- package/dist/rules/modules/resolver-core.js.map +1 -0
- package/dist/rules/modules/resolver.d.ts +17 -0
- package/dist/rules/modules/resolver.d.ts.map +1 -0
- package/dist/rules/modules/resolver.js +49 -0
- package/dist/rules/modules/resolver.js.map +1 -0
- package/dist/rules/modules/stdlib/atomic.rules +52 -0
- package/dist/rules/modules/stdlib/atomic.test.json +152 -0
- package/dist/rules/modules/stdlib/auth.rules +7 -0
- package/dist/rules/modules/stdlib/auth.test.json +53 -0
- package/dist/rules/modules/stdlib/content.rules +50 -0
- package/dist/rules/modules/stdlib/content.test.json +238 -0
- package/dist/rules/modules/stdlib/counters.rules +41 -0
- package/dist/rules/modules/stdlib/counters.test.json +254 -0
- package/dist/rules/modules/stdlib/geometry.rules +43 -0
- package/dist/rules/modules/stdlib/geometry.test.json +200 -0
- package/dist/rules/modules/stdlib/joining.rules +49 -0
- package/dist/rules/modules/stdlib/joining.test.json +287 -0
- package/dist/rules/modules/stdlib/lifecycle.rules +51 -0
- package/dist/rules/modules/stdlib/lifecycle.test.json +288 -0
- package/dist/rules/modules/stdlib/lobby.rules +44 -0
- package/dist/rules/modules/stdlib/lobby.test.json +32 -0
- package/dist/rules/modules/stdlib/membership.rules +40 -0
- package/dist/rules/modules/stdlib/membership.test.json +107 -0
- package/dist/rules/modules/stdlib/spaces.rules +46 -0
- package/dist/rules/modules/stdlib/spaces.test.json +392 -0
- package/dist/rules/modules/stdlib/state.rules +28 -0
- package/dist/rules/modules/stdlib/state.test.json +55 -0
- package/dist/rules/modules/stdlib/timing.rules +20 -0
- package/dist/rules/modules/stdlib/timing.test.json +106 -0
- package/dist/rules/modules/stdlib/transitions.rules +34 -0
- package/dist/rules/modules/stdlib/transitions.test.json +86 -0
- package/dist/rules/modules/stdlib/turns.rules +27 -0
- package/dist/rules/modules/stdlib/turns.test.json +64 -0
- package/dist/rules/modules/stdlib/validation.rules +31 -0
- package/dist/rules/modules/stdlib/validation.test.json +184 -0
- package/dist/rules/modules/stdlib-content.d.ts +11 -0
- package/dist/rules/modules/stdlib-content.d.ts.map +1 -0
- package/dist/rules/modules/stdlib-content.js +590 -0
- package/dist/rules/modules/stdlib-content.js.map +1 -0
- package/dist/rules/node.d.ts +17 -0
- package/dist/rules/node.d.ts.map +1 -0
- package/dist/rules/node.js +21 -0
- package/dist/rules/node.js.map +1 -0
- package/dist/rules/rtdb.d.ts +28 -0
- package/dist/rules/rtdb.d.ts.map +1 -0
- package/dist/rules/rtdb.js +15 -0
- package/dist/rules/rtdb.js.map +1 -0
- package/dist/rules/simulator/evaluator.d.ts +179 -0
- package/dist/rules/simulator/evaluator.d.ts.map +1 -0
- package/dist/rules/simulator/evaluator.js +1221 -0
- package/dist/rules/simulator/evaluator.js.map +1 -0
- package/dist/rules/simulator/expression/eval-errors.d.ts +86 -0
- package/dist/rules/simulator/expression/eval-errors.d.ts.map +1 -0
- package/dist/rules/simulator/expression/eval-errors.js +68 -0
- package/dist/rules/simulator/expression/eval-errors.js.map +1 -0
- package/dist/rules/simulator/expression/evaluator.d.ts +43 -0
- package/dist/rules/simulator/expression/evaluator.d.ts.map +1 -0
- package/dist/rules/simulator/expression/evaluator.js +298 -0
- package/dist/rules/simulator/expression/evaluator.js.map +1 -0
- package/dist/rules/simulator/expression/lexer.d.ts +36 -0
- package/dist/rules/simulator/expression/lexer.d.ts.map +1 -0
- package/dist/rules/simulator/expression/lexer.js +318 -0
- package/dist/rules/simulator/expression/lexer.js.map +1 -0
- package/dist/rules/simulator/expression/parser.d.ts +39 -0
- package/dist/rules/simulator/expression/parser.d.ts.map +1 -0
- package/dist/rules/simulator/expression/parser.js +329 -0
- package/dist/rules/simulator/expression/parser.js.map +1 -0
- package/dist/rules/simulator/expression/types.d.ts +137 -0
- package/dist/rules/simulator/expression/types.d.ts.map +1 -0
- package/dist/rules/simulator/expression/types.js +90 -0
- package/dist/rules/simulator/expression/types.js.map +1 -0
- package/dist/rules/simulator/expression/walk-data.d.ts +52 -0
- package/dist/rules/simulator/expression/walk-data.d.ts.map +1 -0
- package/dist/rules/simulator/expression/walk-data.js +167 -0
- package/dist/rules/simulator/expression/walk-data.js.map +1 -0
- package/dist/rules/simulator/firestore-set.d.ts +25 -0
- package/dist/rules/simulator/firestore-set.d.ts.map +1 -0
- package/dist/rules/simulator/firestore-set.js +89 -0
- package/dist/rules/simulator/firestore-set.js.map +1 -0
- package/dist/rules/simulator/handler.d.ts +36 -0
- package/dist/rules/simulator/handler.d.ts.map +1 -0
- package/dist/rules/simulator/handler.js +583 -0
- package/dist/rules/simulator/handler.js.map +1 -0
- package/dist/rules/simulator/mapdiff.d.ts +29 -0
- package/dist/rules/simulator/mapdiff.d.ts.map +1 -0
- package/dist/rules/simulator/mapdiff.js +72 -0
- package/dist/rules/simulator/mapdiff.js.map +1 -0
- package/dist/rules/simulator/project-after-state.d.ts +33 -0
- package/dist/rules/simulator/project-after-state.d.ts.map +1 -0
- package/dist/rules/simulator/project-after-state.js +88 -0
- package/dist/rules/simulator/project-after-state.js.map +1 -0
- package/dist/rules/simulator/query-proof.d.ts +80 -0
- package/dist/rules/simulator/query-proof.d.ts.map +1 -0
- package/dist/rules/simulator/query-proof.js +142 -0
- package/dist/rules/simulator/query-proof.js.map +1 -0
- package/dist/rules/simulator/value-equality.d.ts +8 -0
- package/dist/rules/simulator/value-equality.d.ts.map +1 -0
- package/dist/rules/simulator/value-equality.js +40 -0
- package/dist/rules/simulator/value-equality.js.map +1 -0
- package/dist/rules/simulator/wrappers/base.d.ts +129 -0
- package/dist/rules/simulator/wrappers/base.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/base.js +90 -0
- package/dist/rules/simulator/wrappers/base.js.map +1 -0
- package/dist/rules/simulator/wrappers/bytes.d.ts +45 -0
- package/dist/rules/simulator/wrappers/bytes.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/bytes.js +130 -0
- package/dist/rules/simulator/wrappers/bytes.js.map +1 -0
- package/dist/rules/simulator/wrappers/duration.d.ts +76 -0
- package/dist/rules/simulator/wrappers/duration.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/duration.js +176 -0
- package/dist/rules/simulator/wrappers/duration.js.map +1 -0
- package/dist/rules/simulator/wrappers/float.d.ts +52 -0
- package/dist/rules/simulator/wrappers/float.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/float.js +88 -0
- package/dist/rules/simulator/wrappers/float.js.map +1 -0
- package/dist/rules/simulator/wrappers/latlng.d.ts +40 -0
- package/dist/rules/simulator/wrappers/latlng.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/latlng.js +100 -0
- package/dist/rules/simulator/wrappers/latlng.js.map +1 -0
- package/dist/rules/simulator/wrappers/path.d.ts +75 -0
- package/dist/rules/simulator/wrappers/path.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/path.js +158 -0
- package/dist/rules/simulator/wrappers/path.js.map +1 -0
- package/dist/rules/simulator/wrappers/reference.d.ts +82 -0
- package/dist/rules/simulator/wrappers/reference.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/reference.js +145 -0
- package/dist/rules/simulator/wrappers/reference.js.map +1 -0
- package/dist/rules/simulator/wrappers/timestamp.d.ts +86 -0
- package/dist/rules/simulator/wrappers/timestamp.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/timestamp.js +207 -0
- package/dist/rules/simulator/wrappers/timestamp.js.map +1 -0
- package/dist/rules/simulator/wrappers/vector.d.ts +43 -0
- package/dist/rules/simulator/wrappers/vector.d.ts.map +1 -0
- package/dist/rules/simulator/wrappers/vector.js +80 -0
- package/dist/rules/simulator/wrappers/vector.js.map +1 -0
- package/dist/rules/simulator-tools-impl.d.ts +64 -0
- package/dist/rules/simulator-tools-impl.d.ts.map +1 -0
- package/dist/rules/simulator-tools-impl.js +462 -0
- package/dist/rules/simulator-tools-impl.js.map +1 -0
- package/dist/rules/simulator.d.ts +20 -0
- package/dist/rules/simulator.d.ts.map +1 -0
- package/dist/rules/simulator.js +24 -0
- package/dist/rules/simulator.js.map +1 -0
- package/dist/rules/stdlib-modules.d.ts +73 -0
- package/dist/rules/stdlib-modules.d.ts.map +1 -0
- package/dist/rules/stdlib-modules.js +849 -0
- package/dist/rules/stdlib-modules.js.map +1 -0
- package/dist/rules/stdlib-tools.d.ts +19 -0
- package/dist/rules/stdlib-tools.d.ts.map +1 -0
- package/dist/rules/stdlib-tools.js +164 -0
- package/dist/rules/stdlib-tools.js.map +1 -0
- package/dist/rules/test/handler.d.ts +9 -0
- package/dist/rules/test/handler.d.ts.map +1 -0
- package/dist/rules/test/handler.js +124 -0
- package/dist/rules/test/handler.js.map +1 -0
- package/dist/rules/test/spec.d.ts +517 -0
- package/dist/rules/test/spec.d.ts.map +1 -0
- package/dist/rules/test/spec.js +227 -0
- package/dist/rules/test/spec.js.map +1 -0
- package/dist/rules/tools.d.ts +41 -0
- package/dist/rules/tools.d.ts.map +1 -0
- package/dist/rules/tools.js +124 -0
- package/dist/rules/tools.js.map +1 -0
- package/dist/rules/write/handler.d.ts +6 -0
- package/dist/rules/write/handler.d.ts.map +1 -0
- package/dist/rules/write/handler.js +126 -0
- package/dist/rules/write/handler.js.map +1 -0
- package/dist/rules/write/spec.d.ts +28 -0
- package/dist/rules/write/spec.d.ts.map +1 -0
- package/dist/rules/write/spec.js +2 -0
- package/dist/rules/write/spec.js.map +1 -0
- package/dist/sandbox/admin-compat.d.ts +20 -0
- package/dist/sandbox/admin-compat.d.ts.map +1 -0
- package/dist/sandbox/admin-compat.js +19 -0
- package/dist/sandbox/admin-compat.js.map +1 -0
- package/dist/sandbox/admin-firestore/error-translation.d.ts +86 -0
- package/dist/sandbox/admin-firestore/error-translation.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/error-translation.js +204 -0
- package/dist/sandbox/admin-firestore/error-translation.js.map +1 -0
- package/dist/sandbox/admin-firestore/index.d.ts +169 -0
- package/dist/sandbox/admin-firestore/index.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/index.js +456 -0
- package/dist/sandbox/admin-firestore/index.js.map +1 -0
- package/dist/sandbox/admin-firestore/remote.d.ts +97 -0
- package/dist/sandbox/admin-firestore/remote.d.ts.map +1 -0
- package/dist/sandbox/admin-firestore/remote.js +735 -0
- package/dist/sandbox/admin-firestore/remote.js.map +1 -0
- package/dist/sandbox/branches/index.d.ts +141 -0
- package/dist/sandbox/branches/index.d.ts.map +1 -0
- package/dist/sandbox/branches/index.js +327 -0
- package/dist/sandbox/branches/index.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/batch.d.ts +34 -0
- package/dist/sandbox/firestore/admin-compat/batch.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/batch.js +73 -0
- package/dist/sandbox/firestore/admin-compat/batch.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts +38 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.js +154 -0
- package/dist/sandbox/firestore/admin-compat/doc-ref.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/firestore.d.ts +45 -0
- package/dist/sandbox/firestore/admin-compat/firestore.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/firestore.js +114 -0
- package/dist/sandbox/firestore/admin-compat/firestore.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/index.d.ts +45 -0
- package/dist/sandbox/firestore/admin-compat/index.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/index.js +27 -0
- package/dist/sandbox/firestore/admin-compat/index.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/paths.d.ts +37 -0
- package/dist/sandbox/firestore/admin-compat/paths.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/paths.js +49 -0
- package/dist/sandbox/firestore/admin-compat/paths.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/query.d.ts +261 -0
- package/dist/sandbox/firestore/admin-compat/query.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/query.js +704 -0
- package/dist/sandbox/firestore/admin-compat/query.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.d.ts +8 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.js +59 -0
- package/dist/sandbox/firestore/admin-compat/read-translation.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.d.ts +39 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.js +50 -0
- package/dist/sandbox/firestore/admin-compat/snapshots.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/transaction.d.ts +35 -0
- package/dist/sandbox/firestore/admin-compat/transaction.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/transaction.js +88 -0
- package/dist/sandbox/firestore/admin-compat/transaction.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/types.d.ts +297 -0
- package/dist/sandbox/firestore/admin-compat/types.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/types.js +127 -0
- package/dist/sandbox/firestore/admin-compat/types.js.map +1 -0
- package/dist/sandbox/firestore/admin-compat/value-order.d.ts +49 -0
- package/dist/sandbox/firestore/admin-compat/value-order.d.ts.map +1 -0
- package/dist/sandbox/firestore/admin-compat/value-order.js +172 -0
- package/dist/sandbox/firestore/admin-compat/value-order.js.map +1 -0
- package/dist/sandbox/firestore/auto-id.d.ts +26 -0
- package/dist/sandbox/firestore/auto-id.d.ts.map +1 -0
- package/dist/sandbox/firestore/auto-id.js +36 -0
- package/dist/sandbox/firestore/auto-id.js.map +1 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts +48 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.js +85 -0
- package/dist/sandbox/firestore/converters/bytes-geopoint.js.map +1 -0
- package/dist/sandbox/firestore/converters/fieldvalue.d.ts +69 -0
- package/dist/sandbox/firestore/converters/fieldvalue.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/fieldvalue.js +166 -0
- package/dist/sandbox/firestore/converters/fieldvalue.js.map +1 -0
- package/dist/sandbox/firestore/converters/reference.d.ts +31 -0
- package/dist/sandbox/firestore/converters/reference.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/reference.js +51 -0
- package/dist/sandbox/firestore/converters/reference.js.map +1 -0
- package/dist/sandbox/firestore/converters/timestamp.d.ts +32 -0
- package/dist/sandbox/firestore/converters/timestamp.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/timestamp.js +63 -0
- package/dist/sandbox/firestore/converters/timestamp.js.map +1 -0
- package/dist/sandbox/firestore/converters/user-timestamp.d.ts +46 -0
- package/dist/sandbox/firestore/converters/user-timestamp.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/user-timestamp.js +65 -0
- package/dist/sandbox/firestore/converters/user-timestamp.js.map +1 -0
- package/dist/sandbox/firestore/converters/vector.d.ts +24 -0
- package/dist/sandbox/firestore/converters/vector.d.ts.map +1 -0
- package/dist/sandbox/firestore/converters/vector.js +53 -0
- package/dist/sandbox/firestore/converters/vector.js.map +1 -0
- package/dist/sandbox/firestore/errors.d.ts +82 -0
- package/dist/sandbox/firestore/errors.d.ts.map +1 -0
- package/dist/sandbox/firestore/errors.js +54 -0
- package/dist/sandbox/firestore/errors.js.map +1 -0
- package/dist/sandbox/firestore/event-log.d.ts +99 -0
- package/dist/sandbox/firestore/event-log.d.ts.map +1 -0
- package/dist/sandbox/firestore/event-log.js +67 -0
- package/dist/sandbox/firestore/event-log.js.map +1 -0
- package/dist/sandbox/firestore/field-merge.d.ts +64 -0
- package/dist/sandbox/firestore/field-merge.d.ts.map +1 -0
- package/dist/sandbox/firestore/field-merge.js +221 -0
- package/dist/sandbox/firestore/field-merge.js.map +1 -0
- package/dist/sandbox/firestore/list-query-proof.d.ts +77 -0
- package/dist/sandbox/firestore/list-query-proof.d.ts.map +1 -0
- package/dist/sandbox/firestore/list-query-proof.js +170 -0
- package/dist/sandbox/firestore/list-query-proof.js.map +1 -0
- package/dist/sandbox/firestore/local-environment.d.ts +806 -0
- package/dist/sandbox/firestore/local-environment.d.ts.map +1 -0
- package/dist/sandbox/firestore/local-environment.js +3028 -0
- package/dist/sandbox/firestore/local-environment.js.map +1 -0
- package/dist/sandbox/firestore/local-state.d.ts +196 -0
- package/dist/sandbox/firestore/local-state.d.ts.map +1 -0
- package/dist/sandbox/firestore/local-state.js +413 -0
- package/dist/sandbox/firestore/local-state.js.map +1 -0
- package/dist/sandbox/firestore/overlay-backing.d.ts +44 -0
- package/dist/sandbox/firestore/overlay-backing.d.ts.map +1 -0
- package/dist/sandbox/firestore/overlay-backing.js +82 -0
- package/dist/sandbox/firestore/overlay-backing.js.map +1 -0
- package/dist/sandbox/firestore/sentinel-capture.d.ts +30 -0
- package/dist/sandbox/firestore/sentinel-capture.d.ts.map +1 -0
- package/dist/sandbox/firestore/sentinel-capture.js +61 -0
- package/dist/sandbox/firestore/sentinel-capture.js.map +1 -0
- package/dist/sandbox/firestore/snapshot-listeners.d.ts +284 -0
- package/dist/sandbox/firestore/snapshot-listeners.d.ts.map +1 -0
- package/dist/sandbox/firestore/snapshot-listeners.js +194 -0
- package/dist/sandbox/firestore/snapshot-listeners.js.map +1 -0
- package/dist/sandbox/firestore/topk.d.ts +12 -0
- package/dist/sandbox/firestore/topk.d.ts.map +1 -0
- package/dist/sandbox/firestore/topk.js +42 -0
- package/dist/sandbox/firestore/topk.js.map +1 -0
- package/dist/sandbox/firestore/transaction-merge.d.ts +55 -0
- package/dist/sandbox/firestore/transaction-merge.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction-merge.js +107 -0
- package/dist/sandbox/firestore/transaction-merge.js.map +1 -0
- package/dist/sandbox/firestore/transaction-types.d.ts +152 -0
- package/dist/sandbox/firestore/transaction-types.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction-types.js +10 -0
- package/dist/sandbox/firestore/transaction-types.js.map +1 -0
- package/dist/sandbox/firestore/transaction.d.ts +79 -0
- package/dist/sandbox/firestore/transaction.d.ts.map +1 -0
- package/dist/sandbox/firestore/transaction.js +95 -0
- package/dist/sandbox/firestore/transaction.js.map +1 -0
- package/dist/sandbox/firestore/value-equality.d.ts +2 -0
- package/dist/sandbox/firestore/value-equality.d.ts.map +1 -0
- package/dist/sandbox/firestore/value-equality.js +35 -0
- package/dist/sandbox/firestore/value-equality.js.map +1 -0
- package/dist/sandbox/firestore/value-resolver.d.ts +172 -0
- package/dist/sandbox/firestore/value-resolver.d.ts.map +1 -0
- package/dist/sandbox/firestore/value-resolver.js +221 -0
- package/dist/sandbox/firestore/value-resolver.js.map +1 -0
- package/dist/sandbox/firestore/wire-encoder.d.ts +14 -0
- package/dist/sandbox/firestore/wire-encoder.d.ts.map +1 -0
- package/dist/sandbox/firestore/wire-encoder.js +130 -0
- package/dist/sandbox/firestore/wire-encoder.js.map +1 -0
- package/dist/sandbox/index.d.ts +50 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +63 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/internal/index.d.ts +36 -0
- package/dist/sandbox/internal/index.d.ts.map +1 -0
- package/dist/sandbox/internal/index.js +72 -0
- package/dist/sandbox/internal/index.js.map +1 -0
- package/dist/sandbox/internal/sandbox-impl.d.ts +343 -0
- package/dist/sandbox/internal/sandbox-impl.d.ts.map +1 -0
- package/dist/sandbox/internal/sandbox-impl.js +718 -0
- package/dist/sandbox/internal/sandbox-impl.js.map +1 -0
- package/dist/sandbox/persistence/backends.d.ts +37 -0
- package/dist/sandbox/persistence/backends.d.ts.map +1 -0
- package/dist/sandbox/persistence/backends.js +202 -0
- package/dist/sandbox/persistence/backends.js.map +1 -0
- package/dist/sandbox/persistence/chunk-format.d.ts +58 -0
- package/dist/sandbox/persistence/chunk-format.d.ts.map +1 -0
- package/dist/sandbox/persistence/chunk-format.js +163 -0
- package/dist/sandbox/persistence/chunk-format.js.map +1 -0
- package/dist/sandbox/persistence/controller.d.ts +39 -0
- package/dist/sandbox/persistence/controller.d.ts.map +1 -0
- package/dist/sandbox/persistence/controller.js +605 -0
- package/dist/sandbox/persistence/controller.js.map +1 -0
- package/dist/sandbox/persistence/index.d.ts +13 -0
- package/dist/sandbox/persistence/index.d.ts.map +1 -0
- package/dist/sandbox/persistence/index.js +5 -0
- package/dist/sandbox/persistence/index.js.map +1 -0
- package/dist/sandbox/persistence/serialize.d.ts +53 -0
- package/dist/sandbox/persistence/serialize.d.ts.map +1 -0
- package/dist/sandbox/persistence/serialize.js +98 -0
- package/dist/sandbox/persistence/serialize.js.map +1 -0
- package/dist/sandbox/persistence/types.d.ts +118 -0
- package/dist/sandbox/persistence/types.d.ts.map +1 -0
- package/dist/sandbox/persistence/types.js +15 -0
- package/dist/sandbox/persistence/types.js.map +1 -0
- package/dist/sandbox/remote.d.ts +118 -0
- package/dist/sandbox/remote.d.ts.map +1 -0
- package/dist/sandbox/remote.js +60 -0
- package/dist/sandbox/remote.js.map +1 -0
- package/dist/sandbox/replay/index.d.ts +106 -0
- package/dist/sandbox/replay/index.d.ts.map +1 -0
- package/dist/sandbox/replay/index.js +246 -0
- package/dist/sandbox/replay/index.js.map +1 -0
- package/dist/sandbox/sandbox-context.d.ts +40 -0
- package/dist/sandbox/sandbox-context.d.ts.map +1 -0
- package/dist/sandbox/sandbox-context.js +72 -0
- package/dist/sandbox/sandbox-context.js.map +1 -0
- package/dist/sandbox/tab-sync/index.d.ts +120 -0
- package/dist/sandbox/tab-sync/index.d.ts.map +1 -0
- package/dist/sandbox/tab-sync/index.js +272 -0
- package/dist/sandbox/tab-sync/index.js.map +1 -0
- package/dist/sandbox/types.d.ts +1235 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +46 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/storage/admin/api.d.ts +206 -0
- package/dist/storage/admin/api.d.ts.map +1 -0
- package/dist/storage/admin/api.js +349 -0
- package/dist/storage/admin/api.js.map +1 -0
- package/dist/storage/admin/handler.d.ts +29 -0
- package/dist/storage/admin/handler.d.ts.map +1 -0
- package/dist/storage/admin/handler.js +82 -0
- package/dist/storage/admin/handler.js.map +1 -0
- package/dist/storage/admin/spec.d.ts +64 -0
- package/dist/storage/admin/spec.d.ts.map +1 -0
- package/dist/storage/admin/spec.js +8 -0
- package/dist/storage/admin/spec.js.map +1 -0
- package/dist/storage/admin/tools.d.ts +23 -0
- package/dist/storage/admin/tools.d.ts.map +1 -0
- package/dist/storage/admin/tools.js +90 -0
- package/dist/storage/admin/tools.js.map +1 -0
- package/dist/storage/download.d.ts +27 -0
- package/dist/storage/download.d.ts.map +1 -0
- package/dist/storage/download.js +136 -0
- package/dist/storage/download.js.map +1 -0
- package/dist/storage/enforce.d.ts +26 -0
- package/dist/storage/enforce.d.ts.map +1 -0
- package/dist/storage/enforce.js +16 -0
- package/dist/storage/enforce.js.map +1 -0
- package/dist/storage/errors.d.ts +46 -0
- package/dist/storage/errors.d.ts.map +1 -0
- package/dist/storage/errors.js +62 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/index.d.ts +49 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +60 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/internal.d.ts +18 -0
- package/dist/storage/internal.d.ts.map +1 -0
- package/dist/storage/internal.js +18 -0
- package/dist/storage/internal.js.map +1 -0
- package/dist/storage/list.d.ts +17 -0
- package/dist/storage/list.d.ts.map +1 -0
- package/dist/storage/list.js +91 -0
- package/dist/storage/list.js.map +1 -0
- package/dist/storage/metadata.d.ts +86 -0
- package/dist/storage/metadata.d.ts.map +1 -0
- package/dist/storage/metadata.js +167 -0
- package/dist/storage/metadata.js.map +1 -0
- package/dist/storage/persistence.d.ts +134 -0
- package/dist/storage/persistence.d.ts.map +1 -0
- package/dist/storage/persistence.js +132 -0
- package/dist/storage/persistence.js.map +1 -0
- package/dist/storage/reference.d.ts +63 -0
- package/dist/storage/reference.d.ts.map +1 -0
- package/dist/storage/reference.js +156 -0
- package/dist/storage/reference.js.map +1 -0
- package/dist/storage/rules.d.ts +176 -0
- package/dist/storage/rules.d.ts.map +1 -0
- package/dist/storage/rules.js +621 -0
- package/dist/storage/rules.js.map +1 -0
- package/dist/storage/service.d.ts +139 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +241 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/upload.d.ts +43 -0
- package/dist/storage/upload.d.ts.map +1 -0
- package/dist/storage/upload.js +235 -0
- package/dist/storage/upload.js.map +1 -0
- package/package.json +109 -3
|
@@ -0,0 +1,1467 @@
|
|
|
1
|
+
import { emitSandboxEvent, makeSandboxCommitEvent, makeSandboxListenerEvent, makeSandboxOperationEvent, makeServiceMutationEvent, } from 'pyric/sandbox/internal';
|
|
2
|
+
import { DataTree, cloneJson, jsonValuesEqual, joinPath, pathSegments, } from './data-tree.js';
|
|
3
|
+
import { generatePushId } from './push-id.js';
|
|
4
|
+
import { resolveSentinels } from './sentinels.js';
|
|
5
|
+
import { RulesEvaluator, permissionDenied, } from './rules-eval.js';
|
|
6
|
+
import { executeQuery } from './query.js';
|
|
7
|
+
import { normalizeWrite, coerceArrays } from './normalize.js';
|
|
8
|
+
/**
|
|
9
|
+
* RtdbBackend — the per-`Sandbox` data plane.
|
|
10
|
+
*/
|
|
11
|
+
export class RtdbBackend {
|
|
12
|
+
tree = new DataTree();
|
|
13
|
+
rules = new RulesEvaluator();
|
|
14
|
+
valueListeners = new Set();
|
|
15
|
+
childListeners = new Set();
|
|
16
|
+
nextId = 0;
|
|
17
|
+
/**
|
|
18
|
+
* Persistence change-subscribers. The sandbox persistence controller
|
|
19
|
+
* registers one of these (via {@link RtdbBackend.subscribeWrites}) so any
|
|
20
|
+
* tree mutation schedules a debounced flush. RTDB writes emit
|
|
21
|
+
* `kind: 'service_mutation'` events, which are NOT in the controller's
|
|
22
|
+
* `isPersistableEvent` set (`write`/`session_boundary`) — so the sandbox
|
|
23
|
+
* event stream never triggers an RTDB flush. This subscriber channel is
|
|
24
|
+
* the sole flush trigger, mirroring how auth drives its own flushes.
|
|
25
|
+
*/
|
|
26
|
+
writeSubscribers = new Set();
|
|
27
|
+
/**
|
|
28
|
+
* The owning `Sandbox`, when this backend was created through the
|
|
29
|
+
* modular surface's `getDatabase(sandbox)` / `getDatabase(ctx)` path.
|
|
30
|
+
* Held only to emit RTDB write activity onto the unified Studio
|
|
31
|
+
* `onEvent`/`history()` stream (keystone, track T1). Optional because
|
|
32
|
+
* the backend is also constructed bare in unit tests that exercise the
|
|
33
|
+
* data plane directly; in that case emission is simply skipped.
|
|
34
|
+
*/
|
|
35
|
+
sandbox;
|
|
36
|
+
constructor(sandbox) {
|
|
37
|
+
this.sandbox = sandbox;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Emit an RTDB {@link ServiceMutationEvent} onto the sandbox's unified
|
|
41
|
+
* stream. No-op when this backend has no owning sandbox (bare-test
|
|
42
|
+
* construction). Best-effort + isolated — a throw must not fail the
|
|
43
|
+
* write that triggered it.
|
|
44
|
+
*/
|
|
45
|
+
emitRtdbEvent(auth, op, path, fields = {}) {
|
|
46
|
+
if (!this.sandbox)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
emitSandboxEvent(this.sandbox, makeServiceMutationEvent({
|
|
50
|
+
service: 'rtdb',
|
|
51
|
+
op,
|
|
52
|
+
path,
|
|
53
|
+
auth,
|
|
54
|
+
before: fields.before,
|
|
55
|
+
after: fields.after,
|
|
56
|
+
detail: fields.detail,
|
|
57
|
+
}), { service: 'rtdb' });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Observational — never let event emission break an RTDB write.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
nextListenerId() {
|
|
64
|
+
this.nextId += 1;
|
|
65
|
+
return `rtdb-listener-${this.nextId.toString(36)}`;
|
|
66
|
+
}
|
|
67
|
+
nextGroupId(prefix) {
|
|
68
|
+
this.nextId += 1;
|
|
69
|
+
return `rtdb-${prefix}-${this.nextId.toString(36)}`;
|
|
70
|
+
}
|
|
71
|
+
emitOperation(auth, method, path, result, evaluation, fields = {}) {
|
|
72
|
+
if (!this.sandbox)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
emitSandboxEvent(this.sandbox, makeSandboxOperationEvent({
|
|
76
|
+
service: 'rtdb',
|
|
77
|
+
method,
|
|
78
|
+
path: canonicalPath(path),
|
|
79
|
+
auth,
|
|
80
|
+
result,
|
|
81
|
+
origin: fields.origin ?? 'user',
|
|
82
|
+
durationMs: fields.durationMs,
|
|
83
|
+
reasons: evaluation?.reasons,
|
|
84
|
+
rules: evaluation
|
|
85
|
+
? {
|
|
86
|
+
engine: 'rtdb',
|
|
87
|
+
matchedPath: evaluation.matchedPath,
|
|
88
|
+
matchedRule: evaluation.matchedRule,
|
|
89
|
+
pathVariableBindings: evaluation.pathVariableBindings,
|
|
90
|
+
reason: evaluation.reason,
|
|
91
|
+
errorCode: evaluation.errorCode,
|
|
92
|
+
}
|
|
93
|
+
: undefined,
|
|
94
|
+
request: fields.request,
|
|
95
|
+
resourceBefore: fields.resourceBefore,
|
|
96
|
+
resourceAfter: fields.resourceAfter,
|
|
97
|
+
groupId: fields.groupId,
|
|
98
|
+
groupKind: fields.groupKind,
|
|
99
|
+
triggeredBy: fields.triggeredBy,
|
|
100
|
+
detail: fields.detail,
|
|
101
|
+
at: fields.at,
|
|
102
|
+
}), { service: 'rtdb' });
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Observational — never let telemetry break RTDB semantics.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
emitCommit(auth, method, path, fields = {}) {
|
|
109
|
+
if (!this.sandbox)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
emitSandboxEvent(this.sandbox, makeSandboxCommitEvent({
|
|
113
|
+
service: 'rtdb',
|
|
114
|
+
method,
|
|
115
|
+
path: canonicalPath(path),
|
|
116
|
+
auth,
|
|
117
|
+
data: fields.data,
|
|
118
|
+
priorState: fields.priorState,
|
|
119
|
+
nextState: fields.nextState,
|
|
120
|
+
groupId: fields.groupId,
|
|
121
|
+
groupKind: fields.groupKind,
|
|
122
|
+
replay: fields.replay,
|
|
123
|
+
detail: fields.detail,
|
|
124
|
+
}), { service: 'rtdb' });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Observational — never let telemetry break RTDB semantics.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
emitListener(phase, listener, auth, fields = {}) {
|
|
131
|
+
if (!this.sandbox)
|
|
132
|
+
return;
|
|
133
|
+
try {
|
|
134
|
+
emitSandboxEvent(this.sandbox, makeSandboxListenerEvent({
|
|
135
|
+
service: 'rtdb',
|
|
136
|
+
phase,
|
|
137
|
+
listenerId: listener.id,
|
|
138
|
+
target: {
|
|
139
|
+
kind: fields.event ?? 'value',
|
|
140
|
+
path: canonicalPath(listener.path),
|
|
141
|
+
},
|
|
142
|
+
auth,
|
|
143
|
+
result: fields.result,
|
|
144
|
+
size: fields.size,
|
|
145
|
+
sample: fields.sample,
|
|
146
|
+
reason: fields.reason,
|
|
147
|
+
error: fields.error,
|
|
148
|
+
triggeredBy: fields.triggeredBy,
|
|
149
|
+
detail: fields.detail,
|
|
150
|
+
}), { service: 'rtdb' });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Observational — never let telemetry break RTDB semantics.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ─── Admin-plane (rule-bypass) operations ───────────────────────────
|
|
157
|
+
//
|
|
158
|
+
// Used by the `sandbox.setData` / `sandbox.snapshotState` test
|
|
159
|
+
// helpers exported on the modular SDK. Don't go through the rule
|
|
160
|
+
// engine.
|
|
161
|
+
setData(seed) {
|
|
162
|
+
this.tree.restore({});
|
|
163
|
+
// Seed each path individually so the tree's prior-trim semantics
|
|
164
|
+
// apply consistently. For a flat seed of nested data we'd just
|
|
165
|
+
// restore(); but per-path seeding lets `setData({'/a/b': 1})` work
|
|
166
|
+
// when the user supplies dotted-out paths.
|
|
167
|
+
for (const [path, value] of Object.entries(seed)) {
|
|
168
|
+
// Resolve sentinels + normalize in seed too — keeps the seed API
|
|
169
|
+
// symmetric with the user-plane writes (array coercion, pruning).
|
|
170
|
+
const resolved = normalizeWrite(resolveSentinels(value, Date.now()), path === '/' ? '' : path);
|
|
171
|
+
this.tree.write(path, resolved);
|
|
172
|
+
}
|
|
173
|
+
this.notifyWrite();
|
|
174
|
+
}
|
|
175
|
+
setRules(rulesJson) {
|
|
176
|
+
this.rules.setRules(rulesJson);
|
|
177
|
+
}
|
|
178
|
+
snapshotState() {
|
|
179
|
+
return this.tree.snapshot();
|
|
180
|
+
}
|
|
181
|
+
adminGet(path) {
|
|
182
|
+
const value = this.tree.read(path);
|
|
183
|
+
this.emitOperation(null, 'get', path, 'not-applicable', undefined, {
|
|
184
|
+
origin: 'admin',
|
|
185
|
+
resourceBefore: { data: value, exists: value !== null },
|
|
186
|
+
});
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
adminGetQuery(path, spec) {
|
|
190
|
+
const rows = executeQuery(this.tree.read(path), spec);
|
|
191
|
+
this.emitOperation(null, 'get', path, 'not-applicable', undefined, {
|
|
192
|
+
origin: 'admin',
|
|
193
|
+
request: { query: spec },
|
|
194
|
+
resourceBefore: { data: rowsToVal(rows), exists: rows.length > 0 },
|
|
195
|
+
});
|
|
196
|
+
return rows;
|
|
197
|
+
}
|
|
198
|
+
adminSet(path, value) {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
const before = this.tree.read(path);
|
|
201
|
+
const resolved = normalizeWrite(resolveSentinels(value, now, before), path === '/' ? '' : path);
|
|
202
|
+
this.emitOperation(null, resolved === null ? 'remove' : 'set', path, 'not-applicable', undefined, {
|
|
203
|
+
origin: 'admin',
|
|
204
|
+
request: { data: value, resourceData: value },
|
|
205
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
206
|
+
resourceAfter: { data: resolved, exists: resolved !== null },
|
|
207
|
+
});
|
|
208
|
+
const priors = this.snapshotChildListenerParents();
|
|
209
|
+
this.tree.write(path, resolved);
|
|
210
|
+
this.fanOut([path]);
|
|
211
|
+
this.fanOutChildren(priors);
|
|
212
|
+
const after = this.tree.read(path);
|
|
213
|
+
const method = after === null ? 'remove' : 'set';
|
|
214
|
+
this.emitCommit(null, method, path, {
|
|
215
|
+
data: value,
|
|
216
|
+
priorState: before,
|
|
217
|
+
nextState: after,
|
|
218
|
+
replay: { requestTime: now },
|
|
219
|
+
detail: { admin: true },
|
|
220
|
+
});
|
|
221
|
+
this.notifyWrite();
|
|
222
|
+
}
|
|
223
|
+
adminUpdate(path, patch) {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const before = this.tree.read(path);
|
|
226
|
+
const isMultiPath = Object.keys(patch).some((k) => k.includes('/'));
|
|
227
|
+
const priors = this.snapshotChildListenerParents();
|
|
228
|
+
if (isMultiPath) {
|
|
229
|
+
const expanded = {};
|
|
230
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
231
|
+
const absPath = joinPath([...pathSegments(path), ...pathSegments(k)]);
|
|
232
|
+
expanded[absPath] = normalizeWrite(resolveSentinels(v, now, this.tree.read(absPath)), absPath);
|
|
233
|
+
}
|
|
234
|
+
this.emitOperation(null, 'update', path, 'not-applicable', undefined, {
|
|
235
|
+
origin: 'admin',
|
|
236
|
+
request: { data: patch, resourceData: patch },
|
|
237
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
238
|
+
detail: { admin: true, multiPath: true, paths: Object.keys(expanded) },
|
|
239
|
+
});
|
|
240
|
+
this.tree.multiUpdate(expanded);
|
|
241
|
+
this.fanOut(Object.keys(expanded));
|
|
242
|
+
this.fanOutChildren(priors);
|
|
243
|
+
const after = this.tree.read(path);
|
|
244
|
+
this.emitCommit(null, 'update', path, {
|
|
245
|
+
data: patch,
|
|
246
|
+
priorState: before,
|
|
247
|
+
nextState: after,
|
|
248
|
+
replay: { requestTime: now },
|
|
249
|
+
detail: { admin: true, multiPath: true, paths: Object.keys(expanded) },
|
|
250
|
+
});
|
|
251
|
+
this.notifyWrite();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const resolvedPatch = {};
|
|
255
|
+
const touched = [];
|
|
256
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
257
|
+
const sub = joinPath([...pathSegments(path), ...pathSegments(k)]);
|
|
258
|
+
touched.push(sub);
|
|
259
|
+
resolvedPatch[k] = normalizeWrite(resolveSentinels(v, now, this.tree.read(sub)), sub);
|
|
260
|
+
}
|
|
261
|
+
this.emitOperation(null, 'update', path, 'not-applicable', undefined, {
|
|
262
|
+
origin: 'admin',
|
|
263
|
+
request: { data: patch, resourceData: patch },
|
|
264
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
265
|
+
detail: { admin: true, multiPath: false, keys: Object.keys(resolvedPatch) },
|
|
266
|
+
});
|
|
267
|
+
this.tree.shallowUpdate(path, resolvedPatch);
|
|
268
|
+
this.fanOut(touched);
|
|
269
|
+
this.fanOutChildren(priors);
|
|
270
|
+
const after = this.tree.read(path);
|
|
271
|
+
this.emitCommit(null, 'update', path, {
|
|
272
|
+
data: patch,
|
|
273
|
+
priorState: before,
|
|
274
|
+
nextState: after,
|
|
275
|
+
replay: { requestTime: now },
|
|
276
|
+
detail: { admin: true, multiPath: false, keys: Object.keys(resolvedPatch) },
|
|
277
|
+
});
|
|
278
|
+
this.notifyWrite();
|
|
279
|
+
}
|
|
280
|
+
adminRemove(path) {
|
|
281
|
+
this.adminSet(path, null);
|
|
282
|
+
}
|
|
283
|
+
// ─── User-plane (rule-gated) operations ────────────────────────────
|
|
284
|
+
get(auth, path) {
|
|
285
|
+
const at = Date.now();
|
|
286
|
+
const before = this.tree.read(path);
|
|
287
|
+
const evaluation = this.rules.evaluate('read', path === '/' ? '/' : path, {
|
|
288
|
+
auth,
|
|
289
|
+
mockData: this.tree.snapshot(),
|
|
290
|
+
});
|
|
291
|
+
if (evaluation.check !== 'allow') {
|
|
292
|
+
this.emitOperation(auth, 'get', path, 'deny', evaluation, {
|
|
293
|
+
at,
|
|
294
|
+
durationMs: Date.now() - at,
|
|
295
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
296
|
+
});
|
|
297
|
+
throw permissionDenied();
|
|
298
|
+
}
|
|
299
|
+
this.emitOperation(auth, 'get', path, 'allow', evaluation, {
|
|
300
|
+
at,
|
|
301
|
+
durationMs: Date.now() - at,
|
|
302
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
303
|
+
});
|
|
304
|
+
// Return the stored (integer-keyed) shape. Array coercion (DB-B2) is
|
|
305
|
+
// a `DataSnapshot.val()`-render concern applied by the snapshot
|
|
306
|
+
// wrapper — structural ops (`forEach`/`child`/`size`) walk the node.
|
|
307
|
+
return this.tree.read(path);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* `set(path, value)` — full overwrite at the path. `null` deletes the
|
|
311
|
+
* subtree (sandbox match for the RTDB invariant
|
|
312
|
+
* `remove(ref) === set(ref, null)`).
|
|
313
|
+
*/
|
|
314
|
+
set(auth, path, value) {
|
|
315
|
+
this.setInternal(auth, path, value, 'set');
|
|
316
|
+
}
|
|
317
|
+
/** Shared `set`/`remove` write path. `op` selects the emitted event
|
|
318
|
+
* label so a `remove` (which is `set(_, null)`) surfaces as `remove`
|
|
319
|
+
* rather than `set` on the Studio stream. */
|
|
320
|
+
setInternal(auth, path, value, op) {
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
// Pass the current value at the path so `increment()` sentinels
|
|
323
|
+
// resolve against the field's prior value (DB-GAP).
|
|
324
|
+
const resolved0 = resolveSentinels(value, now, this.tree.read(path));
|
|
325
|
+
// Write-boundary normalization (DB-B1/B2/B3): validate keys + reject
|
|
326
|
+
// `undefined`/non-finite, coerce arrays → integer-keyed objects, prune
|
|
327
|
+
// null/empty subtrees. An object that prunes to nothing is a delete
|
|
328
|
+
// (`set(ref, {})` === `remove(ref)`).
|
|
329
|
+
const resolved = normalizeWrite(resolved0, path === '/' ? '' : path);
|
|
330
|
+
const before = this.tree.read(path);
|
|
331
|
+
const at = Date.now();
|
|
332
|
+
const evaluation = this.rules.evaluate('write', path === '/' ? '/' : path, {
|
|
333
|
+
auth,
|
|
334
|
+
mockData: this.tree.snapshot(),
|
|
335
|
+
newData: resolved,
|
|
336
|
+
});
|
|
337
|
+
const resourceBefore = { data: before, exists: before !== null };
|
|
338
|
+
const resourceAfter = { data: resolved, exists: resolved !== null };
|
|
339
|
+
if (evaluation.check !== 'allow') {
|
|
340
|
+
this.emitOperation(auth, op, path, 'deny', evaluation, {
|
|
341
|
+
at,
|
|
342
|
+
durationMs: Date.now() - at,
|
|
343
|
+
request: { data: value, resourceData: value },
|
|
344
|
+
resourceBefore,
|
|
345
|
+
resourceAfter,
|
|
346
|
+
});
|
|
347
|
+
throw permissionDenied();
|
|
348
|
+
}
|
|
349
|
+
this.emitOperation(auth, op, path, 'allow', evaluation, {
|
|
350
|
+
at,
|
|
351
|
+
durationMs: Date.now() - at,
|
|
352
|
+
request: { data: value, resourceData: value },
|
|
353
|
+
resourceBefore,
|
|
354
|
+
resourceAfter,
|
|
355
|
+
});
|
|
356
|
+
const priors = this.snapshotChildListenerParents();
|
|
357
|
+
this.tree.write(path, resolved);
|
|
358
|
+
this.fanOut([path]);
|
|
359
|
+
this.fanOutChildren(priors);
|
|
360
|
+
// A write that pruned to nothing (`set(ref, null)` / `set(ref, {})`)
|
|
361
|
+
// is semantically a remove; label it as such even if it arrived via
|
|
362
|
+
// `set`. `after` is the post-write value at the path (`null` when the
|
|
363
|
+
// subtree was deleted).
|
|
364
|
+
const after = this.tree.read(path);
|
|
365
|
+
const effectiveOp = after === null ? 'remove' : op;
|
|
366
|
+
this.emitCommit(auth, effectiveOp, path, {
|
|
367
|
+
data: value,
|
|
368
|
+
priorState: before,
|
|
369
|
+
nextState: after,
|
|
370
|
+
replay: { requestTime: now },
|
|
371
|
+
});
|
|
372
|
+
this.emitRtdbEvent(auth, effectiveOp, canonicalPath(path), { before, after });
|
|
373
|
+
this.notifyWrite();
|
|
374
|
+
}
|
|
375
|
+
remove(auth, path) {
|
|
376
|
+
this.setInternal(auth, path, null, 'remove');
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* `update(path, patch)` — multi-path atomic update when keys are
|
|
380
|
+
* `/`-prefixed and `path === '/'`; otherwise a shallow merge at the
|
|
381
|
+
* named path. Matches `firebase/database`'s discrimination: an
|
|
382
|
+
* `update(rootRef, { '/users/a/x': 1, '/users/b/y': 2 })` fans out
|
|
383
|
+
* to both paths atomically, while `update(usersRef, { name: 'A' })`
|
|
384
|
+
* shallow-merges into `/users`.
|
|
385
|
+
*/
|
|
386
|
+
update(auth, path, patch) {
|
|
387
|
+
const now = Date.now();
|
|
388
|
+
const isMultiPath = Object.keys(patch).some((k) => k.includes('/'));
|
|
389
|
+
if (isMultiPath) {
|
|
390
|
+
const groupId = this.nextGroupId('update');
|
|
391
|
+
// Each key is treated as a path relative to `path`. Resolve
|
|
392
|
+
// sentinels per-leaf so a key whose value contains a
|
|
393
|
+
// `serverTimestamp()` lands as a number.
|
|
394
|
+
const expanded = {};
|
|
395
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
396
|
+
const absSegs = [...pathSegments(path), ...pathSegments(k)];
|
|
397
|
+
const absPath = joinPath(absSegs);
|
|
398
|
+
expanded[absPath] = normalizeWrite(resolveSentinels(v, now, this.tree.read(absPath)), absPath);
|
|
399
|
+
}
|
|
400
|
+
// Check every path under the rules engine. ANY denial fails the
|
|
401
|
+
// entire update — the multi-path atomicity contract.
|
|
402
|
+
const mock = this.tree.snapshot();
|
|
403
|
+
for (const [absPath, value] of Object.entries(expanded)) {
|
|
404
|
+
const at = Date.now();
|
|
405
|
+
const before = this.tree.read(absPath);
|
|
406
|
+
const evaluation = this.rules.evaluate('write', absPath, {
|
|
407
|
+
auth,
|
|
408
|
+
mockData: mock,
|
|
409
|
+
newData: value,
|
|
410
|
+
});
|
|
411
|
+
const common = {
|
|
412
|
+
at,
|
|
413
|
+
durationMs: Date.now() - at,
|
|
414
|
+
origin: 'batch',
|
|
415
|
+
request: { data: value, resourceData: value },
|
|
416
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
417
|
+
resourceAfter: { data: value, exists: value !== null },
|
|
418
|
+
groupId,
|
|
419
|
+
groupKind: 'batch',
|
|
420
|
+
detail: { multiPath: true, rootPath: canonicalPath(path) },
|
|
421
|
+
};
|
|
422
|
+
if (evaluation.check !== 'allow') {
|
|
423
|
+
this.emitOperation(auth, 'update', absPath, 'deny', evaluation, common);
|
|
424
|
+
throw permissionDenied();
|
|
425
|
+
}
|
|
426
|
+
this.emitOperation(auth, 'update', absPath, 'allow', evaluation, common);
|
|
427
|
+
}
|
|
428
|
+
// Apply atomically via the tree's overlap-checked multi-write.
|
|
429
|
+
const beforeRoot = this.tree.read(path);
|
|
430
|
+
const priors = this.snapshotChildListenerParents();
|
|
431
|
+
this.tree.multiUpdate(expanded);
|
|
432
|
+
this.fanOut(Object.keys(expanded));
|
|
433
|
+
this.fanOutChildren(priors);
|
|
434
|
+
const afterRoot = this.tree.read(path);
|
|
435
|
+
this.emitCommit(auth, 'update', path, {
|
|
436
|
+
data: patch,
|
|
437
|
+
priorState: beforeRoot,
|
|
438
|
+
nextState: afterRoot,
|
|
439
|
+
groupId,
|
|
440
|
+
groupKind: 'batch',
|
|
441
|
+
replay: { requestTime: now },
|
|
442
|
+
detail: { multiPath: true, paths: Object.keys(expanded) },
|
|
443
|
+
});
|
|
444
|
+
this.emitRtdbEvent(auth, 'update', canonicalPath(path), {
|
|
445
|
+
after: expanded,
|
|
446
|
+
detail: { multiPath: true, paths: Object.keys(expanded) },
|
|
447
|
+
});
|
|
448
|
+
this.notifyWrite();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// Shallow merge mode. Each top-level key of `patch` replaces the
|
|
452
|
+
// key at `<path>/<key>`. `null` deletes.
|
|
453
|
+
const resolvedPatch = {};
|
|
454
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
455
|
+
const sub = joinPath([...pathSegments(path), ...pathSegments(k)]);
|
|
456
|
+
resolvedPatch[k] = normalizeWrite(resolveSentinels(v, now, this.tree.read(sub)), sub);
|
|
457
|
+
}
|
|
458
|
+
const mock = this.tree.snapshot();
|
|
459
|
+
const touched = [];
|
|
460
|
+
const groupId = this.nextGroupId('update');
|
|
461
|
+
for (const [k, v] of Object.entries(resolvedPatch)) {
|
|
462
|
+
const sub = joinPath([...pathSegments(path), ...pathSegments(k)]);
|
|
463
|
+
touched.push(sub);
|
|
464
|
+
const at = Date.now();
|
|
465
|
+
const before = this.tree.read(sub);
|
|
466
|
+
const evaluation = this.rules.evaluate('write', sub, {
|
|
467
|
+
auth,
|
|
468
|
+
mockData: mock,
|
|
469
|
+
newData: v,
|
|
470
|
+
});
|
|
471
|
+
const common = {
|
|
472
|
+
at,
|
|
473
|
+
durationMs: Date.now() - at,
|
|
474
|
+
origin: 'batch',
|
|
475
|
+
request: { data: v, resourceData: v },
|
|
476
|
+
resourceBefore: { data: before, exists: before !== null },
|
|
477
|
+
resourceAfter: { data: v, exists: v !== null },
|
|
478
|
+
groupId,
|
|
479
|
+
groupKind: 'batch',
|
|
480
|
+
detail: { multiPath: false, rootPath: canonicalPath(path), key: k },
|
|
481
|
+
};
|
|
482
|
+
if (evaluation.check !== 'allow') {
|
|
483
|
+
this.emitOperation(auth, 'update', sub, 'deny', evaluation, common);
|
|
484
|
+
throw permissionDenied();
|
|
485
|
+
}
|
|
486
|
+
this.emitOperation(auth, 'update', sub, 'allow', evaluation, common);
|
|
487
|
+
}
|
|
488
|
+
const beforeRoot = this.tree.read(path);
|
|
489
|
+
const priors = this.snapshotChildListenerParents();
|
|
490
|
+
this.tree.shallowUpdate(path, resolvedPatch);
|
|
491
|
+
this.fanOut(touched);
|
|
492
|
+
this.fanOutChildren(priors);
|
|
493
|
+
const afterRoot = this.tree.read(path);
|
|
494
|
+
this.emitCommit(auth, 'update', path, {
|
|
495
|
+
data: patch,
|
|
496
|
+
priorState: beforeRoot,
|
|
497
|
+
nextState: afterRoot,
|
|
498
|
+
groupId,
|
|
499
|
+
groupKind: 'batch',
|
|
500
|
+
replay: { requestTime: now },
|
|
501
|
+
detail: { multiPath: false, keys: Object.keys(resolvedPatch) },
|
|
502
|
+
});
|
|
503
|
+
this.emitRtdbEvent(auth, 'update', canonicalPath(path), {
|
|
504
|
+
after: resolvedPatch,
|
|
505
|
+
detail: { multiPath: false, keys: Object.keys(resolvedPatch) },
|
|
506
|
+
});
|
|
507
|
+
this.notifyWrite();
|
|
508
|
+
}
|
|
509
|
+
// Note: `push()` is implemented entirely on the modular surface
|
|
510
|
+
// (`modular.ts`) — the key is minted client-side via {@link mintKey}
|
|
511
|
+
// (no rule check) so it's available synchronously even under a denying
|
|
512
|
+
// rule (DB-B7), and the optional value write is deferred onto the
|
|
513
|
+
// returned ThenableReference's promise via the normal `set` path.
|
|
514
|
+
// ─── Listeners ─────────────────────────────────────────────────────
|
|
515
|
+
/**
|
|
516
|
+
* Subscribe to `onValue`. Fires immediately with the current value
|
|
517
|
+
* (or `{ val: null, exists: false }` for an absent path — locked by
|
|
518
|
+
* the prod SDK's behavior), then on every write that touches `path`
|
|
519
|
+
* or any descendant.
|
|
520
|
+
*
|
|
521
|
+
* Returns an unsubscribe.
|
|
522
|
+
*/
|
|
523
|
+
onValue(auth, path, cb, query) {
|
|
524
|
+
// Rules check at subscribe time. A denied subscribe never gets
|
|
525
|
+
// the initial fire — matches production where the listener errors
|
|
526
|
+
// out before any callback.
|
|
527
|
+
const listenerId = this.nextListenerId();
|
|
528
|
+
const at = Date.now();
|
|
529
|
+
const evaluation = this.rules.evaluate('read', path === '/' ? '/' : path, {
|
|
530
|
+
auth,
|
|
531
|
+
mockData: this.tree.snapshot(),
|
|
532
|
+
});
|
|
533
|
+
if (evaluation.check !== 'allow') {
|
|
534
|
+
this.emitOperation(auth, 'listen', path, 'deny', evaluation, {
|
|
535
|
+
at,
|
|
536
|
+
durationMs: Date.now() - at,
|
|
537
|
+
request: query ? { query } : undefined,
|
|
538
|
+
origin: 'listener',
|
|
539
|
+
});
|
|
540
|
+
this.emitListener('errored', { id: listenerId, path }, auth, {
|
|
541
|
+
event: 'value',
|
|
542
|
+
result: 'deny',
|
|
543
|
+
error: {
|
|
544
|
+
code: 'PERMISSION_DENIED',
|
|
545
|
+
message: 'PERMISSION_DENIED: Permission denied',
|
|
546
|
+
reasons: evaluation.reasons,
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
throw permissionDenied();
|
|
550
|
+
}
|
|
551
|
+
this.emitOperation(auth, 'listen', path, 'allow', evaluation, {
|
|
552
|
+
at,
|
|
553
|
+
durationMs: Date.now() - at,
|
|
554
|
+
request: query ? { query } : undefined,
|
|
555
|
+
origin: 'listener',
|
|
556
|
+
});
|
|
557
|
+
const listener = { id: listenerId, auth, cb, path, query };
|
|
558
|
+
this.valueListeners.add(listener);
|
|
559
|
+
this.emitListener('attach', listener, auth, {
|
|
560
|
+
event: 'value',
|
|
561
|
+
result: 'allow',
|
|
562
|
+
detail: query ? { query } : undefined,
|
|
563
|
+
});
|
|
564
|
+
// Initial fire — for queries we record the initial window so the
|
|
565
|
+
// diff check on the next write knows what "the previous result"
|
|
566
|
+
// was. For plain listeners we just feed the path snapshot.
|
|
567
|
+
if (query) {
|
|
568
|
+
const initialWindow = executeQuery(this.tree.read(path), query);
|
|
569
|
+
listener.lastWindow = initialWindow;
|
|
570
|
+
const snap = {
|
|
571
|
+
val: rowsToVal(initialWindow),
|
|
572
|
+
exists: initialWindow.length > 0,
|
|
573
|
+
key: this.keyForPath(path),
|
|
574
|
+
};
|
|
575
|
+
try {
|
|
576
|
+
cb(snap);
|
|
577
|
+
this.emitListener('delivery', listener, auth, {
|
|
578
|
+
event: 'value',
|
|
579
|
+
size: initialWindow.length,
|
|
580
|
+
sample: snap.val,
|
|
581
|
+
detail: { initial: true, query: true },
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
this.emitListener('errored', listener, auth, {
|
|
586
|
+
event: 'value',
|
|
587
|
+
result: 'error',
|
|
588
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
589
|
+
detail: { initial: true, query: true },
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
const snap = this.makeSnap(path);
|
|
595
|
+
// Record the initial value so a subsequent no-change write is
|
|
596
|
+
// suppressed (DB-B8).
|
|
597
|
+
listener.lastValue = snap.val;
|
|
598
|
+
try {
|
|
599
|
+
cb(snap);
|
|
600
|
+
this.emitListener('delivery', listener, auth, {
|
|
601
|
+
event: 'value',
|
|
602
|
+
size: snap.exists ? 1 : 0,
|
|
603
|
+
sample: snap.val,
|
|
604
|
+
detail: { initial: true },
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
catch (e) {
|
|
608
|
+
this.emitListener('errored', listener, auth, {
|
|
609
|
+
event: 'value',
|
|
610
|
+
result: 'error',
|
|
611
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
612
|
+
detail: { initial: true },
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return () => {
|
|
617
|
+
this.valueListeners.delete(listener);
|
|
618
|
+
this.emitListener('detach', listener, auth, { event: 'value' });
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* One-shot `get` against a query — evaluates the constraint chain
|
|
623
|
+
* against the current tree snapshot. Same rules check as plain `get`
|
|
624
|
+
* (the read is at the query's root path; per-child rule evaluation
|
|
625
|
+
* isn't modeled here — RTDB itself rejects queries that span
|
|
626
|
+
* children with mixed read permissions).
|
|
627
|
+
*/
|
|
628
|
+
getQuery(auth, path, spec) {
|
|
629
|
+
const at = Date.now();
|
|
630
|
+
const evaluation = this.rules.evaluate('read', path === '/' ? '/' : path, {
|
|
631
|
+
auth,
|
|
632
|
+
mockData: this.tree.snapshot(),
|
|
633
|
+
});
|
|
634
|
+
if (evaluation.check !== 'allow') {
|
|
635
|
+
this.emitOperation(auth, 'get', path, 'deny', evaluation, {
|
|
636
|
+
at,
|
|
637
|
+
durationMs: Date.now() - at,
|
|
638
|
+
request: { query: spec },
|
|
639
|
+
});
|
|
640
|
+
throw permissionDenied();
|
|
641
|
+
}
|
|
642
|
+
const rows = executeQuery(this.tree.read(path), spec);
|
|
643
|
+
this.emitOperation(auth, 'get', path, 'allow', evaluation, {
|
|
644
|
+
at,
|
|
645
|
+
durationMs: Date.now() - at,
|
|
646
|
+
request: { query: spec },
|
|
647
|
+
resourceBefore: { data: rowsToVal(rows), exists: rows.length > 0 },
|
|
648
|
+
});
|
|
649
|
+
return rows;
|
|
650
|
+
}
|
|
651
|
+
/** The last segment of `path`, or `null` for root. Mirrors
|
|
652
|
+
* `DataSnapshot.key`. */
|
|
653
|
+
keyForPath(path) {
|
|
654
|
+
const segs = pathSegments(path);
|
|
655
|
+
return segs.length === 0 ? null : segs[segs.length - 1];
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Compute the listener payload for `path` against the current tree.
|
|
659
|
+
* `key` is the last segment of the path, or `null` for root —
|
|
660
|
+
* matches `DataSnapshot.key`.
|
|
661
|
+
*/
|
|
662
|
+
makeSnap(path) {
|
|
663
|
+
const val = this.tree.read(path);
|
|
664
|
+
const exists = val !== null;
|
|
665
|
+
const segs = pathSegments(path);
|
|
666
|
+
const key = segs.length === 0 ? null : segs[segs.length - 1];
|
|
667
|
+
return { val, exists, key };
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Fan out `value` listeners after a batch of paths were written.
|
|
671
|
+
* Fires every listener whose path either equals one of the touched
|
|
672
|
+
* paths or is an ancestor/descendant of any (the listener observes
|
|
673
|
+
* the subtree it's watching, so any descendant write triggers).
|
|
674
|
+
*/
|
|
675
|
+
fanOut(touched) {
|
|
676
|
+
if (this.valueListeners.size === 0)
|
|
677
|
+
return;
|
|
678
|
+
const touchedSet = touched.map((p) => joinPath(pathSegments(p)));
|
|
679
|
+
for (const listener of this.valueListeners) {
|
|
680
|
+
const lp = joinPath(pathSegments(listener.path));
|
|
681
|
+
const subtreeTouched = touchedSet.some((tp) => {
|
|
682
|
+
if (tp === lp)
|
|
683
|
+
return true;
|
|
684
|
+
const lpp = lp === '/' ? '/' : lp + '/';
|
|
685
|
+
const tpp = tp === '/' ? '/' : tp + '/';
|
|
686
|
+
// Listener watches root: every write fires it.
|
|
687
|
+
if (lp === '/')
|
|
688
|
+
return true;
|
|
689
|
+
// Touched is a descendant of the listener's path → fires.
|
|
690
|
+
if (tp.startsWith(lpp))
|
|
691
|
+
return true;
|
|
692
|
+
// Touched is an ancestor of the listener's path → also fires
|
|
693
|
+
// (the listener's subtree might be different now).
|
|
694
|
+
if (lp.startsWith(tpp))
|
|
695
|
+
return true;
|
|
696
|
+
return false;
|
|
697
|
+
});
|
|
698
|
+
if (!subtreeTouched)
|
|
699
|
+
continue;
|
|
700
|
+
if (listener.query) {
|
|
701
|
+
// Query listener: only fire if the windowed result changed.
|
|
702
|
+
// Locked by oracle observation
|
|
703
|
+
// `rtdb-modular-onvalue-with-query.json` — a write OUTSIDE the
|
|
704
|
+
// window doesn't fire the query listener.
|
|
705
|
+
const nextWindow = executeQuery(this.tree.read(listener.path), listener.query);
|
|
706
|
+
if (windowsEqual(listener.lastWindow ?? [], nextWindow)) {
|
|
707
|
+
this.emitListener('suppressed', listener, listener.auth, {
|
|
708
|
+
event: 'value',
|
|
709
|
+
reason: 'no-op',
|
|
710
|
+
detail: { query: true },
|
|
711
|
+
});
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
listener.lastWindow = nextWindow;
|
|
715
|
+
const snap = {
|
|
716
|
+
val: rowsToVal(nextWindow),
|
|
717
|
+
exists: nextWindow.length > 0,
|
|
718
|
+
key: this.keyForPath(listener.path),
|
|
719
|
+
};
|
|
720
|
+
this.emitListener('delivery', listener, listener.auth, {
|
|
721
|
+
event: 'value',
|
|
722
|
+
size: nextWindow.length,
|
|
723
|
+
sample: snap.val,
|
|
724
|
+
detail: { query: true },
|
|
725
|
+
});
|
|
726
|
+
try {
|
|
727
|
+
listener.cb(snap);
|
|
728
|
+
}
|
|
729
|
+
catch (e) {
|
|
730
|
+
this.emitListener('errored', listener, listener.auth, {
|
|
731
|
+
event: 'value',
|
|
732
|
+
result: 'error',
|
|
733
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
734
|
+
detail: { query: true },
|
|
735
|
+
});
|
|
736
|
+
// Swallow — see plain-listener branch below.
|
|
737
|
+
}
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
// Plain listener: suppress the fire when the value at the
|
|
741
|
+
// listener's exact path is byte-identical to the last fired value
|
|
742
|
+
// (DB-B8). An ancestor/descendant write that leaves this subtree
|
|
743
|
+
// unchanged must NOT re-fire — RTDB's SyncTree dedups no-change.
|
|
744
|
+
const snap = this.makeSnap(listener.path);
|
|
745
|
+
const last = listener.lastValue;
|
|
746
|
+
if (last !== undefined && jsonValuesEqual(last, snap.val)) {
|
|
747
|
+
this.emitListener('suppressed', listener, listener.auth, {
|
|
748
|
+
event: 'value',
|
|
749
|
+
reason: 'no-op',
|
|
750
|
+
});
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
listener.lastValue = snap.val;
|
|
754
|
+
this.emitListener('delivery', listener, listener.auth, {
|
|
755
|
+
event: 'value',
|
|
756
|
+
size: snap.exists ? 1 : 0,
|
|
757
|
+
sample: snap.val,
|
|
758
|
+
});
|
|
759
|
+
try {
|
|
760
|
+
listener.cb(snap);
|
|
761
|
+
}
|
|
762
|
+
catch (e) {
|
|
763
|
+
this.emitListener('errored', listener, listener.auth, {
|
|
764
|
+
event: 'value',
|
|
765
|
+
result: 'error',
|
|
766
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
767
|
+
});
|
|
768
|
+
// Swallow — matches `firebase/database` which doesn't let
|
|
769
|
+
// one observer's throw block another. The faulty listener
|
|
770
|
+
// is the caller's problem.
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/**
|
|
775
|
+
* `runTransaction(path, updateFn, options?)` — RTDB-flavored atomic
|
|
776
|
+
* read-modify-write. The contract mirrors `firebase/database`'s
|
|
777
|
+
* modular `runTransaction`:
|
|
778
|
+
*
|
|
779
|
+
* - Read the current value at `path`.
|
|
780
|
+
* - Call `updateFn(currentValue)`.
|
|
781
|
+
* - `currentValue` is `null` for an absent path (oracle:
|
|
782
|
+
* `rtdb-modular-runtransaction-current-value-arg.json` →
|
|
783
|
+
* `missingArgs[0].isNull === true`).
|
|
784
|
+
* - If `updateFn` returns `undefined` → **abort**: no write happens,
|
|
785
|
+
* resolves `{ committed: false, snapshot }` where the snapshot
|
|
786
|
+
* reflects the pre-transaction value (oracle:
|
|
787
|
+
* `rtdb-modular-runtransaction-abort-undefined.json` → `committed:
|
|
788
|
+
* false, snapVal: null, afterValOnServer: 100`).
|
|
789
|
+
* - If `updateFn` returns any defined value → run the write under
|
|
790
|
+
* the current identity's rules; on allow, commit and resolve
|
|
791
|
+
* `{ committed: true, snapshot }` with the new value. On deny,
|
|
792
|
+
* throw the RTDB-transaction error shape (oracle:
|
|
793
|
+
* `rtdb-modular-runtransaction-on-rules-denied-path.json` →
|
|
794
|
+
* `threw: true, message: 'permission_denied', code: null,
|
|
795
|
+
* constructorName: 'Error'`).
|
|
796
|
+
*
|
|
797
|
+
* Single-client harness → no real concurrency to retry against; the
|
|
798
|
+
* documented "optimistic concurrency with retry" is degenerate here.
|
|
799
|
+
* The sandbox doesn't speculatively call `updateFn` with `null` first
|
|
800
|
+
* for a seeded path (prod does — second invocation with the server
|
|
801
|
+
* value); our contract is the SIMPLEST observable: one invocation
|
|
802
|
+
* with the actual current value.
|
|
803
|
+
*
|
|
804
|
+
* `applyLocally` semantics: when omitted or `true`, the in-flight
|
|
805
|
+
* (post-update-fn, pre-commit) value fans out to listeners as an
|
|
806
|
+
* optimistic fire. With `false`, no listener fire until commit. The
|
|
807
|
+
* difference is invisible in a single-client harness with a
|
|
808
|
+
* synchronous commit path; we still respect the flag so consumers can
|
|
809
|
+
* tune intermediate-fire counts under custom assertions.
|
|
810
|
+
*
|
|
811
|
+
* Returns a payload the modular surface wraps in a `DataSnapshot`
|
|
812
|
+
* shape — the backend stays snapshot-agnostic so the rules engine
|
|
813
|
+
* doesn't need to know about ref objects.
|
|
814
|
+
*/
|
|
815
|
+
runTransaction(auth, path, updateFn, options) {
|
|
816
|
+
const applyLocally = options?.applyLocally !== false;
|
|
817
|
+
const segs = pathSegments(path);
|
|
818
|
+
const key = segs.length === 0 ? null : segs[segs.length - 1];
|
|
819
|
+
const current = this.tree.read(path);
|
|
820
|
+
// Hand the user-fn a deep clone so mutation of the arg doesn't
|
|
821
|
+
// corrupt our stored tree (consumer code that does
|
|
822
|
+
// `current.count++; return current` would otherwise mutate-then-
|
|
823
|
+
// read the same reference). Array-coerce so the fn sees the same
|
|
824
|
+
// shape `get()` would return (DB-B2).
|
|
825
|
+
const currentForFn = current === null ? null : coerceArrays(cloneJson(current));
|
|
826
|
+
const proposed = updateFn(currentForFn);
|
|
827
|
+
const groupId = this.nextGroupId('transaction');
|
|
828
|
+
if (proposed === undefined) {
|
|
829
|
+
this.emitOperation(auth, 'transaction', path, 'not-applicable', undefined, {
|
|
830
|
+
origin: 'transaction',
|
|
831
|
+
groupId,
|
|
832
|
+
groupKind: 'transaction',
|
|
833
|
+
resourceBefore: { data: current, exists: current !== null },
|
|
834
|
+
detail: { committed: false, aborted: true },
|
|
835
|
+
});
|
|
836
|
+
// Abort — no write, no rule check, no listener fire. Resolve with
|
|
837
|
+
// the pre-transaction value (oracle pinned for the seeded case:
|
|
838
|
+
// `afterValOnServer: 100` preserved).
|
|
839
|
+
return { committed: false, val: current, key };
|
|
840
|
+
}
|
|
841
|
+
// Resolve sentinels + normalize first so the rule check sees the
|
|
842
|
+
// stored shape (consistent with `set`): array coercion, null/empty
|
|
843
|
+
// pruning, key validation.
|
|
844
|
+
const now = Date.now();
|
|
845
|
+
const resolved = normalizeWrite(resolveSentinels(proposed, now, current), path === '/' ? '' : path);
|
|
846
|
+
// Apply locally if allowed — fires listeners with the optimistic
|
|
847
|
+
// value BEFORE the rule check. The single-client sandbox has a
|
|
848
|
+
// synchronous commit path so this is effectively a no-op in
|
|
849
|
+
// practice; the flag is honored for parity with prod's contract.
|
|
850
|
+
if (applyLocally) {
|
|
851
|
+
// Stash the prior value so a denial below can roll back. The
|
|
852
|
+
// tree's write replaces; we hold the snapshot of the pre-write
|
|
853
|
+
// root to restore on rule-deny.
|
|
854
|
+
const priorRoot = this.tree.snapshot();
|
|
855
|
+
this.tree.write(path, resolved);
|
|
856
|
+
this.fanOut([path]);
|
|
857
|
+
const at = Date.now();
|
|
858
|
+
const evaluation = this.rules.evaluate('write', path === '/' ? '/' : path, {
|
|
859
|
+
auth,
|
|
860
|
+
mockData: priorRoot,
|
|
861
|
+
newData: resolved,
|
|
862
|
+
});
|
|
863
|
+
if (evaluation.check !== 'allow') {
|
|
864
|
+
this.emitOperation(auth, 'transaction', path, 'deny', evaluation, {
|
|
865
|
+
at,
|
|
866
|
+
durationMs: Date.now() - at,
|
|
867
|
+
origin: 'transaction',
|
|
868
|
+
request: { data: proposed, resourceData: proposed },
|
|
869
|
+
resourceBefore: { data: current, exists: current !== null },
|
|
870
|
+
resourceAfter: { data: resolved, exists: resolved !== null },
|
|
871
|
+
groupId,
|
|
872
|
+
groupKind: 'transaction',
|
|
873
|
+
});
|
|
874
|
+
// Roll back the local apply and re-fan-out so listeners see
|
|
875
|
+
// the restored value. Then throw the transaction-specific
|
|
876
|
+
// denial shape.
|
|
877
|
+
this.tree.restore(priorRoot);
|
|
878
|
+
this.fanOut([path]);
|
|
879
|
+
throw transactionPermissionDenied();
|
|
880
|
+
}
|
|
881
|
+
this.emitOperation(auth, 'transaction', path, 'allow', evaluation, {
|
|
882
|
+
at,
|
|
883
|
+
durationMs: Date.now() - at,
|
|
884
|
+
origin: 'transaction',
|
|
885
|
+
request: { data: proposed, resourceData: proposed },
|
|
886
|
+
resourceBefore: { data: current, exists: current !== null },
|
|
887
|
+
resourceAfter: { data: resolved, exists: resolved !== null },
|
|
888
|
+
groupId,
|
|
889
|
+
groupKind: 'transaction',
|
|
890
|
+
});
|
|
891
|
+
this.emitCommit(auth, 'transaction', path, {
|
|
892
|
+
data: proposed,
|
|
893
|
+
priorState: current,
|
|
894
|
+
nextState: resolved,
|
|
895
|
+
groupId,
|
|
896
|
+
groupKind: 'transaction',
|
|
897
|
+
replay: { requestTime: now },
|
|
898
|
+
detail: { committed: true, applyLocally: true },
|
|
899
|
+
});
|
|
900
|
+
this.emitRtdbEvent(auth, 'transaction', canonicalPath(path), {
|
|
901
|
+
before: current,
|
|
902
|
+
after: resolved,
|
|
903
|
+
detail: { committed: true },
|
|
904
|
+
});
|
|
905
|
+
this.notifyWrite();
|
|
906
|
+
return { committed: true, val: resolved, key };
|
|
907
|
+
}
|
|
908
|
+
// applyLocally: false — rule-check FIRST, write only if allowed,
|
|
909
|
+
// listeners see only the committed value.
|
|
910
|
+
const at = Date.now();
|
|
911
|
+
const evaluation = this.rules.evaluate('write', path === '/' ? '/' : path, {
|
|
912
|
+
auth,
|
|
913
|
+
mockData: this.tree.snapshot(),
|
|
914
|
+
newData: resolved,
|
|
915
|
+
});
|
|
916
|
+
if (evaluation.check !== 'allow') {
|
|
917
|
+
this.emitOperation(auth, 'transaction', path, 'deny', evaluation, {
|
|
918
|
+
at,
|
|
919
|
+
durationMs: Date.now() - at,
|
|
920
|
+
origin: 'transaction',
|
|
921
|
+
request: { data: proposed, resourceData: proposed },
|
|
922
|
+
resourceBefore: { data: current, exists: current !== null },
|
|
923
|
+
resourceAfter: { data: resolved, exists: resolved !== null },
|
|
924
|
+
groupId,
|
|
925
|
+
groupKind: 'transaction',
|
|
926
|
+
});
|
|
927
|
+
throw transactionPermissionDenied();
|
|
928
|
+
}
|
|
929
|
+
this.emitOperation(auth, 'transaction', path, 'allow', evaluation, {
|
|
930
|
+
at,
|
|
931
|
+
durationMs: Date.now() - at,
|
|
932
|
+
origin: 'transaction',
|
|
933
|
+
request: { data: proposed, resourceData: proposed },
|
|
934
|
+
resourceBefore: { data: current, exists: current !== null },
|
|
935
|
+
resourceAfter: { data: resolved, exists: resolved !== null },
|
|
936
|
+
groupId,
|
|
937
|
+
groupKind: 'transaction',
|
|
938
|
+
});
|
|
939
|
+
this.tree.write(path, resolved);
|
|
940
|
+
this.fanOut([path]);
|
|
941
|
+
this.emitCommit(auth, 'transaction', path, {
|
|
942
|
+
data: proposed,
|
|
943
|
+
priorState: current,
|
|
944
|
+
nextState: resolved,
|
|
945
|
+
groupId,
|
|
946
|
+
groupKind: 'transaction',
|
|
947
|
+
replay: { requestTime: now },
|
|
948
|
+
detail: { committed: true, applyLocally: false },
|
|
949
|
+
});
|
|
950
|
+
this.emitRtdbEvent(auth, 'transaction', canonicalPath(path), {
|
|
951
|
+
before: current,
|
|
952
|
+
after: resolved,
|
|
953
|
+
detail: { committed: true },
|
|
954
|
+
});
|
|
955
|
+
return { committed: true, val: resolved, key };
|
|
956
|
+
}
|
|
957
|
+
// ─── Child-event listeners (Tier 2) ────────────────────────────────
|
|
958
|
+
//
|
|
959
|
+
// Per-child diff fanout. Each child listener watches a parent path;
|
|
960
|
+
// on write we compare the parent's prior children against its next
|
|
961
|
+
// children and dispatch `child_added` / `child_changed` /
|
|
962
|
+
// `child_removed` accordingly.
|
|
963
|
+
//
|
|
964
|
+
// `child_moved` requires an ordered query (see oracle observation
|
|
965
|
+
// `rtdb-modular-onchildmoved-with-orderby.json`) — for plain refs
|
|
966
|
+
// it never fires. Tier 3 will wire the ordered-query path in.
|
|
967
|
+
/**
|
|
968
|
+
* Subscribe to a child event at `path`.
|
|
969
|
+
*
|
|
970
|
+
* Semantics (locked by oracle observations under
|
|
971
|
+
* `scripts/oracle/observations/rtdb-modular-onchild*.json`):
|
|
972
|
+
*
|
|
973
|
+
* - `child_added`: replays every existing direct child of `path` on
|
|
974
|
+
* subscribe (one fire per existing key, in current key-iteration
|
|
975
|
+
* order — matches the upstream `orderByKey` default observed in
|
|
976
|
+
* `rtdb-modular-onchildadded-initial-replay`). Subsequent
|
|
977
|
+
* additions fire once each.
|
|
978
|
+
* - `child_changed`: no initial replay. Fires when an existing
|
|
979
|
+
* child's value transitions to a NEW non-null value. Snapshot
|
|
980
|
+
* carries the NEW value.
|
|
981
|
+
* - `child_removed`: no initial replay. Fires when a child is
|
|
982
|
+
* deleted (its value transitions to absent). Snapshot carries
|
|
983
|
+
* the PRIOR (now-removed) value.
|
|
984
|
+
* - `child_moved`: ordered-query only — never fires on a plain
|
|
985
|
+
* ref (matches the upstream contract; Tier 3 will wire ordered
|
|
986
|
+
* queries in).
|
|
987
|
+
*
|
|
988
|
+
* Rules check happens at subscribe time, identical to `onValue`. A
|
|
989
|
+
* denied subscribe throws the plain-`Error` `PERMISSION_DENIED`
|
|
990
|
+
* shape and the listener is never registered.
|
|
991
|
+
*
|
|
992
|
+
* Returns an idempotent unsubscribe.
|
|
993
|
+
*/
|
|
994
|
+
onChild(auth, event, path, cb, spec) {
|
|
995
|
+
const listenerId = this.nextListenerId();
|
|
996
|
+
const at = Date.now();
|
|
997
|
+
const evaluation = this.rules.evaluate('read', path === '/' ? '/' : path, {
|
|
998
|
+
auth,
|
|
999
|
+
mockData: this.tree.snapshot(),
|
|
1000
|
+
});
|
|
1001
|
+
if (evaluation.check !== 'allow') {
|
|
1002
|
+
this.emitOperation(auth, 'listen', path, 'deny', evaluation, {
|
|
1003
|
+
at,
|
|
1004
|
+
durationMs: Date.now() - at,
|
|
1005
|
+
origin: 'listener',
|
|
1006
|
+
detail: { event },
|
|
1007
|
+
});
|
|
1008
|
+
this.emitListener('errored', { id: listenerId, path }, auth, {
|
|
1009
|
+
event,
|
|
1010
|
+
result: 'deny',
|
|
1011
|
+
error: {
|
|
1012
|
+
code: 'PERMISSION_DENIED',
|
|
1013
|
+
message: 'PERMISSION_DENIED: Permission denied',
|
|
1014
|
+
reasons: evaluation.reasons,
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
throw permissionDenied();
|
|
1018
|
+
}
|
|
1019
|
+
this.emitOperation(auth, 'listen', path, 'allow', evaluation, {
|
|
1020
|
+
at,
|
|
1021
|
+
durationMs: Date.now() - at,
|
|
1022
|
+
origin: 'listener',
|
|
1023
|
+
detail: { event },
|
|
1024
|
+
});
|
|
1025
|
+
const listener = { id: listenerId, auth, event, path, cb, spec };
|
|
1026
|
+
this.childListeners.add(listener);
|
|
1027
|
+
this.emitListener('attach', listener, auth, {
|
|
1028
|
+
event,
|
|
1029
|
+
result: 'allow',
|
|
1030
|
+
detail: spec ? { query: spec } : undefined,
|
|
1031
|
+
});
|
|
1032
|
+
// Initial state. A query child listener records its ordered window so
|
|
1033
|
+
// the next write's diff knows what "the previous result" was; a
|
|
1034
|
+
// `child_added` query listener also replays the window (in window
|
|
1035
|
+
// order). A plain-ref listener replays raw direct children.
|
|
1036
|
+
if (spec) {
|
|
1037
|
+
const initialWindow = executeQuery(this.tree.read(path), spec);
|
|
1038
|
+
listener.lastWindow = initialWindow;
|
|
1039
|
+
if (event === 'child_added') {
|
|
1040
|
+
for (const { key, value } of initialWindow) {
|
|
1041
|
+
this.emitListener('delivery', listener, auth, {
|
|
1042
|
+
event,
|
|
1043
|
+
size: 1,
|
|
1044
|
+
sample: { key, val: value },
|
|
1045
|
+
detail: { initial: true, query: true },
|
|
1046
|
+
});
|
|
1047
|
+
try {
|
|
1048
|
+
cb({ key, val: value });
|
|
1049
|
+
}
|
|
1050
|
+
catch (e) {
|
|
1051
|
+
this.emitListener('errored', listener, auth, {
|
|
1052
|
+
event,
|
|
1053
|
+
result: 'error',
|
|
1054
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1055
|
+
detail: { initial: true, query: true },
|
|
1056
|
+
});
|
|
1057
|
+
// Swallow — see fanOut.
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
else if (event === 'child_added') {
|
|
1063
|
+
// Initial replay — only `child_added` replays existing children.
|
|
1064
|
+
for (const { key, val } of this.directChildren(path)) {
|
|
1065
|
+
this.emitListener('delivery', listener, auth, {
|
|
1066
|
+
event,
|
|
1067
|
+
size: 1,
|
|
1068
|
+
sample: { key, val },
|
|
1069
|
+
detail: { initial: true },
|
|
1070
|
+
});
|
|
1071
|
+
try {
|
|
1072
|
+
cb({ key, val });
|
|
1073
|
+
}
|
|
1074
|
+
catch (e) {
|
|
1075
|
+
this.emitListener('errored', listener, auth, {
|
|
1076
|
+
event,
|
|
1077
|
+
result: 'error',
|
|
1078
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1079
|
+
detail: { initial: true },
|
|
1080
|
+
});
|
|
1081
|
+
// Swallow — see fanOut.
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return () => {
|
|
1086
|
+
this.childListeners.delete(listener);
|
|
1087
|
+
this.emitListener('detach', listener, auth, { event });
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* `off(refPath, eventType?, callback?)` — unsubscribe variant. Matches
|
|
1092
|
+
* the upstream contract (oracle: `rtdb-modular-off-stops-child-fires`):
|
|
1093
|
+
*
|
|
1094
|
+
* - `off(refPath)` removes ALL listeners (value + child) at the path.
|
|
1095
|
+
* - `off(refPath, 'value')` removes only `value` listeners at the path.
|
|
1096
|
+
* - `off(refPath, 'child_added')` (or any child event type) removes
|
|
1097
|
+
* only that child-event variety at the path.
|
|
1098
|
+
* - `off(refPath, eventType, cb)` removes only the matching callback.
|
|
1099
|
+
*
|
|
1100
|
+
* No-throw on absent listeners — matches the upstream behavior.
|
|
1101
|
+
*/
|
|
1102
|
+
off(path, eventType, callback) {
|
|
1103
|
+
const canonical = joinPath(pathSegments(path));
|
|
1104
|
+
// Value listeners.
|
|
1105
|
+
if (eventType === undefined || eventType === 'value') {
|
|
1106
|
+
for (const l of [...this.valueListeners]) {
|
|
1107
|
+
if (joinPath(pathSegments(l.path)) !== canonical)
|
|
1108
|
+
continue;
|
|
1109
|
+
if (callback !== undefined && l.cb !== callback)
|
|
1110
|
+
continue;
|
|
1111
|
+
this.valueListeners.delete(l);
|
|
1112
|
+
this.emitListener('detach', l, l.auth, { event: 'value' });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Child listeners.
|
|
1116
|
+
const childEvents = [
|
|
1117
|
+
'child_added',
|
|
1118
|
+
'child_changed',
|
|
1119
|
+
'child_removed',
|
|
1120
|
+
'child_moved',
|
|
1121
|
+
];
|
|
1122
|
+
const targetEvents = eventType === undefined
|
|
1123
|
+
? childEvents
|
|
1124
|
+
: eventType === 'value'
|
|
1125
|
+
? []
|
|
1126
|
+
: [eventType];
|
|
1127
|
+
if (targetEvents.length > 0) {
|
|
1128
|
+
for (const l of [...this.childListeners]) {
|
|
1129
|
+
if (!targetEvents.includes(l.event))
|
|
1130
|
+
continue;
|
|
1131
|
+
if (joinPath(pathSegments(l.path)) !== canonical)
|
|
1132
|
+
continue;
|
|
1133
|
+
if (callback !== undefined && l.cb !== callback)
|
|
1134
|
+
continue;
|
|
1135
|
+
this.childListeners.delete(l);
|
|
1136
|
+
this.emitListener('detach', l, l.auth, { event: l.event });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/** Direct children of `path` as `{ key, val }` pairs. Returns [] if
|
|
1141
|
+
* the path is absent or its value isn't an object. Key iteration
|
|
1142
|
+
* follows `Object.keys` order, which matches `firebase/database`'s
|
|
1143
|
+
* `orderByKey` default (insertion order for non-numeric keys). */
|
|
1144
|
+
directChildren(path) {
|
|
1145
|
+
const v = this.tree.read(path);
|
|
1146
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v))
|
|
1147
|
+
return [];
|
|
1148
|
+
const out = [];
|
|
1149
|
+
for (const [k, val] of Object.entries(v)) {
|
|
1150
|
+
out.push({ key: k, val });
|
|
1151
|
+
}
|
|
1152
|
+
return out;
|
|
1153
|
+
}
|
|
1154
|
+
/** Snapshot the current direct children of every registered child
|
|
1155
|
+
* listener's parent path. Used to capture the "prior" half of the
|
|
1156
|
+
* diff before a write applies. */
|
|
1157
|
+
snapshotChildListenerParents() {
|
|
1158
|
+
const out = new Map();
|
|
1159
|
+
if (this.childListeners.size === 0)
|
|
1160
|
+
return out;
|
|
1161
|
+
for (const listener of this.childListeners) {
|
|
1162
|
+
const canonical = joinPath(pathSegments(listener.path));
|
|
1163
|
+
if (out.has(canonical))
|
|
1164
|
+
continue;
|
|
1165
|
+
const map = new Map();
|
|
1166
|
+
for (const { key, val } of this.directChildren(canonical)) {
|
|
1167
|
+
map.set(key, val);
|
|
1168
|
+
}
|
|
1169
|
+
out.set(canonical, map);
|
|
1170
|
+
}
|
|
1171
|
+
return out;
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Fan out child events after a write. `priorByParent` is the snapshot
|
|
1175
|
+
* captured by `snapshotChildListenerParents` before the write applied.
|
|
1176
|
+
*
|
|
1177
|
+
* For each listener, compute the diff between prior and current
|
|
1178
|
+
* children, then dispatch:
|
|
1179
|
+
* - `child_added` when a key appears that wasn't in prior.
|
|
1180
|
+
* - `child_changed` when a key's value transitions.
|
|
1181
|
+
* - `child_removed` when a prior key is gone.
|
|
1182
|
+
*/
|
|
1183
|
+
fanOutChildren(priorByParent) {
|
|
1184
|
+
if (this.childListeners.size === 0)
|
|
1185
|
+
return;
|
|
1186
|
+
// Group PLAIN-ref listeners by their canonical parent path so we only
|
|
1187
|
+
// do one diff per parent regardless of how many listeners are attached.
|
|
1188
|
+
// Query listeners diff their ordered window per-listener (below) — the
|
|
1189
|
+
// raw key-set diff would ignore the window/ordering.
|
|
1190
|
+
const byParent = new Map();
|
|
1191
|
+
for (const listener of this.childListeners) {
|
|
1192
|
+
if (listener.spec) {
|
|
1193
|
+
this.fanOutQueryChild(listener);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const canonical = joinPath(pathSegments(listener.path));
|
|
1197
|
+
const arr = byParent.get(canonical) ?? [];
|
|
1198
|
+
arr.push(listener);
|
|
1199
|
+
byParent.set(canonical, arr);
|
|
1200
|
+
}
|
|
1201
|
+
for (const [parentPath, listeners] of byParent) {
|
|
1202
|
+
const prior = priorByParent.get(parentPath) ?? new Map();
|
|
1203
|
+
const next = new Map();
|
|
1204
|
+
for (const { key, val } of this.directChildren(parentPath)) {
|
|
1205
|
+
next.set(key, val);
|
|
1206
|
+
}
|
|
1207
|
+
// Build event lists.
|
|
1208
|
+
const added = [];
|
|
1209
|
+
const changed = [];
|
|
1210
|
+
const removed = [];
|
|
1211
|
+
for (const [k, v] of next) {
|
|
1212
|
+
if (!prior.has(k)) {
|
|
1213
|
+
added.push({ key: k, val: v });
|
|
1214
|
+
}
|
|
1215
|
+
else if (!jsonValuesEqual(prior.get(k), v)) {
|
|
1216
|
+
changed.push({ key: k, val: v });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const [k, v] of prior) {
|
|
1220
|
+
if (!next.has(k)) {
|
|
1221
|
+
removed.push({ key: k, val: v });
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (const listener of listeners) {
|
|
1225
|
+
let events;
|
|
1226
|
+
switch (listener.event) {
|
|
1227
|
+
case 'child_added':
|
|
1228
|
+
events = added;
|
|
1229
|
+
break;
|
|
1230
|
+
case 'child_changed':
|
|
1231
|
+
events = changed;
|
|
1232
|
+
break;
|
|
1233
|
+
case 'child_removed':
|
|
1234
|
+
events = removed;
|
|
1235
|
+
break;
|
|
1236
|
+
case 'child_moved':
|
|
1237
|
+
events = [];
|
|
1238
|
+
break; // ordered-query only
|
|
1239
|
+
}
|
|
1240
|
+
for (const ev of events) {
|
|
1241
|
+
this.emitListener('delivery', listener, listener.auth, {
|
|
1242
|
+
event: listener.event,
|
|
1243
|
+
size: 1,
|
|
1244
|
+
sample: ev,
|
|
1245
|
+
});
|
|
1246
|
+
try {
|
|
1247
|
+
listener.cb(ev);
|
|
1248
|
+
}
|
|
1249
|
+
catch (e) {
|
|
1250
|
+
this.emitListener('errored', listener, listener.auth, {
|
|
1251
|
+
event: listener.event,
|
|
1252
|
+
result: 'error',
|
|
1253
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1254
|
+
});
|
|
1255
|
+
// Swallow — see fanOut.
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Fan out a single QUERY child listener after a write, diffing the
|
|
1263
|
+
* ordered window against the listener's last-fired window:
|
|
1264
|
+
*
|
|
1265
|
+
* - a key that ENTERED the window emits `child_added` (window order);
|
|
1266
|
+
* - a key that LEFT the window emits `child_removed` (carrying its
|
|
1267
|
+
* prior value);
|
|
1268
|
+
* - a key that stayed in-window but changed value emits `child_changed`.
|
|
1269
|
+
*
|
|
1270
|
+
* `child_moved` (reorder within the window) is deliberately NOT emitted
|
|
1271
|
+
* — the reorder / `previousChildName` semantics are held pending fresh
|
|
1272
|
+
* oracle captures (matrix row `rtdb-modular#137`). The window is always
|
|
1273
|
+
* advanced so a later real add/change/remove diffs correctly.
|
|
1274
|
+
*/
|
|
1275
|
+
fanOutQueryChild(listener) {
|
|
1276
|
+
const prior = listener.lastWindow ?? [];
|
|
1277
|
+
const next = executeQuery(this.tree.read(listener.path), listener.spec);
|
|
1278
|
+
listener.lastWindow = next;
|
|
1279
|
+
const priorByKey = new Map(prior.map((r) => [r.key, r.value]));
|
|
1280
|
+
const nextByKey = new Map(next.map((r) => [r.key, r.value]));
|
|
1281
|
+
let events = [];
|
|
1282
|
+
switch (listener.event) {
|
|
1283
|
+
case 'child_added':
|
|
1284
|
+
for (const { key, value } of next) {
|
|
1285
|
+
if (!priorByKey.has(key))
|
|
1286
|
+
events.push({ key, val: value });
|
|
1287
|
+
}
|
|
1288
|
+
break;
|
|
1289
|
+
case 'child_changed':
|
|
1290
|
+
for (const { key, value } of next) {
|
|
1291
|
+
if (priorByKey.has(key) && !jsonValuesEqual(priorByKey.get(key), value)) {
|
|
1292
|
+
events.push({ key, val: value });
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
break;
|
|
1296
|
+
case 'child_removed':
|
|
1297
|
+
for (const { key, value } of prior) {
|
|
1298
|
+
if (!nextByKey.has(key))
|
|
1299
|
+
events.push({ key, val: value });
|
|
1300
|
+
}
|
|
1301
|
+
break;
|
|
1302
|
+
case 'child_moved':
|
|
1303
|
+
// Held — reorder semantics pending two new oracle captures.
|
|
1304
|
+
events = [];
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
for (const ev of events) {
|
|
1308
|
+
this.emitListener('delivery', listener, listener.auth, {
|
|
1309
|
+
event: listener.event,
|
|
1310
|
+
size: 1,
|
|
1311
|
+
sample: ev,
|
|
1312
|
+
detail: { query: true },
|
|
1313
|
+
});
|
|
1314
|
+
try {
|
|
1315
|
+
listener.cb(ev);
|
|
1316
|
+
}
|
|
1317
|
+
catch (e) {
|
|
1318
|
+
this.emitListener('errored', listener, listener.auth, {
|
|
1319
|
+
event: listener.event,
|
|
1320
|
+
result: 'error',
|
|
1321
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
1322
|
+
detail: { query: true },
|
|
1323
|
+
});
|
|
1324
|
+
// Swallow — see fanOut.
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
// ─── Persistence (PersistableService) ──────────────────────────────
|
|
1329
|
+
//
|
|
1330
|
+
// The RTDB tree rides the sandbox persistence controller's `services`
|
|
1331
|
+
// blob exactly like the auth user DB. `getDatabase(sandbox)` registers
|
|
1332
|
+
// these hooks once per sandbox (see modular.ts). Together they give RTDB
|
|
1333
|
+
// the same durability contract as Firestore/auth: worker death / browser
|
|
1334
|
+
// restart restores the whole tree instead of losing it.
|
|
1335
|
+
/** Serialize the whole tree as a plain JSON value for the persistence
|
|
1336
|
+
* snapshot. Defensive deep-copy (via `DataTree.snapshot`). */
|
|
1337
|
+
exportTree() {
|
|
1338
|
+
return this.tree.snapshot();
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Replace the whole tree with a persisted snapshot, then fire listeners
|
|
1342
|
+
* so any live UI (Studio's RTDB tab) converges on the restored data
|
|
1343
|
+
* rather than showing a stale/empty view.
|
|
1344
|
+
*
|
|
1345
|
+
* REPLACE, not merge — the tree becomes EXACTLY the snapshot. On boot /
|
|
1346
|
+
* late-registration the tree is empty so this is a pure load; for a
|
|
1347
|
+
* runtime `loadSnapshot()` it clobbers divergent state, matching auth's
|
|
1348
|
+
* restore policy.
|
|
1349
|
+
*
|
|
1350
|
+
* Notification: we capture each child-listener parent's prior children
|
|
1351
|
+
* BEFORE the swap, then fan out value listeners from the root (every
|
|
1352
|
+
* listener re-reads its path) and child listeners against the diff. The
|
|
1353
|
+
* value-listener no-op suppression (DB-B8) means a listener whose subtree
|
|
1354
|
+
* is byte-identical to the snapshot won't spuriously re-fire.
|
|
1355
|
+
*/
|
|
1356
|
+
restoreTree(root) {
|
|
1357
|
+
const priors = this.snapshotChildListenerParents();
|
|
1358
|
+
this.tree.restore(root ?? {});
|
|
1359
|
+
this.fanOut(['/']);
|
|
1360
|
+
this.fanOutChildren(priors);
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Register a persistence change-subscriber. Returns an unsubscribe.
|
|
1364
|
+
* Fired (best-effort, isolated) after every committed tree mutation so
|
|
1365
|
+
* the controller schedules a debounced flush.
|
|
1366
|
+
*/
|
|
1367
|
+
subscribeWrites(onChange) {
|
|
1368
|
+
this.writeSubscribers.add(onChange);
|
|
1369
|
+
return () => {
|
|
1370
|
+
this.writeSubscribers.delete(onChange);
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
/** Notify persistence subscribers that the tree changed. Best-effort +
|
|
1374
|
+
* isolated — a subscriber throw must never break the write. */
|
|
1375
|
+
notifyWrite() {
|
|
1376
|
+
if (this.writeSubscribers.size === 0)
|
|
1377
|
+
return;
|
|
1378
|
+
for (const sub of this.writeSubscribers) {
|
|
1379
|
+
try {
|
|
1380
|
+
sub();
|
|
1381
|
+
}
|
|
1382
|
+
catch {
|
|
1383
|
+
// Observational — never let a flush-scheduler throw break a write.
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
/** Generate a key without writing (advisory — same key the next
|
|
1388
|
+
* `push(path)` would produce). Used by the `ref.push().key`
|
|
1389
|
+
* pattern. Each call returns a fresh key. */
|
|
1390
|
+
mintKey() {
|
|
1391
|
+
return generatePushId();
|
|
1392
|
+
}
|
|
1393
|
+
/** Test helper — count value listeners. */
|
|
1394
|
+
listenerCount() {
|
|
1395
|
+
return this.valueListeners.size;
|
|
1396
|
+
}
|
|
1397
|
+
/** Test helper — count child listeners. */
|
|
1398
|
+
childListenerCount() {
|
|
1399
|
+
return this.childListeners.size;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
/** Normalize a path to its canonical `/`-joined form for event paths. */
|
|
1403
|
+
function canonicalPath(path) {
|
|
1404
|
+
return joinPath(pathSegments(path));
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Transaction-specific denial constructor. Pinned by oracle observation
|
|
1408
|
+
* `rtdb-modular-runtransaction-on-rules-denied-path.json`:
|
|
1409
|
+
*
|
|
1410
|
+
* - plain `Error` (NOT `FirebaseError`).
|
|
1411
|
+
* - `.message === 'permission_denied'` (LOWERCASE — distinct from
|
|
1412
|
+
* `set`/`get`'s `'PERMISSION_DENIED: Permission denied'` shape).
|
|
1413
|
+
* - **No** `.code` field on the error (prod observation shows
|
|
1414
|
+
* `code: null` — the FirebaseError code shape isn't applied to
|
|
1415
|
+
* transaction rejections).
|
|
1416
|
+
* - `.constructor.name === 'Error'`.
|
|
1417
|
+
*
|
|
1418
|
+
* This is a deliberately distinct shape from the regular
|
|
1419
|
+
* `permissionDenied()` used by `set`/`get`/`update` — prod really does
|
|
1420
|
+
* emit a different surface for `runTransaction` rejections. The test
|
|
1421
|
+
* suite asserts both shapes so a future "unify the error shape"
|
|
1422
|
+
* refactor would catch the divergence at unit time.
|
|
1423
|
+
*/
|
|
1424
|
+
function transactionPermissionDenied() {
|
|
1425
|
+
return new Error('permission_denied');
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Pack an ordered list of `QueryRow`s into a JSON object — the shape
|
|
1429
|
+
* the `DataSnapshot.val()` contract returns from a query.
|
|
1430
|
+
*
|
|
1431
|
+
* RTDB query snapshots present the windowed rows as a key→value object;
|
|
1432
|
+
* ordering is observable via `snap.forEach` (NOT through `val()`'s
|
|
1433
|
+
* iteration order, which is unspecified for plain objects but happens
|
|
1434
|
+
* to be insertion order in modern V8). The sandbox snap-wrapper
|
|
1435
|
+
* preserves the row order so `snap.forEach` yields children in the
|
|
1436
|
+
* computed query order.
|
|
1437
|
+
*/
|
|
1438
|
+
function rowsToVal(rows) {
|
|
1439
|
+
if (rows.length === 0)
|
|
1440
|
+
return null;
|
|
1441
|
+
const out = {};
|
|
1442
|
+
for (const { key, value } of rows) {
|
|
1443
|
+
out[key] = value;
|
|
1444
|
+
}
|
|
1445
|
+
return out;
|
|
1446
|
+
}
|
|
1447
|
+
/** Compare two windowed result lists. Used to decide whether a query
|
|
1448
|
+
* listener should re-fire. Uses RTDB JSON value equality rather than
|
|
1449
|
+
* `JSON.stringify` so a re-write that only reorders object keys is
|
|
1450
|
+
* correctly treated as "no change" (DB-B11: RTDB treats objects as
|
|
1451
|
+
* order-equal). */
|
|
1452
|
+
function windowsEqual(a, b) {
|
|
1453
|
+
if (a.length !== b.length)
|
|
1454
|
+
return false;
|
|
1455
|
+
for (let i = 0; i < a.length; i++) {
|
|
1456
|
+
const ai = a[i];
|
|
1457
|
+
const bi = b[i];
|
|
1458
|
+
if (ai.key !== bi.key)
|
|
1459
|
+
return false;
|
|
1460
|
+
if (!jsonValuesEqual(ai.value, bi.value))
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
return true;
|
|
1464
|
+
}
|
|
1465
|
+
// Re-exports the modular SDK uses internally.
|
|
1466
|
+
export { cloneJson, pathSegments, joinPath } from './data-tree.js';
|
|
1467
|
+
//# sourceMappingURL=backend.js.map
|