@codyswann/lisa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +867 -0
- package/all/copy-overwrite/.claude/README.md +205 -0
- package/all/copy-overwrite/.claude/agents/agent-architect.md +311 -0
- package/all/copy-overwrite/.claude/agents/codebase-analyzer.md +146 -0
- package/all/copy-overwrite/.claude/agents/codebase-locator.md +125 -0
- package/all/copy-overwrite/.claude/agents/codebase-pattern-finder.md +237 -0
- package/all/copy-overwrite/.claude/agents/git-history-analyzer.md +183 -0
- package/all/copy-overwrite/.claude/agents/hooks-expert.md +74 -0
- package/all/copy-overwrite/.claude/agents/skill-evaluator.md +246 -0
- package/all/copy-overwrite/.claude/agents/slash-command-architect.md +87 -0
- package/all/copy-overwrite/.claude/agents/web-search-researcher.md +112 -0
- package/all/copy-overwrite/.claude/commands/git/commit-and-submit-pr.md +8 -0
- package/all/copy-overwrite/.claude/commands/git/commit.md +44 -0
- package/all/copy-overwrite/.claude/commands/git/prune.md +34 -0
- package/all/copy-overwrite/.claude/commands/git/submit-pr.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/create.md +50 -0
- package/all/copy-overwrite/.claude/commands/jira/verify.md +34 -0
- package/all/copy-overwrite/.claude/commands/project/archive.md +8 -0
- package/all/copy-overwrite/.claude/commands/project/bootstrap.md +49 -0
- package/all/copy-overwrite/.claude/commands/project/complete-task.md +7 -0
- package/all/copy-overwrite/.claude/commands/project/debrief.md +65 -0
- package/all/copy-overwrite/.claude/commands/project/execute.md +94 -0
- package/all/copy-overwrite/.claude/commands/project/implement.md +42 -0
- package/all/copy-overwrite/.claude/commands/project/local-code-review.md +88 -0
- package/all/copy-overwrite/.claude/commands/project/lower-code-complexity.md +74 -0
- package/all/copy-overwrite/.claude/commands/project/plan.md +314 -0
- package/all/copy-overwrite/.claude/commands/project/research.md +248 -0
- package/all/copy-overwrite/.claude/commands/project/review.md +63 -0
- package/all/copy-overwrite/.claude/commands/project/setup.md +19 -0
- package/all/copy-overwrite/.claude/commands/project/verify.md +38 -0
- package/all/copy-overwrite/.claude/commands/pull-request/review.md +12 -0
- package/all/copy-overwrite/.claude/commands/rules/format-md.md +72 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/check.md +6 -0
- package/all/copy-overwrite/.claude/commands/sonarqube/fix.md +3 -0
- package/all/copy-overwrite/.claude/hooks/README.md +301 -0
- package/all/copy-overwrite/.claude/hooks/notify-ntfy.sh +181 -0
- package/all/copy-overwrite/.claude/settings.json +41 -0
- package/all/copy-overwrite/.claude/settings.local.json.example +14 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/SKILL.md +405 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/function-structure.md +416 -0
- package/all/copy-overwrite/.claude/skills/coding-philosophy/references/immutable-patterns.md +316 -0
- package/all/copy-overwrite/.claude/skills/prompt-complexity-scorer/SKILL.md +118 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/SKILL.md +210 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/__pycache__/quick_validate.cpython-312.pyc +0 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/all/copy-overwrite/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/all/copy-overwrite/CLAUDE.md +77 -0
- package/all/copy-overwrite/HUMAN.md +17 -0
- package/all/copy-overwrite/specs/.keep +0 -0
- package/all/create-only/PROJECT_RULES.md +0 -0
- package/cdk/merge/package.json +20 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +107 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/prompts.d.ts +45 -0
- package/dist/cli/prompts.d.ts.map +1 -0
- package/dist/cli/prompts.js +58 -0
- package/dist/cli/prompts.js.map +1 -0
- package/dist/core/config.d.ts +73 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +36 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lisa.d.ts +81 -0
- package/dist/core/lisa.d.ts.map +1 -0
- package/dist/core/lisa.js +459 -0
- package/dist/core/lisa.js.map +1 -0
- package/dist/core/manifest.d.ts +58 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +104 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/detection/detector.interface.d.ts +15 -0
- package/dist/detection/detector.interface.d.ts.map +1 -0
- package/dist/detection/detector.interface.js +2 -0
- package/dist/detection/detector.interface.js.map +1 -0
- package/dist/detection/detectors/cdk.d.ts +10 -0
- package/dist/detection/detectors/cdk.d.ts.map +1 -0
- package/dist/detection/detectors/cdk.js +34 -0
- package/dist/detection/detectors/cdk.js.map +1 -0
- package/dist/detection/detectors/expo.d.ts +10 -0
- package/dist/detection/detectors/expo.d.ts.map +1 -0
- package/dist/detection/detectors/expo.js +30 -0
- package/dist/detection/detectors/expo.js.map +1 -0
- package/dist/detection/detectors/nestjs.d.ts +10 -0
- package/dist/detection/detectors/nestjs.d.ts.map +1 -0
- package/dist/detection/detectors/nestjs.js +34 -0
- package/dist/detection/detectors/nestjs.js.map +1 -0
- package/dist/detection/detectors/npm-package.d.ts +13 -0
- package/dist/detection/detectors/npm-package.d.ts.map +1 -0
- package/dist/detection/detectors/npm-package.js +30 -0
- package/dist/detection/detectors/npm-package.js.map +1 -0
- package/dist/detection/detectors/typescript.d.ts +10 -0
- package/dist/detection/detectors/typescript.d.ts.map +1 -0
- package/dist/detection/detectors/typescript.js +25 -0
- package/dist/detection/detectors/typescript.js.map +1 -0
- package/dist/detection/index.d.ts +24 -0
- package/dist/detection/index.d.ts.map +1 -0
- package/dist/detection/index.js +57 -0
- package/dist/detection/index.js.map +1 -0
- package/dist/errors/index.d.ts +69 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +110 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/console-logger.d.ts +12 -0
- package/dist/logging/console-logger.d.ts.map +1 -0
- package/dist/logging/console-logger.js +22 -0
- package/dist/logging/console-logger.js.map +1 -0
- package/dist/logging/index.d.ts +4 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +3 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.interface.d.ts +20 -0
- package/dist/logging/logger.interface.d.ts.map +1 -0
- package/dist/logging/logger.interface.js +2 -0
- package/dist/logging/logger.interface.js.map +1 -0
- package/dist/logging/silent-logger.d.ts +12 -0
- package/dist/logging/silent-logger.d.ts.map +1 -0
- package/dist/logging/silent-logger.js +21 -0
- package/dist/logging/silent-logger.js.map +1 -0
- package/dist/strategies/copy-contents.d.ts +14 -0
- package/dist/strategies/copy-contents.d.ts.map +1 -0
- package/dist/strategies/copy-contents.js +69 -0
- package/dist/strategies/copy-contents.js.map +1 -0
- package/dist/strategies/copy-overwrite.d.ts +14 -0
- package/dist/strategies/copy-overwrite.d.ts.map +1 -0
- package/dist/strategies/copy-overwrite.js +47 -0
- package/dist/strategies/copy-overwrite.js.map +1 -0
- package/dist/strategies/create-only.d.ts +13 -0
- package/dist/strategies/create-only.d.ts.map +1 -0
- package/dist/strategies/create-only.js +30 -0
- package/dist/strategies/create-only.js.map +1 -0
- package/dist/strategies/index.d.ts +31 -0
- package/dist/strategies/index.d.ts.map +1 -0
- package/dist/strategies/index.js +52 -0
- package/dist/strategies/index.js.map +1 -0
- package/dist/strategies/merge.d.ts +13 -0
- package/dist/strategies/merge.d.ts.map +1 -0
- package/dist/strategies/merge.js +60 -0
- package/dist/strategies/merge.js.map +1 -0
- package/dist/strategies/strategy.interface.d.ts +31 -0
- package/dist/strategies/strategy.interface.d.ts.map +1 -0
- package/dist/strategies/strategy.interface.js +2 -0
- package/dist/strategies/strategy.interface.js.map +1 -0
- package/dist/transaction/backup.d.ts +38 -0
- package/dist/transaction/backup.d.ts.map +1 -0
- package/dist/transaction/backup.js +97 -0
- package/dist/transaction/backup.js.map +1 -0
- package/dist/transaction/index.d.ts +4 -0
- package/dist/transaction/index.d.ts.map +1 -0
- package/dist/transaction/index.js +3 -0
- package/dist/transaction/index.js.map +1 -0
- package/dist/transaction/transaction.d.ts +34 -0
- package/dist/transaction/transaction.d.ts.map +1 -0
- package/dist/transaction/transaction.js +68 -0
- package/dist/transaction/transaction.js.map +1 -0
- package/dist/utils/file-operations.d.ts +29 -0
- package/dist/utils/file-operations.d.ts.map +1 -0
- package/dist/utils/file-operations.js +84 -0
- package/dist/utils/file-operations.js.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/json-utils.d.ts +22 -0
- package/dist/utils/json-utils.d.ts.map +1 -0
- package/dist/utils/json-utils.js +57 -0
- package/dist/utils/json-utils.js.map +1 -0
- package/dist/utils/path-utils.d.ts +21 -0
- package/dist/utils/path-utils.d.ts.map +1 -0
- package/dist/utils/path-utils.js +35 -0
- package/dist/utils/path-utils.js.map +1 -0
- package/eslint-plugin-code-organization/README.md +149 -0
- package/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/eslint-plugin-code-organization/index.js +23 -0
- package/eslint-plugin-code-organization/package.json +10 -0
- package/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/SKILL.md +238 -0
- package/expo/copy-overwrite/.claude/skills/apollo-client/references/mutation-patterns.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/SKILL.md +360 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/atomic-levels.md +417 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/folder-structure.md +257 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/references/gluestack-mapping.md +233 -0
- package/expo/copy-overwrite/.claude/skills/atomic-design-gluestack/scripts/validate_atomic_structure.py +327 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/SKILL.md +299 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/examples.md +749 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/references/patterns.md +318 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/create_component.py +198 -0
- package/expo/copy-overwrite/.claude/skills/container-view-pattern/scripts/validate_component.py +207 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/SKILL.md +268 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/common-issues.md +619 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/file-extensions.md +340 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/references/platform-api.md +276 -0
- package/expo/copy-overwrite/.claude/skills/cross-platform-compatibility/scripts/validate_cross_platform.py +414 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/SKILL.md +202 -0
- package/expo/copy-overwrite/.claude/skills/directory-structure/scripts/validate_structure.py +443 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/SKILL.md +309 -0
- package/expo/copy-overwrite/.claude/skills/expo-env-config/references/validation-patterns.md +417 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/SKILL.md +431 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/references/official-docs.md +290 -0
- package/expo/copy-overwrite/.claude/skills/expo-router-best-practices/scripts/generate-route.py +169 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/SKILL.md +411 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/color-tokens.md +343 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/component-mapping.md +307 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/references/spacing-scale.md +300 -0
- package/expo/copy-overwrite/.claude/skills/gluestack-nativewind/scripts/validate_styling.py +354 -0
- package/expo/copy-overwrite/.claude/skills/local-state/SKILL.md +362 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/async-storage.md +505 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/persistence-patterns.md +711 -0
- package/expo/copy-overwrite/.claude/skills/local-state/references/reactive-variables.md +446 -0
- package/expo/copy-overwrite/.claude/skills/playwright-selectors/SKILL.md +223 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/SKILL.md +319 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/async-patterns.md +420 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/expo-router-testing.md +556 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/mocking-patterns.md +590 -0
- package/expo/copy-overwrite/.claude/skills/testing-library/references/query-priority.md +291 -0
- package/expo/copy-overwrite/.easignore.extra +2 -0
- package/expo/copy-overwrite/.mcp.json +33 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/README.md +234 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/plugin-index.test.js +84 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/require-memo-in-view.test.js +196 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/__tests__/single-component-per-file.test.js +289 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/index.js +32 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/enforce-component-structure.js +230 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/no-return-in-view.js +91 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/require-memo-in-view.js +178 -0
- package/expo/copy-overwrite/eslint-plugin-component-structure/rules/single-component-per-file.js +238 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/README.md +260 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/index.js +29 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/package.json +10 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-classname-outside-ui.js +51 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-direct-rn-imports.js +55 -0
- package/expo/copy-overwrite/eslint-plugin-ui-standards/rules/no-inline-styles.js +73 -0
- package/expo/copy-overwrite/eslint.config.mjs +560 -0
- package/expo/copy-overwrite/lighthouserc.js +194 -0
- package/expo/create-only/lighthouserc-config.json +28 -0
- package/expo/merge/package.json +132 -0
- package/lisa.sh +35 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/SKILL.md +176 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/advanced-features.md +527 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/project-patterns.md +483 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/quick-start.md +257 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/resolvers-mutations.md +413 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-graphql/references/types-scalars.md +513 -0
- package/nestjs/copy-overwrite/.claude/skills/nestjs-rules/SKILL.md +536 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/SKILL.md +275 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/configuration-patterns.md +487 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/entity-patterns.md +450 -0
- package/nestjs/copy-overwrite/.claude/skills/typeorm-patterns/references/observability-patterns.md +536 -0
- package/nestjs/merge/package.json +75 -0
- package/package.json +124 -0
- package/typescript/copy-contents/.husky/commit-msg +91 -0
- package/typescript/copy-contents/.husky/pre-commit +96 -0
- package/typescript/copy-contents/.husky/pre-push +211 -0
- package/typescript/copy-overwrite/.claude/hooks/format-on-edit.sh +74 -0
- package/typescript/copy-overwrite/.claude/hooks/install_pkgs.sh +59 -0
- package/typescript/copy-overwrite/.claude/hooks/lint-on-edit.sh +103 -0
- package/typescript/copy-overwrite/.claude/skills/jsdoc-best-practices/SKILL.md +388 -0
- package/typescript/copy-overwrite/.github/README.md +455 -0
- package/typescript/copy-overwrite/.github/dependabot.yml +40 -0
- package/typescript/copy-overwrite/.github/k6/BROWSER_TESTING_NOTE.md +129 -0
- package/typescript/copy-overwrite/.github/k6/INTEGRATION_GUIDE.md +354 -0
- package/typescript/copy-overwrite/.github/k6/README.md +386 -0
- package/typescript/copy-overwrite/.github/k6/SCENARIO_SELECTION_GUIDE.md +264 -0
- package/typescript/copy-overwrite/.github/k6/examples/customer-deploy-integration.yml +115 -0
- package/typescript/copy-overwrite/.github/k6/examples/data-driven-test.js +268 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.js +142 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/load.json +27 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.js +26 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/smoke.json +20 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.js +244 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/soak.json +29 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.js +180 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/spike.json +32 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.js +206 -0
- package/typescript/copy-overwrite/.github/k6/scenarios/stress.json +38 -0
- package/typescript/copy-overwrite/.github/k6/scripts/api-test.js +452 -0
- package/typescript/copy-overwrite/.github/k6/scripts/default-test.js +185 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/normal.json +30 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/relaxed.json +21 -0
- package/typescript/copy-overwrite/.github/k6/thresholds/strict.json +29 -0
- package/typescript/copy-overwrite/.github/workflows/build.yml +72 -0
- package/typescript/copy-overwrite/.github/workflows/ci.yml +49 -0
- package/typescript/copy-overwrite/.github/workflows/claude.yml +51 -0
- package/typescript/copy-overwrite/.github/workflows/create-github-issue-on-failure.yml +113 -0
- package/typescript/copy-overwrite/.github/workflows/create-jira-issue-on-failure.yml +195 -0
- package/typescript/copy-overwrite/.github/workflows/create-sentry-issue-on-failure.yml +267 -0
- package/typescript/copy-overwrite/.github/workflows/deploy.yml +228 -0
- package/typescript/copy-overwrite/.github/workflows/k6-load-test-README.md +230 -0
- package/typescript/copy-overwrite/.github/workflows/lighthouse.yml +68 -0
- package/typescript/copy-overwrite/.github/workflows/load-test.yml +282 -0
- package/typescript/copy-overwrite/.github/workflows/quality.yml +1737 -0
- package/typescript/copy-overwrite/.github/workflows/release.yml +1599 -0
- package/typescript/copy-overwrite/.gitleaksignore +28 -0
- package/typescript/copy-overwrite/.nvmrc +1 -0
- package/typescript/copy-overwrite/.prettierignore +23 -0
- package/typescript/copy-overwrite/.prettierrc.json +22 -0
- package/typescript/copy-overwrite/.versionrc +42 -0
- package/typescript/copy-overwrite/.yamllint +20 -0
- package/typescript/copy-overwrite/commitlint.config.js +11 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/README.md +149 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/__tests__/enforce-statement-order.test.js +468 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/index.js +23 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/package.json +10 -0
- package/typescript/copy-overwrite/eslint-plugin-code-organization/rules/enforce-statement-order.js +157 -0
- package/typescript/copy-overwrite/eslint.config.mjs +390 -0
- package/typescript/copy-overwrite/eslint.ignore.config.json +57 -0
- package/typescript/copy-overwrite/eslint.thresholds.config.json +5 -0
- package/typescript/github-rulesets/base.json +106 -0
- package/typescript/merge/.claude/settings.json +28 -0
- package/typescript/merge/package.json +71 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
# Persistence Patterns Reference
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This reference covers patterns for persisting reactive variables to AsyncStorage, ensuring that local state survives app restarts while maintaining reactive UI updates.
|
|
6
|
+
|
|
7
|
+
## Why Persistence is Needed
|
|
8
|
+
|
|
9
|
+
Reactive variables are in-memory only. When the app restarts:
|
|
10
|
+
|
|
11
|
+
- All reactive variable values reset to their defaults
|
|
12
|
+
- User preferences are lost
|
|
13
|
+
- Feature flag states reset
|
|
14
|
+
- Filter selections disappear
|
|
15
|
+
|
|
16
|
+
Persistence bridges this gap by saving state to AsyncStorage and restoring it on app launch.
|
|
17
|
+
|
|
18
|
+
## Pattern 1: Immediate Persistence
|
|
19
|
+
|
|
20
|
+
Update storage immediately on every change. Best for critical user preferences that must never be lost.
|
|
21
|
+
|
|
22
|
+
### Implementation
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
26
|
+
import { makeVar, useReactiveVar } from "@apollo/client";
|
|
27
|
+
import { useCallback, useEffect } from "react";
|
|
28
|
+
|
|
29
|
+
// Types
|
|
30
|
+
interface IUserPreferences {
|
|
31
|
+
readonly theme: "light" | "dark";
|
|
32
|
+
readonly language: string;
|
|
33
|
+
readonly notifications: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Constants
|
|
37
|
+
const STORAGE_KEY = "@whatever:user-preferences";
|
|
38
|
+
const DEFAULT_PREFERENCES: IUserPreferences = {
|
|
39
|
+
theme: "light",
|
|
40
|
+
language: "en",
|
|
41
|
+
notifications: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Reactive Variable
|
|
45
|
+
export const userPreferencesVar =
|
|
46
|
+
makeVar<IUserPreferences>(DEFAULT_PREFERENCES);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Update preferences and immediately persist to storage
|
|
50
|
+
* @param updates - Partial preferences to merge
|
|
51
|
+
*/
|
|
52
|
+
const updatePreferences = async (
|
|
53
|
+
updates: Partial<IUserPreferences>
|
|
54
|
+
): Promise<void> => {
|
|
55
|
+
const current = userPreferencesVar();
|
|
56
|
+
const newPrefs: IUserPreferences = { ...current, ...updates };
|
|
57
|
+
|
|
58
|
+
// Update reactive variable first (immediate UI update)
|
|
59
|
+
userPreferencesVar(newPrefs);
|
|
60
|
+
|
|
61
|
+
// Persist to storage (async, but starts immediately)
|
|
62
|
+
try {
|
|
63
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newPrefs));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
66
|
+
console.error("Failed to persist preferences:", message);
|
|
67
|
+
// Note: UI already updated, storage just failed to persist
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load persisted preferences on app start
|
|
73
|
+
*/
|
|
74
|
+
export const loadUserPreferences = async (): Promise<void> => {
|
|
75
|
+
try {
|
|
76
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
77
|
+
if (stored) {
|
|
78
|
+
const parsed = JSON.parse(stored) as Partial<IUserPreferences>;
|
|
79
|
+
// Merge with defaults to handle missing fields from older versions
|
|
80
|
+
userPreferencesVar({ ...DEFAULT_PREFERENCES, ...parsed });
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
84
|
+
console.error("Failed to load preferences:", message);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Custom hook for consuming and updating preferences
|
|
90
|
+
*/
|
|
91
|
+
export const useUserPreferences = () => {
|
|
92
|
+
const preferences = useReactiveVar(userPreferencesVar);
|
|
93
|
+
|
|
94
|
+
const setTheme = useCallback((theme: "light" | "dark") => {
|
|
95
|
+
updatePreferences({ theme });
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const setLanguage = useCallback((language: string) => {
|
|
99
|
+
updatePreferences({ language });
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
const setNotifications = useCallback((notifications: boolean) => {
|
|
103
|
+
updatePreferences({ notifications });
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
preferences,
|
|
108
|
+
setTheme,
|
|
109
|
+
setLanguage,
|
|
110
|
+
setNotifications,
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### App Initialization
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// In root layout or app entry
|
|
119
|
+
import { useEffect } from "react";
|
|
120
|
+
import { loadUserPreferences } from "@/features/settings/stores/userPreferences";
|
|
121
|
+
|
|
122
|
+
export default function RootLayout() {
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
loadUserPreferences();
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
// ... app content
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### When to Use
|
|
134
|
+
|
|
135
|
+
- User preferences that are critical
|
|
136
|
+
- Settings that affect app behavior immediately
|
|
137
|
+
- Data that users expect to persist always
|
|
138
|
+
|
|
139
|
+
### Trade-offs
|
|
140
|
+
|
|
141
|
+
- **Pro**: Data is always persisted, minimal data loss risk
|
|
142
|
+
- **Con**: More storage operations, potential performance impact with frequent changes
|
|
143
|
+
|
|
144
|
+
## Pattern 2: AppState-Based Persistence
|
|
145
|
+
|
|
146
|
+
Save to storage when app goes to background. Better for non-critical state that changes frequently.
|
|
147
|
+
|
|
148
|
+
### Implementation
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
152
|
+
import { makeVar, useReactiveVar } from "@apollo/client";
|
|
153
|
+
import { useEffect, useCallback } from "react";
|
|
154
|
+
import { AppState, AppStateStatus } from "react-native";
|
|
155
|
+
|
|
156
|
+
// Types
|
|
157
|
+
interface IFilterState {
|
|
158
|
+
readonly minAge: number;
|
|
159
|
+
readonly maxAge: number;
|
|
160
|
+
readonly positions: readonly string[];
|
|
161
|
+
readonly teams: readonly string[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Constants
|
|
165
|
+
const STORAGE_KEY = "@whatever:filter-state";
|
|
166
|
+
const DEFAULT_FILTERS: IFilterState = {
|
|
167
|
+
minAge: 18,
|
|
168
|
+
maxAge: 40,
|
|
169
|
+
positions: [],
|
|
170
|
+
teams: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Reactive Variable
|
|
174
|
+
export const filterStateVar = makeVar<IFilterState>(DEFAULT_FILTERS);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Load persisted filter state on app start
|
|
178
|
+
*/
|
|
179
|
+
const loadFilterState = async (): Promise<void> => {
|
|
180
|
+
try {
|
|
181
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
182
|
+
if (stored) {
|
|
183
|
+
const parsed = JSON.parse(stored) as Partial<IFilterState>;
|
|
184
|
+
filterStateVar({ ...DEFAULT_FILTERS, ...parsed });
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
188
|
+
console.error("Failed to load filter state:", message);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Save filter state to storage
|
|
194
|
+
*/
|
|
195
|
+
const saveFilterState = async (): Promise<void> => {
|
|
196
|
+
try {
|
|
197
|
+
const current = filterStateVar();
|
|
198
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
201
|
+
console.error("Failed to save filter state:", message);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Hook to set up persistence lifecycle
|
|
207
|
+
* Call once in a top-level component
|
|
208
|
+
*/
|
|
209
|
+
export const useFilterStatePersistence = (): void => {
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
// Load on mount
|
|
212
|
+
loadFilterState();
|
|
213
|
+
|
|
214
|
+
// Save on background/inactive
|
|
215
|
+
const handleAppStateChange = (nextAppState: AppStateStatus): void => {
|
|
216
|
+
if (nextAppState === "background" || nextAppState === "inactive") {
|
|
217
|
+
saveFilterState();
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const subscription = AppState.addEventListener(
|
|
222
|
+
"change",
|
|
223
|
+
handleAppStateChange
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return () => {
|
|
227
|
+
// Save on unmount as well
|
|
228
|
+
saveFilterState();
|
|
229
|
+
subscription.remove();
|
|
230
|
+
};
|
|
231
|
+
}, []);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Custom hook for consuming and updating filters
|
|
236
|
+
*/
|
|
237
|
+
export const useFilterState = () => {
|
|
238
|
+
const filters = useReactiveVar(filterStateVar);
|
|
239
|
+
|
|
240
|
+
const setMinAge = useCallback((minAge: number) => {
|
|
241
|
+
filterStateVar({ ...filterStateVar(), minAge });
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const setMaxAge = useCallback((maxAge: number) => {
|
|
245
|
+
filterStateVar({ ...filterStateVar(), maxAge });
|
|
246
|
+
}, []);
|
|
247
|
+
|
|
248
|
+
const togglePosition = useCallback((position: string) => {
|
|
249
|
+
const current = filterStateVar();
|
|
250
|
+
const positions = current.positions.includes(position)
|
|
251
|
+
? current.positions.filter(p => p !== position)
|
|
252
|
+
: [...current.positions, position];
|
|
253
|
+
filterStateVar({ ...current, positions });
|
|
254
|
+
}, []);
|
|
255
|
+
|
|
256
|
+
const resetFilters = useCallback(() => {
|
|
257
|
+
filterStateVar(DEFAULT_FILTERS);
|
|
258
|
+
}, []);
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
filters,
|
|
262
|
+
setMinAge,
|
|
263
|
+
setMaxAge,
|
|
264
|
+
togglePosition,
|
|
265
|
+
resetFilters,
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### When to Use
|
|
271
|
+
|
|
272
|
+
- Filter states and UI preferences
|
|
273
|
+
- Data that changes frequently during a session
|
|
274
|
+
- Non-critical state where occasional loss is acceptable
|
|
275
|
+
|
|
276
|
+
### Trade-offs
|
|
277
|
+
|
|
278
|
+
- **Pro**: Fewer storage operations, better performance for frequently changing data
|
|
279
|
+
- **Con**: Risk of data loss if app crashes before backgrounding
|
|
280
|
+
|
|
281
|
+
## Pattern 3: Debounced Persistence
|
|
282
|
+
|
|
283
|
+
Persist after a delay following the last change. Good for data that changes in bursts.
|
|
284
|
+
|
|
285
|
+
### Implementation
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
289
|
+
import { makeVar, useReactiveVar } from "@apollo/client";
|
|
290
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
291
|
+
|
|
292
|
+
// Types
|
|
293
|
+
interface ISearchHistory {
|
|
294
|
+
readonly queries: readonly string[];
|
|
295
|
+
readonly lastUpdated: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Constants
|
|
299
|
+
const STORAGE_KEY = "@whatever:search-history";
|
|
300
|
+
const DEBOUNCE_MS = 1000;
|
|
301
|
+
const MAX_HISTORY_ITEMS = 20;
|
|
302
|
+
const DEFAULT_HISTORY: ISearchHistory = {
|
|
303
|
+
queries: [],
|
|
304
|
+
lastUpdated: new Date().toISOString(),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Reactive Variable
|
|
308
|
+
export const searchHistoryVar = makeVar<ISearchHistory>(DEFAULT_HISTORY);
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Custom hook with debounced persistence
|
|
312
|
+
*/
|
|
313
|
+
export const useSearchHistory = () => {
|
|
314
|
+
const history = useReactiveVar(searchHistoryVar);
|
|
315
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
316
|
+
|
|
317
|
+
// Load on mount
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
const loadHistory = async () => {
|
|
320
|
+
try {
|
|
321
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
322
|
+
if (stored) {
|
|
323
|
+
const parsed = JSON.parse(stored) as ISearchHistory;
|
|
324
|
+
searchHistoryVar(parsed);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("Failed to load search history:", error);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
loadHistory();
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
333
|
+
// Debounced save function
|
|
334
|
+
const scheduleSave = useCallback(() => {
|
|
335
|
+
if (timeoutRef.current) {
|
|
336
|
+
clearTimeout(timeoutRef.current);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
timeoutRef.current = setTimeout(async () => {
|
|
340
|
+
try {
|
|
341
|
+
const current = searchHistoryVar();
|
|
342
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(current));
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error("Failed to save search history:", error);
|
|
345
|
+
}
|
|
346
|
+
}, DEBOUNCE_MS);
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
// Cleanup on unmount
|
|
350
|
+
useEffect(() => {
|
|
351
|
+
return () => {
|
|
352
|
+
if (timeoutRef.current) {
|
|
353
|
+
clearTimeout(timeoutRef.current);
|
|
354
|
+
// Final save on unmount
|
|
355
|
+
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(searchHistoryVar()));
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}, []);
|
|
359
|
+
|
|
360
|
+
const addQuery = useCallback(
|
|
361
|
+
(query: string) => {
|
|
362
|
+
const current = searchHistoryVar();
|
|
363
|
+
const trimmedQuery = query.trim();
|
|
364
|
+
|
|
365
|
+
if (!trimmedQuery) return;
|
|
366
|
+
|
|
367
|
+
// Remove duplicate if exists, add to front
|
|
368
|
+
const filtered = current.queries.filter(q => q !== trimmedQuery);
|
|
369
|
+
const queries = [trimmedQuery, ...filtered].slice(0, MAX_HISTORY_ITEMS);
|
|
370
|
+
|
|
371
|
+
searchHistoryVar({
|
|
372
|
+
queries,
|
|
373
|
+
lastUpdated: new Date().toISOString(),
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
scheduleSave();
|
|
377
|
+
},
|
|
378
|
+
[scheduleSave]
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const clearHistory = useCallback(() => {
|
|
382
|
+
searchHistoryVar(DEFAULT_HISTORY);
|
|
383
|
+
scheduleSave();
|
|
384
|
+
}, [scheduleSave]);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
queries: history.queries,
|
|
388
|
+
addQuery,
|
|
389
|
+
clearHistory,
|
|
390
|
+
};
|
|
391
|
+
};
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### When to Use
|
|
395
|
+
|
|
396
|
+
- Search history or recent items
|
|
397
|
+
- Form draft auto-save
|
|
398
|
+
- Any data with burst updates
|
|
399
|
+
|
|
400
|
+
### Trade-offs
|
|
401
|
+
|
|
402
|
+
- **Pro**: Optimal balance between persistence and performance
|
|
403
|
+
- **Con**: More complex implementation, potential for lost updates during debounce window
|
|
404
|
+
|
|
405
|
+
## Pattern 4: Context Provider with Persistence
|
|
406
|
+
|
|
407
|
+
For state that needs to be available throughout the app with persistence.
|
|
408
|
+
|
|
409
|
+
### Implementation
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
413
|
+
import React, {
|
|
414
|
+
createContext,
|
|
415
|
+
useContext,
|
|
416
|
+
useState,
|
|
417
|
+
useEffect,
|
|
418
|
+
useCallback,
|
|
419
|
+
useMemo,
|
|
420
|
+
ReactNode,
|
|
421
|
+
} from "react";
|
|
422
|
+
|
|
423
|
+
// Types
|
|
424
|
+
interface IFeatureFlags {
|
|
425
|
+
readonly darkModeEnabled: boolean;
|
|
426
|
+
readonly betaFeaturesEnabled: boolean;
|
|
427
|
+
readonly debugModeEnabled: boolean;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
interface IFeatureFlagsContext extends IFeatureFlags {
|
|
431
|
+
readonly isLoaded: boolean;
|
|
432
|
+
readonly setDarkModeEnabled: (enabled: boolean) => void;
|
|
433
|
+
readonly setBetaFeaturesEnabled: (enabled: boolean) => void;
|
|
434
|
+
readonly setDebugModeEnabled: (enabled: boolean) => void;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Constants
|
|
438
|
+
const STORAGE_KEY = "@whatever:feature-flags";
|
|
439
|
+
const DEFAULT_FLAGS: IFeatureFlags = {
|
|
440
|
+
darkModeEnabled: false,
|
|
441
|
+
betaFeaturesEnabled: false,
|
|
442
|
+
debugModeEnabled: false,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Context
|
|
446
|
+
const FeatureFlagsContext = createContext<IFeatureFlagsContext | null>(null);
|
|
447
|
+
|
|
448
|
+
interface ProviderProps {
|
|
449
|
+
readonly children: ReactNode;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Feature flags provider with AsyncStorage persistence
|
|
454
|
+
*/
|
|
455
|
+
export const FeatureFlagsProvider = ({ children }: ProviderProps) => {
|
|
456
|
+
const [flags, setFlags] = useState<IFeatureFlags>(DEFAULT_FLAGS);
|
|
457
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
458
|
+
|
|
459
|
+
// Load from storage on mount
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
const loadFlags = async () => {
|
|
462
|
+
try {
|
|
463
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
464
|
+
if (stored) {
|
|
465
|
+
const parsed = JSON.parse(stored) as Partial<IFeatureFlags>;
|
|
466
|
+
setFlags(prev => ({ ...prev, ...parsed }));
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error("Failed to load feature flags:", error);
|
|
470
|
+
} finally {
|
|
471
|
+
setIsLoaded(true);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
loadFlags();
|
|
475
|
+
}, []);
|
|
476
|
+
|
|
477
|
+
// Save helper
|
|
478
|
+
const saveFlags = useCallback(async (newFlags: IFeatureFlags) => {
|
|
479
|
+
try {
|
|
480
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newFlags));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
console.error("Failed to save feature flags:", error);
|
|
483
|
+
}
|
|
484
|
+
}, []);
|
|
485
|
+
|
|
486
|
+
// Individual setters
|
|
487
|
+
const setDarkModeEnabled = useCallback(
|
|
488
|
+
(enabled: boolean) => {
|
|
489
|
+
setFlags(prev => {
|
|
490
|
+
const newFlags = { ...prev, darkModeEnabled: enabled };
|
|
491
|
+
saveFlags(newFlags);
|
|
492
|
+
return newFlags;
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
[saveFlags]
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
const setBetaFeaturesEnabled = useCallback(
|
|
499
|
+
(enabled: boolean) => {
|
|
500
|
+
setFlags(prev => {
|
|
501
|
+
const newFlags = { ...prev, betaFeaturesEnabled: enabled };
|
|
502
|
+
saveFlags(newFlags);
|
|
503
|
+
return newFlags;
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
[saveFlags]
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const setDebugModeEnabled = useCallback(
|
|
510
|
+
(enabled: boolean) => {
|
|
511
|
+
setFlags(prev => {
|
|
512
|
+
const newFlags = { ...prev, debugModeEnabled: enabled };
|
|
513
|
+
saveFlags(newFlags);
|
|
514
|
+
return newFlags;
|
|
515
|
+
});
|
|
516
|
+
},
|
|
517
|
+
[saveFlags]
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
const contextValue = useMemo(
|
|
521
|
+
(): IFeatureFlagsContext => ({
|
|
522
|
+
...flags,
|
|
523
|
+
isLoaded,
|
|
524
|
+
setDarkModeEnabled,
|
|
525
|
+
setBetaFeaturesEnabled,
|
|
526
|
+
setDebugModeEnabled,
|
|
527
|
+
}),
|
|
528
|
+
[flags, isLoaded, setDarkModeEnabled, setBetaFeaturesEnabled, setDebugModeEnabled]
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
return (
|
|
532
|
+
<FeatureFlagsContext.Provider value={contextValue}>
|
|
533
|
+
{children}
|
|
534
|
+
</FeatureFlagsContext.Provider>
|
|
535
|
+
);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Hook to consume feature flags
|
|
540
|
+
* @throws Error if used outside provider
|
|
541
|
+
*/
|
|
542
|
+
export const useFeatureFlags = (): IFeatureFlagsContext => {
|
|
543
|
+
const context = useContext(FeatureFlagsContext);
|
|
544
|
+
if (!context) {
|
|
545
|
+
throw new Error("useFeatureFlags must be used within FeatureFlagsProvider");
|
|
546
|
+
}
|
|
547
|
+
return context;
|
|
548
|
+
};
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Usage
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// In app root
|
|
555
|
+
export default function RootLayout() {
|
|
556
|
+
return (
|
|
557
|
+
<FeatureFlagsProvider>
|
|
558
|
+
<App />
|
|
559
|
+
</FeatureFlagsProvider>
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// In any component
|
|
564
|
+
const SettingsScreen = () => {
|
|
565
|
+
const { darkModeEnabled, setDarkModeEnabled, isLoaded } = useFeatureFlags();
|
|
566
|
+
|
|
567
|
+
if (!isLoaded) {
|
|
568
|
+
return <LoadingSpinner />;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return (
|
|
572
|
+
<Switch
|
|
573
|
+
value={darkModeEnabled}
|
|
574
|
+
onValueChange={setDarkModeEnabled}
|
|
575
|
+
/>
|
|
576
|
+
);
|
|
577
|
+
};
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### When to Use
|
|
581
|
+
|
|
582
|
+
- Feature flags
|
|
583
|
+
- App-wide settings
|
|
584
|
+
- State that needs to be accessed before Apollo Client initializes
|
|
585
|
+
|
|
586
|
+
### Trade-offs
|
|
587
|
+
|
|
588
|
+
- **Pro**: Works without Apollo Client, clear loading state
|
|
589
|
+
- **Con**: More boilerplate, separate from Apollo ecosystem
|
|
590
|
+
|
|
591
|
+
## Choosing the Right Pattern
|
|
592
|
+
|
|
593
|
+
| Pattern | Best For | Performance | Reliability |
|
|
594
|
+
| ---------------- | ------------------ | ------------------- | ----------- |
|
|
595
|
+
| Immediate | Critical user data | Lower (more writes) | Highest |
|
|
596
|
+
| AppState-based | Frequent changes | Higher | Medium |
|
|
597
|
+
| Debounced | Burst updates | Highest | Medium |
|
|
598
|
+
| Context Provider | Non-Apollo state | Medium | High |
|
|
599
|
+
|
|
600
|
+
## Combined Pattern Example
|
|
601
|
+
|
|
602
|
+
For complex features, combine patterns:
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// stores/playerFilters.ts
|
|
606
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
607
|
+
import { makeVar, useReactiveVar } from "@apollo/client";
|
|
608
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
609
|
+
import { AppState, AppStateStatus } from "react-native";
|
|
610
|
+
|
|
611
|
+
interface IPlayerFilters {
|
|
612
|
+
readonly minAge: number;
|
|
613
|
+
readonly maxAge: number;
|
|
614
|
+
readonly positions: readonly string[];
|
|
615
|
+
readonly searchQuery: string;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const STORAGE_KEY = "@whatever:player-filters";
|
|
619
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
620
|
+
const DEFAULT_FILTERS: IPlayerFilters = {
|
|
621
|
+
minAge: 18,
|
|
622
|
+
maxAge: 45,
|
|
623
|
+
positions: [],
|
|
624
|
+
searchQuery: "",
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
export const playerFiltersVar = makeVar<IPlayerFilters>(DEFAULT_FILTERS);
|
|
628
|
+
|
|
629
|
+
export const usePlayerFilters = () => {
|
|
630
|
+
const filters = useReactiveVar(playerFiltersVar);
|
|
631
|
+
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
632
|
+
|
|
633
|
+
// Load on mount
|
|
634
|
+
useEffect(() => {
|
|
635
|
+
const load = async () => {
|
|
636
|
+
try {
|
|
637
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
638
|
+
if (stored) {
|
|
639
|
+
const parsed = JSON.parse(stored) as Partial<IPlayerFilters>;
|
|
640
|
+
playerFiltersVar({ ...DEFAULT_FILTERS, ...parsed });
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
console.error("Failed to load filters:", error);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
load();
|
|
647
|
+
}, []);
|
|
648
|
+
|
|
649
|
+
// Save on background (for non-search fields)
|
|
650
|
+
useEffect(() => {
|
|
651
|
+
const handleAppState = (state: AppStateStatus) => {
|
|
652
|
+
if (state === "background") {
|
|
653
|
+
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(playerFiltersVar()));
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const sub = AppState.addEventListener("change", handleAppState);
|
|
658
|
+
return () => sub.remove();
|
|
659
|
+
}, []);
|
|
660
|
+
|
|
661
|
+
// Immediate save for critical changes (positions)
|
|
662
|
+
const togglePosition = useCallback((position: string) => {
|
|
663
|
+
const current = playerFiltersVar();
|
|
664
|
+
const positions = current.positions.includes(position)
|
|
665
|
+
? current.positions.filter(p => p !== position)
|
|
666
|
+
: [...current.positions, position];
|
|
667
|
+
|
|
668
|
+
const newFilters = { ...current, positions };
|
|
669
|
+
playerFiltersVar(newFilters);
|
|
670
|
+
|
|
671
|
+
// Immediate persist for position changes
|
|
672
|
+
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newFilters));
|
|
673
|
+
}, []);
|
|
674
|
+
|
|
675
|
+
// Debounced for search query
|
|
676
|
+
const setSearchQuery = useCallback((searchQuery: string) => {
|
|
677
|
+
playerFiltersVar({ ...playerFiltersVar(), searchQuery });
|
|
678
|
+
|
|
679
|
+
if (searchTimeoutRef.current) {
|
|
680
|
+
clearTimeout(searchTimeoutRef.current);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
searchTimeoutRef.current = setTimeout(() => {
|
|
684
|
+
AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(playerFiltersVar()));
|
|
685
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
686
|
+
}, []);
|
|
687
|
+
|
|
688
|
+
// Standard setters (saved on background)
|
|
689
|
+
const setMinAge = useCallback((minAge: number) => {
|
|
690
|
+
playerFiltersVar({ ...playerFiltersVar(), minAge });
|
|
691
|
+
}, []);
|
|
692
|
+
|
|
693
|
+
const setMaxAge = useCallback((maxAge: number) => {
|
|
694
|
+
playerFiltersVar({ ...playerFiltersVar(), maxAge });
|
|
695
|
+
}, []);
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
filters,
|
|
699
|
+
setMinAge,
|
|
700
|
+
setMaxAge,
|
|
701
|
+
togglePosition,
|
|
702
|
+
setSearchQuery,
|
|
703
|
+
};
|
|
704
|
+
};
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
This combined approach uses:
|
|
708
|
+
|
|
709
|
+
- **AppState persistence** for age filters (saved on background)
|
|
710
|
+
- **Immediate persistence** for position toggles (critical user selections)
|
|
711
|
+
- **Debounced persistence** for search query (changes with every keystroke)
|