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

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

```ts
createCollaborationEditingHistory(runtime: HistoryRuntime): EditingHistory
```
83 changes: 73 additions & 10 deletions docs/api-reference/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,52 +551,57 @@ calendarVisibleHourBand(startMinutes: number, endMinutes: number, hourStart: num
## `createAnnotationEditor`

```ts
createAnnotationEditor(source: EditingDocumentSource<AnnotationDocument>): AnnotationEditor
createAnnotationEditor(source: EditingDocumentSource<AnnotationDocument>, options?: EditingHistoryOptions): AnnotationEditor
```
## `createCalendarEditor`

```ts
createCalendarEditor(source: EditingDocumentSource<CalendarDocument>, options?: { readonly createId?: () => string; readonly initialEventIds?: ReadonlyArray<string>; }): CalendarEditor
createCalendarEditor(source: EditingDocumentSource<CalendarDocument>, options?: EditingHistoryOptions & { readonly createId?: () => string; readonly initialEventIds?: ReadonlyArray<string>; }): CalendarEditor
```
## `createDatabaseEditor`

```ts
createDatabaseEditor(source: EditingDocumentSource<DatabaseDocument>): DatabaseEditor
createDatabaseEditor(source: EditingDocumentSource<DatabaseDocument>, options?: EditingHistoryOptions): DatabaseEditor
```
## `createDocumentEditor`

```ts
createDocumentEditor(source: EditingDocumentSource<BlockDocument>, options?: { readonly createId?: () => string; }): DocumentEditor
createDocumentEditor(source: EditingDocumentSource<BlockDocument>, options?: EditingHistoryOptions & { readonly createId?: () => string; }): DocumentEditor
```
## `createEditingId`

```ts
createEditingId(prefix: string): string
```
## `createEditingSession`

```ts
createEditingSession<Selection extends JSONValue>(options: { readonly document: JSONDocument; readonly selection: Selection; readonly reconcileSelection?: (selection: Selection, value: JSONValue) => Selection; }): EditingSession<Selection>
createEditingSession<Selection extends JSONValue>(options: EditingSessionOptions<Selection>): EditingSession<Selection>
```
## `createKanbanEditor`

```ts
createKanbanEditor(source: EditingDocumentSource<KanbanDocument>): KanbanEditor
createKanbanEditor(source: EditingDocumentSource<KanbanDocument>, options?: EditingHistoryOptions): KanbanEditor
```
## `createObjectEditor`

```ts
createObjectEditor(source: EditingDocumentSource<ObjectDocument>, options?: { readonly createId?: () => string; }): ObjectEditor
createObjectEditor(source: EditingDocumentSource<ObjectDocument>, options?: EditingHistoryOptions & { readonly createId?: () => string; }): ObjectEditor
```
## `createOrderEditor`

```ts
createOrderEditor(source: EditingDocumentSource<OrderDocument>, options?: { readonly createId?: () => string; }): OrderEditor
createOrderEditor(source: EditingDocumentSource<OrderDocument>, options?: EditingHistoryOptions & { readonly createId?: () => string; }): OrderEditor
```
## `createSheetEditor`

```ts
createSheetEditor(source: EditingDocumentSource<SheetDocument>): SheetEditor
createSheetEditor(source: EditingDocumentSource<SheetDocument>, options?: EditingHistoryOptions): SheetEditor
```
## `createTreeEditor`

```ts
createTreeEditor(source: EditingDocumentSource<TreeDocument>, options?: { readonly createId?: () => string; }): TreeEditor
createTreeEditor(source: EditingDocumentSource<TreeDocument>, options?: EditingHistoryOptions & { readonly createId?: () => string; }): TreeEditor
```
## `cutEditingClipboard`

Expand Down Expand Up @@ -902,6 +907,53 @@ interface EditingDispatch<Intent extends EditingIntent, Selection extends JSONVa
dispatch(intent: Intent): EditingResult<Selection>;
}
```
## `EditingDocumentChange`

```ts
interface EditingDocumentChange {
readonly before: JSONValue;
readonly after: JSONValue;
/** Null when catching up without an observed, matching applied change. */
readonly change: JSONAppliedChange | null;
}
```
## `EditingHistory`

```ts
interface EditingHistory {
status(): EditingHistoryStatus;
undo(): EditingHistoryResult;
redo(): EditingHistoryResult;
/** Includes history-only changes, even when the document value stays equal. */
subscribe(listener: () => void): () => void;
}
```
## `EditingHistoryOptions`

```ts
interface EditingHistoryOptions {
/** Use the history belonging to the same document. Omit for local history. */
readonly history?: EditingHistory;
}
```
## `EditingHistoryResult`

```ts
type EditingHistoryResult =
| { readonly ok: true; readonly target: string }
| { readonly ok: false; readonly code: string; readonly reason?: string };
```
## `EditingHistoryStatus`

```ts
interface EditingHistoryStatus {
readonly undoTarget: string | null;
readonly redoTarget: string | null;
readonly canUndo: boolean;
readonly canRedo: boolean;
readonly revision: number;
}
```
## `EditingIntent`

```ts
Expand All @@ -917,6 +969,7 @@ interface EditingPlan<Selection extends JSONValue> {
readonly selectionAfter: Selection;
readonly origin: string;
readonly history?: "record" | "ignore";
/** Groups local inverse history. An external history owner defines its own steps. */
readonly historyGroup?: string;
}
```
Expand All @@ -940,6 +993,16 @@ interface EditingSession<Selection extends JSONValue> {
subscribe(listener: (snapshot: EditingSnapshot<Selection>) => void): () => void;
}
```
## `EditingSessionOptions`

```ts
interface EditingSessionOptions<Selection extends JSONValue> extends EditingHistoryOptions {
readonly document: JSONDocument;
readonly selection: Selection;
readonly mapSelection?: (selection: Selection, change: EditingDocumentChange) => Selection;
readonly reconcileSelection?: (selection: Selection, value: JSONValue) => Selection;
}
```
## `EditingSnapshot`

```ts
Expand Down
8 changes: 7 additions & 1 deletion docs/api-reference/packages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ export const apiReferencePackages = [
["rich-text-react", "@interactive-os/json-document-rich-text-react", "packages/json-document-rich-text-react/src/index.tsx", "Connector", "Rich Text React connector"],
["collaboration", "@interactive-os/json-document-collaboration", "packages/json-document-collaboration/src/index.ts", "Collaboration", "replica, history, text collaboration runtime"],
["contenteditable-collaboration", "@interactive-os/json-document-contenteditable-collaboration", "packages/contenteditable-collaboration/src/index.ts", "Collaboration", "collaborative contenteditable lease"],
].map(([slug, packageName, entrypoint, owner, responsibility]) => ({ slug, packageName, entrypoint, owner, responsibility }));
].map(([slug, packageName, entrypoint, owner, responsibility]) => ({
slug, packageName, entrypoint, owner, responsibility,
subpaths: slug === "collaboration" ? [{
packageName: "@interactive-os/json-document-collaboration/editing",
entrypoint: "packages/json-document-collaboration/src/editing-index.ts",
}] : [],
}));
2 changes: 1 addition & 1 deletion docs/api-reference/rich-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ type RichTextEditorCreationResult =
## `RichTextEditorOptions`

```ts
interface RichTextEditorOptions {
interface RichTextEditorOptions extends EditingHistoryOptions {
readonly document: JSONDocument;
readonly pointer?: Pointer;
readonly selection?: RichTextSelection;
Expand Down
1 change: 1 addition & 0 deletions docs/evaluate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ const activeCompanionPackages = new Set([
"@interactive-os/json-document-rich-text-suggestion-react",
"@interactive-os/json-document-rich-text-mention",
"@interactive-os/json-document-rich-text-mention-react",
"@interactive-os/json-document-rich-text",
"@interactive-os/json-document-selection",
"@interactive-os/json-document-react",
"@interactive-os/json-document-react-hook-form",
Expand Down
55 changes: 54 additions & 1 deletion docs/public/collaboration-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,57 @@ Collaborative History는 지금 참여자가 만든 인과 기여를 끄거나
Editing의 로컬 History와 다릅니다. 로컬 undo는 한 editor의 값과 Selection을
같이 되돌립니다. 여기서의 undo는 문서 시간 여행이 아닙니다.

자세한 호출 예와 상태 읽기는 이어서 채웁니다.
## Editor에 연결하기

`createCollaborationEditingHistory(runtime)`는
`@interactive-os/json-document-collaboration/editing`의 공개 API입니다.
같은 runtime의 document와 history를 editor에 함께 전달합니다.

```ts
import { createTextRuntime } from "@interactive-os/json-document-collaboration/text";
import { createCollaborationEditingHistory } from "@interactive-os/json-document-collaboration/editing";
import { createRichTextEditor } from "@interactive-os/json-document-rich-text";

const runtime = createTextRuntime(initialRichText, {
actorId: "browser-a",
epochId: "draft-42/v1",
ruleset: { id: "rich-text/v1", digest: "my-schema/v1" },
});
const editor = createRichTextEditor({
document: runtime.document,
history: createCollaborationEditingHistory(runtime),
});

editor.dispatch({ type: "text.insert", text: "안녕하세요" });
editor.undo();
editor.redo();
editor.snapshot.canUndo;
editor.snapshot.canRedo;
```

Document·Order·Object·Sheet·Tree·Database·Kanban·Calendar·Annotation editor도
두 번째 인자로 `{ history }`를 받습니다. 연결 없이 협업 document만 넣으면
기존 local inverse history가 유지되며 외부 변경 시 비워집니다.

## 하나의 history owner

Toolbar, Cmd/Ctrl+Z, native history input은 모두 editor의 undo/redo를 호출합니다.
별도 Host stack을 만들지 않습니다. availability와 값이 바뀌지 않는 인과 history
통지도 Collaboration이 소유합니다.

Editor는 자신이 기록한 target의 Selection을 복원하되 현재 문서로 mapping합니다.
다른 참여자의 text 삽입은 위치 계산에 반영하고, 삭제된 domain ID는 정리합니다.
Editor가 생성되기 전에 작성된 target은 알 수 없는 과거 Selection을 만들지 않고
현재 Selection을 reconcile합니다. Selection은 collaboration wire에 들어가지 않습니다.

협업 undo 단위는 인과 commit 하나입니다. Local `historyGroup`은 이 단위를 합치지
않으며, external history에서 `history: "ignore"` plan은 변경 전에 거절됩니다.
기본 local 사용에서는 기존 grouping을 유지합니다.

## Usage

[Rich Text 협업 history 실행 예](/editing/rich-text?history=collaboration)에서
직접 입력 → 원격 변경 수신 → Undo/Redo를 확인할 수 있습니다.
Usage와 Source 탭은 공개 editor와 connection의 정본 구현으로 연결됩니다.
API 타입은 [Collaboration API](/docs/api/collaboration)와
[Editing API](/docs/api/editing)에 있습니다.
4 changes: 4 additions & 0 deletions docs/public/history.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ History 항목은 JSON 값이 실제로 바뀐 편집에서 생깁니다. Select
현재 편집 대상만 바꾸므로 기록을 추가하지 않습니다. 검사를 통과하지 못한
요청과 문서 값이 그대로인 요청도 되돌릴 값이 없어 기록되지 않습니다.

기본 local history는 외부 문서 변경을 받으면 비워집니다. 다른 참여자의 변경을
보존하며 내 기여만 취소하려면 [Collaborative History](collaboration-history.md)의
공식 연결 API를 사용합니다. document만 바꾸는 것으로 history 의미까지 바뀌지는 않습니다.

여기까지 `editor.dispatch`로 시작한 요청이 Selection과 Topology를 읽고,
Clipboard를 거쳐 문서와 History를 바꾸는 흐름을 살펴봤습니다. editor가
받는 전체 요청은 [Intent 레퍼런스](intent.md)에서 확인할 수 있습니다.
Expand Down
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions packages/json-document-collaboration/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# @interactive-os/json-document-collaboration

## Editing history integration

`@interactive-os/json-document-collaboration/editing` exports
`createCollaborationEditingHistory(runtime: HistoryRuntime): EditingHistory`.
This optional subpath connects existing selective history to all Editing domain
editors, including Rich Text. It does not extend JSONDocument or the wire.

```ts
import { createHistoryRuntime } from "@interactive-os/json-document-collaboration/history";
import { createCollaborationEditingHistory } from "@interactive-os/json-document-collaboration/editing";
import { createDocumentEditor } from "@interactive-os/json-document-editing";

const runtime = createHistoryRuntime({ blocks: [{ id: "a", text: "Draft" }] }, {
actorId: "browser-a", epochId: "document-42/v1",
ruleset: { id: "blocks", digest: "blocks/v1" },
});
const editor = createDocumentEditor(runtime.document, {
history: createCollaborationEditingHistory(runtime),
});
editor.dispatch({ type: "text.replace", blockId: "a", text: "Edited" });
editor.undo(); // Same selective owner as runtime.history, with editor selection restoration.
```

Use `createTextRuntime` for concurrent text splices. Pass the history and document
from the **same runtime**. Toolbar and DOM integrations call `editor.undo/redo`;
they must not maintain separate stacks. Status includes causal-only changes and
subscriptions are released with the last editor observer.

One causal commit is one undo step. Editing's local `historyGroup` does not group
causal changes, and an external-history Editing plan cannot opt out of recording.
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).

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
11 changes: 10 additions & 1 deletion packages/json-document-collaboration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"./text": {
"types": "./dist/text-index.d.ts",
"import": "./dist/text-index.js"
},
"./editing": {
"types": "./dist/editing-index.d.ts",
"import": "./dist/editing-index.js"
}
},
"scripts": {
Expand All @@ -48,10 +52,15 @@
"verify": "npm run typecheck && npm test && npm run build"
},
"peerDependencies": {
"@interactive-os/json-document": "^3.0.0"
"@interactive-os/json-document": "^3.0.0",
"@interactive-os/json-document-editing": "^0.1.0-rc.0"
},
"peerDependenciesMeta": {
"@interactive-os/json-document-editing": { "optional": true }
},
"devDependencies": {
"@interactive-os/json-document": "*",
"@interactive-os/json-document-editing": "*",
"@types/node": "^25.9.0",
"typescript": "^5.0.0",
"vitest": "^4.1.7"
Expand Down
28 changes: 28 additions & 0 deletions packages/json-document-collaboration/src/editing-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { EditingHistory } from "@interactive-os/json-document-editing";
import { changeIdKey } from "./change.js";
import type { HistoryRuntime } 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),
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;
},
redo() {
const result = runtime.history.redo();
return result.ok ? { ok: true, target: changeIdKey(result.target) } : result;
},
subscribe: (listener) => runtime.replica.subscribe(listener),
};
}
Loading