@tastic/profile 0.10.0 → 0.11.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/README.md CHANGED
@@ -244,13 +244,18 @@ so a host app plugs the reducer(s) into whatever store it already has:
244
244
  - **`createProfileSelectionSlice<TSeat>(namespace, defaultState)`** — a factory for the *other*
245
245
  per-seat piece of state: which profile id each seat currently has selected (parallel to
246
246
  `createProfileExtensionSlice` above, but keyed by seat instead of by profile id). Returns a
247
- `{ actions: { select, clearProfile }, reducer }` pair over a plain `Record<TSeat, string | null>`.
247
+ `{ actions: { select, clearProfile, pruneToRoster }, reducer }` pair over a plain
248
+ `Record<TSeat, string | null>`.
248
249
  Unlike `createProfileExtensionSlice`, the seat set can't be derived from `TSeat` alone (types
249
250
  don't exist at runtime), so callers pass their own `defaultState` — the same literal object (e.g.
250
251
  `{ 1: null, 2: null }`) every app already declares today. `select({ seat, profileId })` sets one
251
252
  seat's selection directly; `clearProfile(profileId)` walks every seat and nulls out any that
252
253
  currently points at that id — call it when a profile is deleted so no seat is left referencing a
253
- ghost id. Always local, like the guest/CPU/override colors below — never part of the shared
254
+ ghost id. `pruneToRoster(ids)` nulls every seat whose id is no longer in the given roster; dispatch
255
+ it after both `profilesActions.setAll` paths (the initial `resolveInitialProfiles` reconcile and
256
+ `onRemoteChange`), since a sibling app can delete a profile without this app's own delete ever
257
+ running, and it returns the same state object when nothing changes.
258
+ Always local, like the guest/CPU/override colors below, and never part of the shared
254
259
  App Group roster, since who's selected on *this* device is naturally per-app, per-device state.
255
260
 
256
261
  ## Muted profile colors (`ProfileColorProvider`)
package/dist/index.d.mts CHANGED
@@ -141,6 +141,7 @@ interface SelectProfilePayload<TSeat> {
141
141
  }
142
142
  interface ProfileSelectionActions<TSeat> {
143
143
  clearProfile: ActionCreator<string>;
144
+ pruneToRoster: ActionCreator<string[]>;
144
145
  select: ActionCreator<SelectProfilePayload<TSeat>>;
145
146
  }
146
147
  type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
package/dist/index.d.ts CHANGED
@@ -141,6 +141,7 @@ interface SelectProfilePayload<TSeat> {
141
141
  }
142
142
  interface ProfileSelectionActions<TSeat> {
143
143
  clearProfile: ActionCreator<string>;
144
+ pruneToRoster: ActionCreator<string[]>;
144
145
  select: ActionCreator<SelectProfilePayload<TSeat>>;
145
146
  }
146
147
  type ProfileSelectionState<TSeat extends string | number> = Record<TSeat, string | null>;
package/dist/index.js CHANGED
@@ -607,6 +607,7 @@ var REHYDRATE = "persist/REHYDRATE";
607
607
  function createProfileSelectionSlice(namespace, defaultState, mountKey = "profileSelection") {
608
608
  const select = createActionCreator(`profileSelection/${namespace}/select`);
609
609
  const clearProfile = createActionCreator(`profileSelection/${namespace}/clearProfile`);
610
+ const pruneToRoster = createActionCreator(`profileSelection/${namespace}/pruneToRoster`);
610
611
  function reducer(state = defaultState, action) {
611
612
  if (action.type === REHYDRATE) {
612
613
  const persisted = action.payload?.[mountKey];
@@ -620,9 +621,22 @@ function createProfileSelectionSlice(namespace, defaultState, mountKey = "profil
620
621
  }
621
622
  return next;
622
623
  }
624
+ if (pruneToRoster.match(action)) {
625
+ const roster = new Set(action.payload);
626
+ const next = { ...state };
627
+ let changed = false;
628
+ for (const seat of Object.keys(state)) {
629
+ const profileId = state[seat];
630
+ if (profileId !== null && !roster.has(profileId)) {
631
+ next[seat] = null;
632
+ changed = true;
633
+ }
634
+ }
635
+ return changed ? next : state;
636
+ }
623
637
  return state;
624
638
  }
625
- return { actions: { clearProfile, select }, reducer };
639
+ return { actions: { clearProfile, pruneToRoster, select }, reducer };
626
640
  }
627
641
 
628
642
  // src/redux/profilesSlice.ts
package/dist/index.mjs CHANGED
@@ -566,6 +566,7 @@ var REHYDRATE = "persist/REHYDRATE";
566
566
  function createProfileSelectionSlice(namespace, defaultState, mountKey = "profileSelection") {
567
567
  const select = createActionCreator(`profileSelection/${namespace}/select`);
568
568
  const clearProfile = createActionCreator(`profileSelection/${namespace}/clearProfile`);
569
+ const pruneToRoster = createActionCreator(`profileSelection/${namespace}/pruneToRoster`);
569
570
  function reducer(state = defaultState, action) {
570
571
  if (action.type === REHYDRATE) {
571
572
  const persisted = action.payload?.[mountKey];
@@ -579,9 +580,22 @@ function createProfileSelectionSlice(namespace, defaultState, mountKey = "profil
579
580
  }
580
581
  return next;
581
582
  }
583
+ if (pruneToRoster.match(action)) {
584
+ const roster = new Set(action.payload);
585
+ const next = { ...state };
586
+ let changed = false;
587
+ for (const seat of Object.keys(state)) {
588
+ const profileId = state[seat];
589
+ if (profileId !== null && !roster.has(profileId)) {
590
+ next[seat] = null;
591
+ changed = true;
592
+ }
593
+ }
594
+ return changed ? next : state;
595
+ }
582
596
  return state;
583
597
  }
584
- return { actions: { clearProfile, select }, reducer };
598
+ return { actions: { clearProfile, pruneToRoster, select }, reducer };
585
599
  }
586
600
 
587
601
  // src/redux/profilesSlice.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tastic/profile",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Player profile identity for local-multiplayer React Native games — a saved name/color/tag a player selects per seat, with a picker, an inline-editable roster screen, and a reusable identity badge, deliberately blind to any app-specific fields (control scheme, stats) an app layers on top",
5
5
  "keywords": [
6
6
  "react-native",
@@ -14,6 +14,7 @@ export interface SelectProfilePayload<TSeat> {
14
14
 
15
15
  export interface ProfileSelectionActions<TSeat> {
16
16
  clearProfile: ActionCreator<string>
17
+ pruneToRoster: ActionCreator<string[]>
17
18
  select: ActionCreator<SelectProfilePayload<TSeat>>
18
19
  }
19
20
 
@@ -50,9 +51,15 @@ export interface ProfileSelectionSlice<TSeat extends string | number> {
50
51
  // from inside its own reducer function — only worth overriding if some future consumer ever mounts
51
52
  // this under a different key. Note `namespace` above is a different thing: it's the app's own name
52
53
  // (e.g. 'airhockey', 'boxhockey'), used only to prefix action types, not the mount key.
54
+ //
55
+ // `pruneToRoster(ids)` nulls every seat whose profile id is no longer in the given roster. Call it
56
+ // after any profilesActions.setAll (initial reconcile, onRemoteChange), since a sibling app can
57
+ // delete a profile without this app's own deleteProfile ever running. It returns the same state
58
+ // object when nothing changes, so dispatching it on every foreground resume costs no re-render.
53
59
  export function createProfileSelectionSlice<TSeat extends string | number>(namespace: string, defaultState: ProfileSelectionState<TSeat>, mountKey = 'profileSelection'): ProfileSelectionSlice<TSeat> {
54
60
  const select = createActionCreator<SelectProfilePayload<TSeat>>(`profileSelection/${namespace}/select`)
55
61
  const clearProfile = createActionCreator<string>(`profileSelection/${namespace}/clearProfile`)
62
+ const pruneToRoster = createActionCreator<string[]>(`profileSelection/${namespace}/pruneToRoster`)
56
63
 
57
64
  function reducer(state: ProfileSelectionState<TSeat> = defaultState, action: { type: string; payload?: unknown }): ProfileSelectionState<TSeat> {
58
65
  // redux-persist's own default stateReconciler (autoMergeLevel1) HARD-REPLACES this slice's
@@ -74,8 +81,21 @@ export function createProfileSelectionSlice<TSeat extends string | number>(names
74
81
  }
75
82
  return next
76
83
  }
84
+ if (pruneToRoster.match(action)) {
85
+ const roster = new Set(action.payload)
86
+ const next = { ...state }
87
+ let changed = false
88
+ for (const seat of Object.keys(state) as TSeat[]) {
89
+ const profileId = state[seat]
90
+ if (profileId !== null && !roster.has(profileId)) {
91
+ next[seat] = null
92
+ changed = true
93
+ }
94
+ }
95
+ return changed ? next : state
96
+ }
77
97
  return state
78
98
  }
79
99
 
80
- return { actions: { clearProfile, select }, reducer }
100
+ return { actions: { clearProfile, pruneToRoster, select }, reducer }
81
101
  }