# PROMPT 05 — Co-Browse Engine Service
## Prerequisites: Prompts 01–04 complete

---

## Your Task
Implement `core/services/collab-cobrowse.service.ts` — the intelligence
layer that broadcasts and receives route, cursor, and form sync events.

---

## Inject
```typescript
private readonly hub          = inject(CollabHubService);
private readonly router       = inject(Router);
private readonly sessionState = inject(SessionState);
private readonly participantState = inject(ParticipantState);
private readonly config       = inject(COLLAB_CONFIG);
private readonly destroyRef   = inject(DestroyRef);
```

---

## Form Registry

```typescript
private readonly formRegistry = new Map<string, AbstractControl>();

registerForm(formId: string, form: AbstractControl): void {
  this.formRegistry.set(formId, form);
  if (this.participantState.hasControl()) {
    this.watchForm(formId, form);
  }
}

unregisterForm(formId: string): void {
  this.formRegistry.delete(formId);
  this.formWatchers.get(formId)?.unsubscribe();
  this.formWatchers.delete(formId);
}

private readonly formWatchers = new Map<string, Subscription>();
```

---

## Route Broadcasting

```typescript
startRouteBroadcast(sessionId: string, context: CollabContext): void {
  // Subscribe to router.events, filter(e => e instanceof NavigationEnd)
  // Use distinctUntilChanged((a, b) => a.urlAfterRedirects === b.urlAfterRedirects)
  // Only broadcast if participantState.hasControl() is true
  // Build RouteSyncPayload with context + current route
  // Call hub.syncRoute(payload)
  // Store subscription for cleanup
}

private handleIncomingRoute(payload: RouteSyncPayload): void {
  // Only navigate if: isFollowingHost() && !hasControl()
  if (!this.participantState.isFollowingHost() || this.participantState.hasControl()) return;
  this.router.navigateByUrl(payload.route);
  this._routeChangeApplied$.next(payload);
}
```

---

## Cursor Broadcasting

```typescript
startCursorBroadcast(sessionId: string): void {
  fromEvent<MouseEvent>(document, 'mousemove').pipe(
    throttleTime(COLLAB_CURSOR_THROTTLE_MS, asyncScheduler, { leading: true, trailing: true }),
    map(e => ({
      xPercent: (e.clientX / window.innerWidth) * 100,
      yPercent: (e.clientY / window.innerHeight) * 100,
    })),
    distinctUntilChanged((a, b) =>
      Math.abs(a.xPercent - b.xPercent) < 0.5 &&
      Math.abs(a.yPercent - b.yPercent) < 0.5
    ),
    takeUntilDestroyed(this.destroyRef),
  ).subscribe(({ xPercent, yPercent }) => {
    // Only broadcast if config.enableCursorBroadcast
    // Build CursorPayload with myUserId, cursorColor from participant
    this.hub.syncCursor({ sessionId, userId: ..., xPercent, yPercent, ... });
  });
}

private handleIncomingCursor(payload: CursorPayload): void {
  // Update presence — this drives the cursor overlay reactively
  this.participantState.updateCursor(payload.userId, payload.xPercent, payload.yPercent);
}
```

---

## Form Sync Broadcasting

```typescript
private watchForm(formId: string, form: AbstractControl): void {
  // Get last sent snapshot to diff against (prevent re-broadcasting received changes)
  let lastSentSnapshot: Record<string, unknown> = {};

  const sub = form.valueChanges.pipe(
    debounceTime(COLLAB_FORM_DEBOUNCE_MS),
    // Only if config.enableFormSync and hasControl
    filter(() => this.participantState.hasControl()),
    takeUntilDestroyed(this.destroyRef),
  ).subscribe(value => {
    const sessionId = untracked(() => this.sessionState.currentSessionId());
    if (!sessionId) return;

    // Deep diff value vs lastSentSnapshot
    // For each changed field path, emit one FormSyncPayload
    const changes = deepDiff(lastSentSnapshot, value);
    lastSentSnapshot = structuredClone(value);

    changes.forEach(({ path, value: fieldValue }) => {
      this.hub.syncFormState({ sessionId, formId, fieldPath: path, value: fieldValue, ... });
    });
  });

  this.formWatchers.set(formId, sub);
}

private handleIncomingFormSync(payload: FormSyncPayload): void {
  // Do NOT apply if hasControl() — you are the editor
  if (this.participantState.hasControl()) return;

  const form = this.formRegistry.get(payload.formId);
  if (!form) return;

  // Apply value at fieldPath using angular form patching
  // CRITICAL: use { emitEvent: false } to prevent feedback loop
  applyFormValue(form, payload.fieldPath, payload.value);
}
```

---

## Master Start / Stop

```typescript
startCoBrowse(sessionId: string, context: CollabContext): void {
  // Start route broadcast
  // Start cursor broadcast (only if config.enableCursorBroadcast)
  // Start watching all registered forms (only if config.enableFormSync)
  // Subscribe to all hub incoming streams:
  this.hub.routeChanged$.pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(p => this.handleIncomingRoute(p));
  this.hub.cursorMoved$.pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(p => this.handleIncomingCursor(p));
  this.hub.formStateChanged$.pipe(takeUntilDestroyed(this.destroyRef))
    .subscribe(p => this.handleIncomingFormSync(p));
}

stopCoBrowse(): void {
  // Unsubscribe route, cursor broadcasts
  // Unsubscribe all form watchers
  // Complete internal subjects
  this.formRegistry.clear();
  this.formWatchers.forEach(s => s.unsubscribe());
  this.formWatchers.clear();
}
```

---

## Exposed Observables

```typescript
private readonly _routeChangeApplied$ = new Subject<RouteSyncPayload>();
readonly routeChangeApplied$ = this._routeChangeApplied$.asObservable();

private readonly _formSyncApplied$ = new Subject<FormSyncPayload>();
readonly formSyncApplied$ = this._formSyncApplied$.asObservable();
```

---

## Utility: `applyFormValue(form, path, value)`

```typescript
// Helper function (not a method — pure util)
// Traverses dot-notation path on AbstractControl tree
// e.g. "invoice.lineItems.0.amount" → form.get('invoice')?.get('lineItems')?.get('0')?.get('amount')
// Calls .patchValue(value, { emitEvent: false, onlySelf: true })
// Handles array indices in path
```

---

## Verification Checklist
- [ ] Form sync uses `{ emitEvent: false }` to avoid feedback loop
- [ ] Cursor throttle uses `asyncScheduler` with both leading and trailing
- [ ] Route sync uses `distinctUntilChanged` to avoid duplicate broadcasts
- [ ] `untracked()` when reading signals inside subscriptions
- [ ] All subscriptions cleaned up in `stopCoBrowse()`
- [ ] `registerForm` is safe to call before session starts (no-ops until startCoBrowse)
