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
79 changes: 79 additions & 0 deletions docs/api-reference/collaboration.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,85 @@ interface TextSpliceOperation {
readonly inserted: string;
}
```
## `@interactive-os/json-document-collaboration/history`

아래 API는 package root가 아닌 이 subpath에서 import합니다.
### `createHistoryRuntime`

```ts
createHistoryRuntime(initial: unknown, options: CollaborationRuntimeOptions): HistoryRuntime
```
### `History`

```ts
interface History {
status(): HistoryStatus;
canUndo(): JSONPatchValidationResult;
undo(): HistoryResult;
canRedo(): JSONPatchValidationResult;
redo(): HistoryResult;
}
```
### `HistoryRestoreResult`

```ts
type HistoryRestoreResult =
| {
readonly ok: true;
readonly runtime: HistoryRuntime;
}
| {
readonly ok: false;
readonly code: string;
readonly reason: string;
};
```
### `HistoryResult`

```ts
type HistoryResult =
| {
readonly ok: true;
readonly changeId: ChangeId;
readonly target: ChangeId;
readonly didChangeDocument: boolean;
/** This operation's applied change; null when it only changes causal history. */
readonly change: JSONAppliedChange | null;
/** Captured before subscribers can author a later transition. */
readonly status: HistoryStatus & {
readonly canUndo: boolean;
readonly canRedo: boolean;
};
}
| {
readonly ok: false;
readonly code: string;
readonly reason?: string;
};
```
### `HistoryRuntime`

```ts
interface HistoryRuntime extends CollaborationRuntime {
readonly history: History;
}
```
### `HistoryStatus`

```ts
interface HistoryStatus {
readonly undoTarget: ChangeId | null;
readonly redoTarget: ChangeId | null;
readonly undoDepth: number;
readonly redoDepth: number;
readonly revision: number;
}
```
### `restoreHistoryRuntime`

```ts
restoreHistoryRuntime(input: unknown, options: CollaborationRestoreOptions): HistoryRestoreResult
```
## `@interactive-os/json-document-collaboration/editing`

아래 API는 package root가 아닌 이 subpath에서 import합니다.
Expand Down
9 changes: 8 additions & 1 deletion docs/api-reference/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,14 @@ interface EditingHistoryOptions {

```ts
type EditingHistoryResult =
| { readonly ok: true; readonly target: string }
| {
readonly ok: true;
readonly target: string;
/** This operation's applied change; null for a history-only transition. */
readonly change: JSONAppliedChange | null;
/** This operation's status, captured before notifying subscribers. */
readonly status: EditingHistoryStatus;
}
| { readonly ok: false; readonly code: string; readonly reason?: string };
```
## `EditingHistoryStatus`
Expand Down
3 changes: 3 additions & 0 deletions docs/api-reference/packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const apiReferencePackages = [
].map(([slug, packageName, entrypoint, owner, responsibility]) => ({
slug, packageName, entrypoint, owner, responsibility,
subpaths: slug === "collaboration" ? [{
packageName: "@interactive-os/json-document-collaboration/history",
entrypoint: "packages/json-document-collaboration/src/history-index.ts",
}, {
packageName: "@interactive-os/json-document-collaboration/editing",
entrypoint: "packages/json-document-collaboration/src/editing-index.ts",
}] : [],
Expand Down
18 changes: 18 additions & 0 deletions docs/public/collaboration-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ Editor는 자신이 기록한 target의 Selection을 복원하되 현재 문서
Editor가 생성되기 전에 작성된 target은 알 수 없는 과거 Selection을 만들지 않고
현재 Selection을 reconcile합니다. Selection은 collaboration wire에 들어가지 않습니다.

`runtime.history.undo/redo`의 성공 결과는 자기 작업의 `change`와 `status`를
담습니다. `change`는 실제 `JSONAppliedChange`이며, 문서 값이 그대로면 `null`입니다.
`status`는 구독자 통지 전의 undo/redo target·revision·`canUndo`·`canRedo`입니다.
구독자가 별도 편집을 실행해도 그 변경을 원래 undo/redo 결과에 섞지 않습니다.
공식 연결 API가 이 정보를 Editing에 전달하므로 Host는 알림 순서로 작업을
구분할 필요가 없습니다.

직접 `EditingHistory`를 구현한다면 성공 결과에 `target`, `change`, `status`를
모두 반환합니다. `change`는 해당 작업 직전 문서에 적용할 수 있는 자기 변경이고,
`status`는 해당 작업 직후의 `EditingHistoryStatus`입니다. 이전의 target-only
구현은 이 두 필드를 추가해야 합니다.

선택 mapping/reconciliation 콜백이 예외를 던지면 일관된 snapshot이 준비될 때까지
세션은 새 편집이나 undo/redo를 실행하지 않습니다. 이미 성공한 협업 undo/redo는
취소되지 않으며, 다음 읽기는 보관한 작업 결과의 선택 복원만 재시도합니다.
콜백을 수정하거나 editor를 다시 구성한 뒤 진행합니다. 콜백 예외를 history 작업의
거절로 해석해 같은 작업을 다시 실행하지 않습니다.

협업 undo 단위는 인과 commit 하나입니다. Local `historyGroup`은 이 단위를 합치지
않으며, external history에서 `history: "ignore"` plan은 변경 전에 거절됩니다.
기본 local 사용에서는 기존 grouping을 유지합니다.
Expand Down
11 changes: 11 additions & 0 deletions docs/public/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ History 항목은 JSON 값이 실제로 바뀐 편집에서 생깁니다. Select
보존하며 내 기여만 취소하려면 [Collaborative History](collaboration-history.md)의
공식 연결 API를 사용합니다. document만 바꾸는 것으로 history 의미까지 바뀌지는 않습니다.

`createEditingSession`의 선택 mapping/reconciliation 콜백은 외부 변경에 맞는
선택을 계산한 뒤 값·선택·history 상태·revision을 함께 확정합니다. 콜백이
예외를 던지면 이전의 일관된 상태를 보관하고, 다음 읽기나 명령에서 동기화를
재시도합니다. 실패가 지속되는 동안에는 오래된 undo를 현재 문서에 적용하지
않습니다. 동기화에 성공하면 local history를 비우고 새 snapshot을 알립니다.
외부 document commit 자체는 이미 완료됐으므로 콜백 오류로 되돌아가지 않습니다.

snapshot 읽기에서 외부 변경을 따라잡아도 그 revision의 알림은 전달됩니다.
다른 구독자가 먼저 읽었다는 이유로 알림이 누락되지 않습니다. 구독 해제 함수는
여러 번 호출해도 같은 콜백으로 새로 만든 구독을 해제하지 않습니다.

여기까지 `editor.dispatch`로 시작한 요청이 Selection과 Topology를 읽고,
Clipboard를 거쳐 문서와 History를 바꾸는 흐름을 살펴봤습니다. editor가
받는 전체 요청은 [Intent 레퍼런스](intent.md)에서 확인할 수 있습니다.
Expand Down
8 changes: 8 additions & 0 deletions packages/json-document-collaboration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ History remains local unless this connection is explicitly configured.
See [Collaborative History](../../docs/public/collaboration-history.md) and the
owner [API reference](../../docs/api-reference/collaboration.md).

Successful `runtime.history.undo/redo` results include their own `change`
(`JSONAppliedChange | null`) and immutable resulting `status`, including
`canUndo` and `canRedo`. Both are captured before notifying document or replica
subscribers. A history-only operation returns `change: null`, even when a
subscriber authors another document change before the call returns. The Editing
connection forwards that result instead of treating the first observed change
as the history operation. These fields do not enter bundles or checkpoints.

Remote `document.subscribe` notifications compile visible tree identities into
ordered JSON Patch moves, insertions, and removals. Consumers can use
`trackPointer(pointer, change.applied, before)` with the previous snapshot to
Expand Down
26 changes: 17 additions & 9 deletions packages/json-document-collaboration/src/editing-index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
import type { EditingHistory } from "@interactive-os/json-document-editing";
import type { EditingHistory, EditingHistoryStatus } from "@interactive-os/json-document-editing";
import { changeIdKey } from "./change.js";
import type { HistoryRuntime } from "./types.js";
import type { HistoryRuntime, HistoryStatus } from "./types.js";

/** Bind Editing to this runtime's selective history, one causal commit per step. */
export function createCollaborationEditingHistory(runtime: HistoryRuntime): EditingHistory {
return {
status() {
const status = runtime.history.status();
return {
undoTarget: status.undoTarget === null ? null : changeIdKey(status.undoTarget),
redoTarget: status.redoTarget === null ? null : changeIdKey(status.redoTarget),
return editingStatus({
...status,
canUndo: runtime.history.canUndo().ok,
canRedo: runtime.history.canRedo().ok,
revision: status.revision,
};
});
},
undo() {
const result = runtime.history.undo();
return result.ok ? { ok: true, target: changeIdKey(result.target) } : result;
return result.ok ? { ok: true, target: changeIdKey(result.target), change: result.change, status: editingStatus(result.status) } : result;
},
redo() {
const result = runtime.history.redo();
return result.ok ? { ok: true, target: changeIdKey(result.target) } : result;
return result.ok ? { ok: true, target: changeIdKey(result.target), change: result.change, status: editingStatus(result.status) } : result;
},
subscribe: (listener) => runtime.replica.subscribe(listener),
};
}

function editingStatus(status: HistoryStatus & { readonly canUndo: boolean; readonly canRedo: boolean }): EditingHistoryStatus {
return Object.freeze({
undoTarget: status.undoTarget === null ? null : changeIdKey(status.undoTarget),
redoTarget: status.redoTarget === null ? null : changeIdKey(status.redoTarget),
canUndo: status.canUndo,
canRedo: status.canRedo,
revision: status.revision,
});
}
20 changes: 13 additions & 7 deletions packages/json-document-collaboration/src/history-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,23 @@ export function createHistory(state: RuntimeState): History {
}
documentChange = documentCommit.change;
}
state.notify({
...(documentChange === undefined ? {} : { documentChange }),
replicaStatus: state.replicaStatus(),
});

return Object.freeze({
ok: true,
const result = Object.freeze({
ok: true as const,
changeId: freezeChangeId(prepared.value.change.changeId),
target: freezeChangeId(prepared.value.target),
didChangeDocument: prepared.value.didChangeDocument,
change: documentChange ?? null,
status: Object.freeze({
...resolveHistoryState().status,
canUndo: prepareHistoryChange("undo").ok,
canRedo: prepareHistoryChange("redo").ok,
}),
});
state.notify({
...(documentChange === undefined ? {} : { documentChange }),
replicaStatus: state.replicaStatus(),
});
return result;
}

return Object.freeze({
Expand Down
7 changes: 7 additions & 0 deletions packages/json-document-collaboration/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,13 @@ export type HistoryResult =
readonly changeId: ChangeId;
readonly target: ChangeId;
readonly didChangeDocument: boolean;
/** This operation's applied change; null when it only changes causal history. */
readonly change: JSONAppliedChange | null;
/** Captured before subscribers can author a later transition. */
readonly status: HistoryStatus & {
readonly canUndo: boolean;
readonly canRedo: boolean;
};
}
| {
readonly ok: false;
Expand Down
Loading