All files / session features.ts

87.5% Statements 7/8
50% Branches 3/6
100% Functions 3/3
100% Lines 7/7

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                                                                    1x                               1x                                                     2x 2x         2x                     1x 1x                
import { deepMergeRecord } from '../internal/deepMergeRecord.js';
 
/** Runtime-configurable session features. Nested objects deliberately mirror
 * the profile JSON so the normal profile merge and provenance rules apply. */
export type MoveBackTriggerSpec = {
  kind: 'moveBack';
  windowMs: number;
};
 
/** A shake gesture over the calibrated gyro stream. Params default to
 * `ShakeTrigger.defaults`; a profile or host overrides only what it needs. */
export type ShakeTriggerSpec = {
  kind: 'shake';
  minStepAngleDeg?: number;
  minSteps?: number;
  minReversals?: number;
  maxSampleGapMs?: number;
  burstWindowMs?: number;
  faceGuardMs?: number;
  cooldownMs?: number;
};
 
/** One configured app-level gesture detector. */
export type CustomTriggerSpec = MoveBackTriggerSpec | ShakeTriggerSpec;
 
/** Complete runtime feature configuration after profile resolution. */
export type SessionFeatures = {
  stabilizer: {
    enabled: boolean;
    radiusDeg: number;
    snapDeg: number;
    velocityMax: number;
    hysteresis: { enabled: boolean; marginDeg: number };
    drift: { enabled: boolean; degPerSec: number };
  };
  regrip: {
    enabled: boolean;
  };
  customTrigger: { enabled: boolean; triggers: CustomTriggerSpec[] };
};
 
/** Partial profile or host override for session features. */
export type SessionFeaturesPatch = {
  stabilizer?: {
    enabled?: boolean;
    radiusDeg?: number;
    snapDeg?: number;
    velocityMax?: number;
    hysteresis?: { enabled?: boolean; marginDeg?: number };
    drift?: { enabled?: boolean; degPerSec?: number };
  };
  regrip?: {
    enabled?: boolean;
  };
  customTrigger?: { enabled?: boolean; triggers?: CustomTriggerSpec[] };
};
 
/** Baseline feature configuration used when no profile overrides it. */
export const defaultSessionFeatures: SessionFeatures = {
  stabilizer: {
    enabled: true,
    radiusDeg: 35,
    snapDeg: 4,
    velocityMax: 2.5,
    hysteresis: { enabled: true, marginDeg: 6 },
    drift: { enabled: true, degPerSec: 2 },
  },
  // Virtual regrips were opt-in before the feature model.
  regrip: { enabled: false },
  // Move-back gestures were always enabled before the feature model.
  customTrigger: { enabled: true, triggers: [{ kind: 'moveBack', windowMs: 300 }] },
};
 
/** Useful explicit starting points for hosts which do not use profiles. */
export const featurePresets: Record<'all' | 'minimal' | 'none', SessionFeatures> = {
  all: {
    ...defaultSessionFeatures,
    regrip: { enabled: true },
    customTrigger: {
      enabled: true,
      triggers: [{ kind: 'moveBack', windowMs: 300 }, { kind: 'shake' }],
    },
  },
  minimal: {
    ...defaultSessionFeatures,
    regrip: { ...defaultSessionFeatures.regrip, enabled: false },
    customTrigger: { ...defaultSessionFeatures.customTrigger, enabled: false },
  },
  none: {
    ...defaultSessionFeatures,
    stabilizer: { ...defaultSessionFeatures.stabilizer, enabled: false },
    regrip: { ...defaultSessionFeatures.regrip, enabled: false },
    customTrigger: { ...defaultSessionFeatures.customTrigger, enabled: false },
  },
};
 
/** Deeply merge a feature patch. Arrays (the trigger list) replace wholesale. */
export function mergeSessionFeatures(
  base: SessionFeatures,
  patch: SessionFeaturesPatch | undefined,
): SessionFeatures {
  Iif (!patch) return base;
  return deepMergeRecord(base, patch) as SessionFeatures;
}
 
/** Resolve profile feature overrides onto the baseline configuration. */
export function resolveSessionFeatures(profileFeatures?: SessionFeaturesPatch): SessionFeatures {
  return mergeSessionFeatures(defaultSessionFeatures, profileFeatures);
}
 
/** Adapter for the existing domain stabilizer configuration. */
export function stabilizerConfig(features: SessionFeatures): {
  radiusDeg: number;
  snapDeg: number;
  hysteresisDeg: number;
  velocityMax: number;
  driftDegPerSec: number;
} {
  const { stabilizer } = features;
  return {
    radiusDeg: stabilizer.radiusDeg,
    snapDeg: stabilizer.snapDeg,
    hysteresisDeg: stabilizer.hysteresis.enabled ? stabilizer.hysteresis.marginDeg : 0,
    velocityMax: stabilizer.velocityMax,
    driftDegPerSec: stabilizer.drift.enabled ? stabilizer.drift.degPerSec : 0,
  };
}