Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/content/en/api-reference/app/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,16 @@ const bootstrap = await dmn.app.bootstrap();
console.log(bootstrap.selectedKeyType); // 'keyboard' | 'mouse'
console.log(bootstrap.settings); // app settings object
console.log(bootstrap.keys.keyboard); // key mapping
console.log(bootstrap.keys.counter); // counter state
console.log(bootstrap.keyCounters); // counter state
console.log(bootstrap.keyCountersSessionId); // counter stream session
console.log(bootstrap.keyCountersRevision); // counter snapshot revision
```

`keyCountersRevision` is a read-only, monotonically increasing watermark for
the returned `keyCounters` snapshot. It is intended for ordering bootstrap
snapshots against live counter updates within the same
`keyCountersSessionId`. A changed session ID starts a new revision sequence.

### `dmn.app.restart(): Promise<void>`

Restarts the app. Settings are preserved.
Expand Down
18 changes: 15 additions & 3 deletions docs/content/en/api-reference/keys/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,27 @@ Subscribes to counter changes.

```typescript
interface CounterEvent {
mode: string;
key: string;
count: number;
sessionId: string;
revision: number;
}

const unsub = dmn.keys.onCounterChanged(({ key, count }) => {
console.log(`${key}: ${count}`);
});
const unsub = dmn.keys.onCounterChanged(
({ mode, key, count, sessionId, revision }) => {
console.log(
`[${mode}] ${key}: ${count} (${sessionId}, revision ${revision})`,
);
},
);
```

`revision` is the monotonically increasing order of the runtime counter
change within `sessionId`. Compare both fields with
`keyCountersSessionId`/`keyCountersRevision` from `dmn.app.bootstrap()` when
reconciling a bootstrap snapshot with live events.

---

## Mode Events
Expand Down
7 changes: 7 additions & 0 deletions docs/content/ko/api-reference/app/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ interface BootstrapPayload {
anchor: string;
};
keyCounters: KeyCounters;
keyCountersSessionId: string;
keyCountersRevision: number;
}
```

`keyCountersRevision`은 반환된 `keyCounters` 스냅샷의 순서를 나타내는 읽기
전용 단조 증가 watermark입니다. 같은 `keyCountersSessionId` 안에서 bootstrap
스냅샷과 실시간 카운터 변경의 선후 관계를 판별할 때 사용하며, 세션 ID가
바뀌면 새로운 revision 순서가 시작됩니다.

```javascript
const bootstrap = await dmn.app.bootstrap();
console.log('현재 모드:', bootstrap.selectedKeyType);
Expand Down
14 changes: 11 additions & 3 deletions docs/content/ko/api-reference/keys/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,19 @@ const unsub = dmn.keys.onModeChanged(({ mode }) => {
개별 키 카운트 변경 이벤트를 구독합니다.

```javascript
const unsub = dmn.keys.onCounterChanged(({ mode, key, count }) => {
console.log(`[${mode}] ${key}: ${count}`);
});
const unsub = dmn.keys.onCounterChanged(
({ mode, key, count, sessionId, revision }) => {
console.log(
`[${mode}] ${key}: ${count} (${sessionId}, revision ${revision})`,
);
},
);
```

`revision`은 `sessionId` 안에서 런타임 카운터 변경의 단조 증가 순서입니다.
bootstrap 스냅샷과 실시간 이벤트를 조정할 때 `dmn.app.bootstrap()`의
`keyCountersSessionId`/`keyCountersRevision`과 함께 비교할 수 있습니다.

### onCountersChanged(listener)

전체 키 카운터 변경 이벤트를 구독합니다.
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2085,6 +2085,8 @@ pub struct BootstrapPayload {
pub active_keys: Vec<String>,
pub overlay: BootstrapOverlayState,
pub key_counters: KeyCounters,
pub key_counters_session_id: String,
pub key_counters_revision: u64,
pub layer_groups: LayerGroups,
pub tab_note_overrides: TabNoteOverrides,
pub tab_css_overrides: TabCssOverrides,
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/services/obs_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ impl ObsBridgeService {
"keys:changed",
"keys:counters",
"keys:counter",
"keys:counters-state",
"keys:mode-changed",
"positions:changed",
"statPositions:changed",
Expand Down
81 changes: 72 additions & 9 deletions src-tauri/src/state/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,23 +591,57 @@ pub(crate) struct AdmittedCounterMutation {
/// 카운터 write lock 내부 전용 이벤트 송신 경계
/// 동기 Rust listener에서 AppState 카운터 API 재진입 금지
pub(crate) trait KeyCounterEventEmitter {
fn emit_key_counters(&self, counters: &KeyCounters) -> Result<()>;
fn emit_key_counter(&self, mode: &str, key: &str, count: u32) -> Result<()>;
fn emit_key_counters(
&self,
counters: &KeyCounters,
session_id: &str,
revision: u64,
) -> Result<()>;
fn emit_key_counter(
&self,
mode: &str,
key: &str,
count: u32,
session_id: &str,
revision: u64,
) -> Result<()>;
}

impl KeyCounterEventEmitter for AppHandle {
fn emit_key_counters(&self, counters: &KeyCounters) -> Result<()> {
fn emit_key_counters(
&self,
counters: &KeyCounters,
session_id: &str,
revision: u64,
) -> Result<()> {
self.emit("keys:counters", counters)?;
self.emit(
"keys:counters-state",
&json!({
"sessionId": session_id,
"revision": revision,
"counters": counters,
}),
)?;
Ok(())
}

fn emit_key_counter(&self, mode: &str, key: &str, count: u32) -> Result<()> {
fn emit_key_counter(
&self,
mode: &str,
key: &str,
count: u32,
session_id: &str,
revision: u64,
) -> Result<()> {
self.emit(
"keys:counter",
&json!({
"mode": mode,
"key": key,
"count": count,
"sessionId": session_id,
"revision": revision,
}),
)?;
Ok(())
Expand Down Expand Up @@ -869,6 +903,8 @@ pub struct AppState {
keyboard_task: RwLock<Option<KeyboardDaemonTask>>,
keyboard_task_generation: AtomicU64,
key_counters: Arc<RwLock<KeyCounters>>,
key_counters_session_id: String,
key_counters_revision: AtomicU64,
counter_history_barrier: Mutex<CounterHistoryBarrierState>,
counter_history_ready: Condvar,
runtime_publication: Mutex<RuntimePublicationState>,
Expand Down Expand Up @@ -945,6 +981,8 @@ impl AppState {
keyboard_task: RwLock::new(None),
keyboard_task_generation: AtomicU64::new(0),
key_counters,
key_counters_session_id: uuid::Uuid::new_v4().simple().to_string(),
key_counters_revision: AtomicU64::new(0),
counter_history_barrier: Mutex::new(CounterHistoryBarrierState::default()),
counter_history_ready: Condvar::new(),
runtime_publication: Mutex::new(RuntimePublicationState::default()),
Expand Down Expand Up @@ -1119,6 +1157,13 @@ impl AppState {
let mut custom_js = state.custom_js.clone();
let _ = custom_js.normalize();
let (current_mode, active_keys) = bootstrap_keyboard_state(&self.keyboard);
let (key_counters, key_counters_revision) = {
let counters = self.key_counters.read();
(
counters.clone(),
self.key_counters_revision.load(Ordering::Relaxed),
)
};
BootstrapPayload {
defaults: DefaultsPayload {
settings: SettingsState::default(),
Expand Down Expand Up @@ -1162,7 +1207,9 @@ impl AppState {
locked: state.overlay_locked,
anchor: state.overlay_resize_anchor.as_str().to_string(),
},
key_counters: self.key_counters.read().clone(),
key_counters,
key_counters_session_id: self.key_counters_session_id.clone(),
key_counters_revision,
layer_groups: state.layer_groups.clone(),
tab_note_overrides: state.tab_note_overrides.clone(),
tab_css_overrides: state.tab_css_overrides.clone(),
Expand Down Expand Up @@ -3406,7 +3453,9 @@ impl AppState {
key,
count
);
let emit_result = emitter.emit_key_counter(mode, key, count);
let revision = self.next_key_counters_revision();
let emit_result =
emitter.emit_key_counter(mode, key, count, &self.key_counters_session_id, revision);
drop(counters);
if let Err(err) = emit_result {
error!("failed to emit keys:counter event: {err}");
Expand All @@ -3423,6 +3472,13 @@ impl AppState {
self.key_counters.read().clone()
}

/// key_counters write lock 보유 중에만 호출 — 스냅샷과 이벤트 revision의 인과 순서 보장
fn next_key_counters_revision(&self) -> u64 {
self.key_counters_revision
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1)
}

pub(crate) fn begin_counter_history_barrier(&self) {
let mut barrier = self.counter_history_barrier.lock();
debug_assert!(!barrier.queueing, "counter history barrier already active");
Expand Down Expand Up @@ -3480,7 +3536,10 @@ impl AppState {
publication.counters_generation.max(publication_generation);
}
if counters_restored || queued_count != 0 {
if let Err(error) = emitter.emit_key_counters(&counters) {
let revision = self.next_key_counters_revision();
if let Err(error) =
emitter.emit_key_counters(&counters, &self.key_counters_session_id, revision)
{
log::error!("failed to emit restored key counters: {error:#}");
}
}
Expand Down Expand Up @@ -3519,7 +3578,9 @@ impl AppState {
let mut publication = self.runtime_publication.lock();
if publication_generation > publication.counters_generation {
*guard = persisted.clone();
let emit_result = emitter.emit_key_counters(&guard);
let revision = self.next_key_counters_revision();
let emit_result =
emitter.emit_key_counters(&guard, &self.key_counters_session_id, revision);
publication.counters_generation = publication_generation;
emit_result.map_err(|error| EditorCommitError::io(error.to_string()))?;
}
Expand Down Expand Up @@ -3687,7 +3748,9 @@ impl AppState {
return Ok(());
}
*counter_guard = counters.clone();
let emit_result = emitter.emit_key_counters(counter_guard);
let revision = self.next_key_counters_revision();
let emit_result =
emitter.emit_key_counters(counter_guard, &self.key_counters_session_id, revision);
publication.counters_generation = generation;
emit_result
}
Expand Down
20 changes: 18 additions & 2 deletions src-tauri/src/state/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4061,7 +4061,12 @@ mod tests {
}

impl KeyCounterEventEmitter for TestCounterEmitter {
fn emit_key_counters(&self, counters: &KeyCounters) -> anyhow::Result<()> {
fn emit_key_counters(
&self,
counters: &KeyCounters,
_session_id: &str,
_revision: u64,
) -> anyhow::Result<()> {
let count = counters[&self.mode][&self.key];
self.events
.lock()
Expand All @@ -4076,7 +4081,14 @@ mod tests {
Ok(())
}

fn emit_key_counter(&self, _mode: &str, _key: &str, count: u32) -> anyhow::Result<()> {
fn emit_key_counter(
&self,
_mode: &str,
_key: &str,
count: u32,
_session_id: &str,
_revision: u64,
) -> anyhow::Result<()> {
self.events.lock().unwrap().push(format!("counter:{count}"));
Ok(())
}
Expand Down Expand Up @@ -8851,6 +8863,10 @@ mod tests {
increment_handle.join().unwrap();
assert_eq!(*events.lock().unwrap(), vec!["snapshot:0", "counter:1"]);
assert_eq!(state.snapshot_key_counters()[&mode][&key], 1);
let bootstrap = state.bootstrap_payload();
assert_eq!(bootstrap.key_counters[&mode][&key], 1);
assert!(!bootstrap.key_counters_session_id.is_empty());
assert_eq!(bootstrap.key_counters_revision, 2);

state.shutdown();
drop(state);
Expand Down
Loading