@k2b/cloud 0.43.0 → 0.45.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/package.json +11 -2
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +158 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +7 -5
- package/src/_internal/define-app.ts +57 -6
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +11 -1
- package/src/_internal/registry.ts +8 -1
- package/src/_internal/runtime-context.ts +8 -1
- package/src/access/AppAccessEditor.tsx +2 -0
- package/src/access/GroupCoverage.tsx +20 -5
- package/src/access/PermissionEditor.tsx +20 -5
- package/src/access/PrincipalAvatar.tsx +30 -21
- package/src/access/PrincipalPicker.tsx +14 -4
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-messages.ts +6 -0
- package/src/access/messages.ts +4 -4
- package/src/account/EntitySearch.tsx +12 -0
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +7 -0
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +10 -6
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +40 -25
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +12 -5
- package/src/ai/inference-calls.ts +5 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api.ts +26 -9
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/project-tool.ts +4 -4
- package/src/ai/projects-authorization.ts +53 -0
- package/src/ai/projects-routes.ts +58 -47
- package/src/ai/projects.ts +110 -88
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +55 -6
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider.ts +1 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +4 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +9 -3
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +47 -48
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/skill-tool.ts +4 -4
- package/src/ai/skills-routes.ts +27 -26
- package/src/ai/skills.ts +120 -81
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +21 -44
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +27 -22
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +22 -3
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-skills.ts +4 -1
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +35 -5
- package/src/api/capabilities.ts +28 -4
- package/src/api/index.ts +7 -1
- package/src/api/mcp.ts +175 -2
- package/src/api/me.ts +6 -4
- package/src/api/pwa-phone.ts +75 -26
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/app-pwa/contracts.ts +4 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +1 -0
- package/src/app-pwa/service.ts +91 -0
- package/src/app-pwa/transport.ts +22 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/session.ts +39 -0
- package/src/browser/shared-text-history-validation.ts +151 -0
- package/src/browser/shared-text-history.ts +464 -0
- package/src/browser/shared-text-offline-authorized.ts +29 -0
- package/src/browser/shared-text-offline.ts +351 -0
- package/src/browser/shared-text-provider.ts +714 -0
- package/src/browser/shared-text-table-contract.ts +150 -0
- package/src/browser/shared-text.ts +19 -0
- package/src/capabilities/server-invoke.ts +2 -11
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +5 -2
- package/src/cli/apps.ts +89 -26
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +26 -2
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +21 -5
- package/src/contracts/capabilities.ts +143 -3
- package/src/contracts/index.ts +2 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/pwa.ts +17 -1
- package/src/contracts/registry.ts +6 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +6 -1
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +1 -1
- package/src/server/api/respond.ts +17 -4
- package/src/server/index.ts +2 -0
- package/src/server/middleware/app-access.ts +1 -1
- package/src/server/middleware/auth.ts +21 -7
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +2 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +31 -10
- package/src/server/services/index.ts +1 -0
- package/src/services/accounts/app.ts +36 -13
- package/src/services/accounts/entities.ts +43 -19
- package/src/services/accounts/group-sql.ts +23 -0
- package/src/services/accounts/groups.ts +19 -14
- package/src/services/accounts/identities.ts +70 -11
- package/src/services/accounts/identity-reconciliation.ts +3 -1
- package/src/services/accounts/local-groups.ts +69 -4
- package/src/services/accounts/users.ts +45 -14
- package/src/services/app-access.ts +25 -7
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/ledger.ts +28 -10
- package/src/services/geo/service.ts +43 -24
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/invocation-authority.ts +4 -0
- package/src/services/identity/invocation-token.ts +238 -56
- package/src/services/identity/issue-invocation.ts +6 -1
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +13 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/index.ts +2 -2
- package/src/services/mandates/policy.ts +20 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/session/index.ts +26 -3
- package/src/services/settings/core-settings.ts +71 -43
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/email-html.ts +1 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/markdown/extensions/katex.ts +8 -1
- package/src/shared/markdown/extensions/tables.ts +14 -1
- package/src/shared/markdown/index.ts +22 -11
- package/src/shared-text/anchors.ts +29 -0
- package/src/shared-text/awareness.ts +87 -0
- package/src/shared-text/changes.ts +27 -0
- package/src/shared-text/documents.ts +433 -65
- package/src/shared-text/events.ts +12 -2
- package/src/shared-text/index.ts +16 -1
- package/src/shared-text/log.ts +24 -5
- package/src/shared-text/snapshots.ts +69 -23
- package/src/shared-text/structural-history.ts +377 -0
- package/src/shared-text/table-actions.ts +432 -0
- package/src/shared-text/text-diff.ts +75 -0
- package/src/shared-text/types.ts +24 -5
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +9 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +2 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +2 -2
- package/src/ssr/CloudShell.island.tsx +44 -7
- package/src/ssr/Layout.tsx +9 -7
- package/src/ssr/MobileProfileActions.tsx +2 -2
- package/src/ssr/ProfilePreferences.tsx +2 -1
- package/src/ssr/PwaLayout.tsx +16 -1
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -31
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-launchpad.tsx +6 -1
- package/src/ssr/index.ts +8 -0
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +11 -1
- package/src/ssr/profile-actions.ts +3 -6
- package/src/ssr/pwa-messages.ts +2 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +3 -1
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/language/expressions.ts +3 -2
- package/src/workflows/runtime/executor.ts +134 -34
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- package/src/shared/ai-model-request-settings.ts +0 -21
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/** Fixed-character table intents shared by browser capture and server planning. */
|
|
2
|
+
import type { MenuAction, TableTarget } from "@k2b/ui/text-editor";
|
|
3
|
+
import * as T from "@k2b/ui/text-editor/model";
|
|
4
|
+
import type * as Y from "yjs";
|
|
5
|
+
import { captureTextAnchor, resolveTextAnchor } from "./anchors";
|
|
6
|
+
import type { SharedTextChange } from "./types";
|
|
7
|
+
|
|
8
|
+
type RowAction = Extract<MenuAction, "rowAbove" | "rowBelow" | "deleteRow" | "moveRowUp" | "moveRowDown">;
|
|
9
|
+
type RowMove = Extract<RowAction, "moveRowUp" | "moveRowDown">;
|
|
10
|
+
type ColumnMove = Extract<MenuAction, "moveColLeft" | "moveColRight">;
|
|
11
|
+
type TableAction = Extract<MenuAction, "completeColumns" | "toggleHeader" | "deleteTable">;
|
|
12
|
+
type ColumnAction = Exclude<MenuAction, RowAction | TableAction>;
|
|
13
|
+
type TableWitness = { headerBreak: Uint8Array; separatorHyphen: Uint8Array };
|
|
14
|
+
type RowWitness = { kind: "header" } | { kind: "body"; beforeBreak: Uint8Array; inside: Uint8Array; insideKind: "pipe" | "break" };
|
|
15
|
+
type ColumnWitness = { hyphen: Uint8Array; left: Uint8Array; right: Uint8Array };
|
|
16
|
+
|
|
17
|
+
/** Transport encodes the byte arrays; no old numeric row/column survives capture. */
|
|
18
|
+
export type TableActionIntent =
|
|
19
|
+
| { action: TableAction; table: TableWitness }
|
|
20
|
+
| { action: "replaceSource"; table: TableWitness; expected: string; source: string }
|
|
21
|
+
| { action: Exclude<RowAction, RowMove>; table: TableWitness; row: RowWitness }
|
|
22
|
+
| { action: RowMove; table: TableWitness; row: RowWitness; neighbor: RowWitness }
|
|
23
|
+
| { action: Exclude<ColumnAction, ColumnMove>; table: TableWitness; column: ColumnWitness }
|
|
24
|
+
| { action: ColumnMove; table: TableWitness; column: ColumnWitness; neighbor: ColumnWitness };
|
|
25
|
+
|
|
26
|
+
/** Stable conflict for malformed, unsupported, deleted, replaced, or moved targets. */
|
|
27
|
+
export class TableActionRejectedError extends Error {
|
|
28
|
+
readonly code = "table_target_rejected";
|
|
29
|
+
|
|
30
|
+
constructor() {
|
|
31
|
+
super("The table target is unavailable. Inspect the table and retry.");
|
|
32
|
+
this.name = "TableActionRejectedError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TableActionLimitError extends RangeError {
|
|
37
|
+
readonly code = "table_action_too_large";
|
|
38
|
+
constructor() {
|
|
39
|
+
super("The table action exceeds the document text limit");
|
|
40
|
+
this.name = "TableActionLimitError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const reject = (): never => {
|
|
45
|
+
throw new TableActionRejectedError();
|
|
46
|
+
};
|
|
47
|
+
const isIndex = (value: number) => Number.isSafeInteger(value) && value >= 0;
|
|
48
|
+
const isBreak = (source: string, at: number) => source[at] === "\n" || source[at] === "\r";
|
|
49
|
+
const hyphenAt = (cell: T.Cell) => cell.from + cell.text.indexOf("-");
|
|
50
|
+
|
|
51
|
+
// Both edges must be actual characters. Optional outer pipes use the physical
|
|
52
|
+
// newline instead; an omitted right pipe at EOF has no safe witness.
|
|
53
|
+
const columnEdges = (table: T.Table, column: number): [number, number] => {
|
|
54
|
+
const separator = table.lines[1]!;
|
|
55
|
+
return [
|
|
56
|
+
separator.leadingPipe ? separator.pipes[column]! : column === 0 ? table.lines[0]!.to : separator.pipes[column - 1]!,
|
|
57
|
+
separator.leadingPipe ? (separator.pipes[column + 1] ?? separator.to) : (separator.pipes[column] ?? separator.to),
|
|
58
|
+
];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Call synchronously in structure.perform, before awaiting a flush or request.
|
|
63
|
+
* The host must ensure its edits reached the server. Capture grants no access;
|
|
64
|
+
* applications authorize the document and fence restore revisions externally.
|
|
65
|
+
*/
|
|
66
|
+
export const captureTableAction = (text: Y.Text, action: MenuAction, target: TableTarget): TableActionIntent => {
|
|
67
|
+
if (!text.doc) return reject();
|
|
68
|
+
const source = text.toString();
|
|
69
|
+
const table = T.findTables(source).find((candidate) => candidate.from === target.tableFrom);
|
|
70
|
+
const row = table && T.gridLine(table, target.row);
|
|
71
|
+
// UI columnFrom clamps extra body cells to the last header cell. Validate that
|
|
72
|
+
// offset, but never mistake it for an extra column's identity.
|
|
73
|
+
const headerCell = table?.lines[0]?.cells[Math.min(target.column, table.columns - 1)];
|
|
74
|
+
const width = table?.lines.reduce((max, line) => Math.max(max, line.cells.length), 0) ?? 0;
|
|
75
|
+
if (
|
|
76
|
+
!table ||
|
|
77
|
+
!row ||
|
|
78
|
+
!headerCell ||
|
|
79
|
+
!isIndex(target.row) ||
|
|
80
|
+
!isIndex(target.column) ||
|
|
81
|
+
target.column >= width ||
|
|
82
|
+
row.from !== target.rowFrom ||
|
|
83
|
+
headerCell.from !== target.columnFrom
|
|
84
|
+
)
|
|
85
|
+
return reject();
|
|
86
|
+
const anchor = (at: number) => captureTextAnchor(text, at);
|
|
87
|
+
const witness: TableWitness = {
|
|
88
|
+
headerBreak: anchor(table.lines[0]!.to),
|
|
89
|
+
separatorHyphen: anchor(hyphenAt(table.lines[1]!.cells[0]!)),
|
|
90
|
+
};
|
|
91
|
+
const rowWitness = (index: number): RowWitness => {
|
|
92
|
+
if (index === 0) return { kind: "header" };
|
|
93
|
+
const line = T.gridLine(table, index);
|
|
94
|
+
if (!line || index < 1) return reject();
|
|
95
|
+
const inside = line.pipes[0] ?? line.to;
|
|
96
|
+
if (!line.pipes.length && !isBreak(source, inside)) return reject();
|
|
97
|
+
return {
|
|
98
|
+
kind: "body",
|
|
99
|
+
beforeBreak: anchor(table.lines[index]!.to),
|
|
100
|
+
inside: anchor(inside),
|
|
101
|
+
insideKind: line.pipes.length ? "pipe" : "break",
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const columnWitness = (index: number): ColumnWitness => {
|
|
105
|
+
const cell = table.lines[1]!.cells[index];
|
|
106
|
+
if (!cell) return reject();
|
|
107
|
+
const [left, right] = columnEdges(table, index);
|
|
108
|
+
if (right === table.lines[1]!.to && !isBreak(source, right)) return reject();
|
|
109
|
+
return { hyphen: anchor(hyphenAt(cell)), left: anchor(left), right: anchor(right) };
|
|
110
|
+
};
|
|
111
|
+
switch (action) {
|
|
112
|
+
case "completeColumns":
|
|
113
|
+
case "toggleHeader":
|
|
114
|
+
case "deleteTable":
|
|
115
|
+
return { action, table: witness };
|
|
116
|
+
case "rowAbove":
|
|
117
|
+
case "rowBelow":
|
|
118
|
+
case "deleteRow":
|
|
119
|
+
case "moveRowUp":
|
|
120
|
+
case "moveRowDown": {
|
|
121
|
+
const row = rowWitness(target.row);
|
|
122
|
+
if (action === "moveRowUp" || action === "moveRowDown") {
|
|
123
|
+
const to = target.row + (action === "moveRowUp" ? -1 : 1);
|
|
124
|
+
if (target.row === 0 || to < 1) return reject();
|
|
125
|
+
return { action, table: witness, row, neighbor: rowWitness(to) };
|
|
126
|
+
}
|
|
127
|
+
return { action, table: witness, row };
|
|
128
|
+
}
|
|
129
|
+
case "moveColLeft":
|
|
130
|
+
case "moveColRight":
|
|
131
|
+
case "colLeft":
|
|
132
|
+
case "colRight":
|
|
133
|
+
case "deleteCol":
|
|
134
|
+
case "alignLeft":
|
|
135
|
+
case "alignCenter":
|
|
136
|
+
case "alignRight": {
|
|
137
|
+
const column = columnWitness(target.column);
|
|
138
|
+
if (action === "moveColLeft" || action === "moveColRight")
|
|
139
|
+
return { action, table: witness, column, neighbor: columnWitness(target.column + (action === "moveColLeft" ? -1 : 1)) };
|
|
140
|
+
return { action, table: witness, column };
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return reject();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** An explicit source draft uses the same fixed target and exact source CAS. */
|
|
148
|
+
export const captureTableSource = (text: Y.Text, target: TableTarget, expected: string, source: string): TableActionIntent => {
|
|
149
|
+
const captured = captureTableAction(text, "completeColumns", target);
|
|
150
|
+
return { action: "replaceSource", table: captured.table, expected, source };
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** A move changes column identity. Reissue both raw cell spans, including their
|
|
154
|
+
* padding/separator identities, so stale fixed witnesses cannot target the
|
|
155
|
+
* neighbor whose contents the pure text model moved into that physical slot. */
|
|
156
|
+
const moveColumns = (source: string, table: T.Table, column: number, to: number): SharedTextChange[] => {
|
|
157
|
+
const changes = T.moveColumn(table, column, to);
|
|
158
|
+
if (!changes.length) return [];
|
|
159
|
+
const first = Math.min(column, to),
|
|
160
|
+
last = Math.max(column, to);
|
|
161
|
+
return table.lines.map((line) => {
|
|
162
|
+
const from = line.leadingPipe ? line.pipes[first]! + 1 : first === 0 ? line.from : line.pipes[first - 1]! + 1;
|
|
163
|
+
const end = (line.leadingPipe ? line.pipes[last + 1] : line.pipes[last]) ?? line.to;
|
|
164
|
+
const edits = changes
|
|
165
|
+
.filter((change) => change.from >= from && change.to <= end)
|
|
166
|
+
.map((change) => ({ ...change, from: change.from - from, to: change.to - from }));
|
|
167
|
+
return { from, to: end, insert: T.applyChanges(source.slice(from, end), edits), recreate: true as const };
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reparse a reconstructed Y.Text and plan disjoint model edits without mutation.
|
|
173
|
+
* Apply in descending source order in one Yjs transaction, under the writer's
|
|
174
|
+
* current-head CAS. Only an explicit source draft replaces a whole table.
|
|
175
|
+
*/
|
|
176
|
+
export const planTableAction = (text: Y.Text, intent: TableActionIntent, maxLength: number): SharedTextChange[] => {
|
|
177
|
+
try {
|
|
178
|
+
const source = text.toString();
|
|
179
|
+
if (!Number.isSafeInteger(maxLength) || maxLength < 0) return reject();
|
|
180
|
+
const resolve = (bytes: Uint8Array): number => {
|
|
181
|
+
if (!(bytes instanceof Uint8Array)) return reject();
|
|
182
|
+
return resolveTextAnchor(text, bytes) ?? reject();
|
|
183
|
+
};
|
|
184
|
+
const headerBreak = resolve(intent.table.headerBreak);
|
|
185
|
+
const separatorHyphen = resolve(intent.table.separatorHyphen);
|
|
186
|
+
const table = T.findTables(source).find(
|
|
187
|
+
(candidate) =>
|
|
188
|
+
candidate.lines[0]!.to === headerBreak &&
|
|
189
|
+
candidate.lines[1]!.cells.some((cell) => separatorHyphen >= cell.from && separatorHyphen < cell.to),
|
|
190
|
+
);
|
|
191
|
+
if (!table || !isBreak(source, headerBreak) || source[separatorHyphen] !== "-") return reject();
|
|
192
|
+
const resolveRow = (witness: RowWitness): number => {
|
|
193
|
+
if (witness.kind === "header") return 0;
|
|
194
|
+
if (witness.kind !== "body") return reject();
|
|
195
|
+
const before = resolve(witness.beforeBreak),
|
|
196
|
+
inside = resolve(witness.inside);
|
|
197
|
+
if (!isBreak(source, before)) return reject();
|
|
198
|
+
// The preceding newline is deleted by deleteRow. A row's own ending
|
|
199
|
+
// newline alone is unsafe: deletion can leave it attached to another row.
|
|
200
|
+
for (let row = 1; row < T.rowCount(table); row++) {
|
|
201
|
+
const line = T.gridLine(table, row)!;
|
|
202
|
+
if (table.lines[row]!.to !== before) continue;
|
|
203
|
+
if (witness.insideKind === "pipe" && line.pipes.includes(inside)) return row;
|
|
204
|
+
if (witness.insideKind === "break" && line.to === inside && isBreak(source, inside)) return row;
|
|
205
|
+
}
|
|
206
|
+
return reject();
|
|
207
|
+
};
|
|
208
|
+
const resolveColumn = (witness: ColumnWitness): number => {
|
|
209
|
+
const hyphen = resolve(witness.hyphen),
|
|
210
|
+
left = resolve(witness.left),
|
|
211
|
+
right = resolve(witness.right);
|
|
212
|
+
if (source[hyphen] !== "-") return reject();
|
|
213
|
+
const column = table.lines[1]!.cells.findIndex((cell) => hyphen >= cell.from && hyphen < cell.to);
|
|
214
|
+
if (column < 0) return reject();
|
|
215
|
+
const [currentLeft, currentRight] = columnEdges(table, column);
|
|
216
|
+
if (left !== currentLeft || right !== currentRight) return reject();
|
|
217
|
+
if (right === table.lines[1]!.to && !isBreak(source, right)) return reject();
|
|
218
|
+
return column;
|
|
219
|
+
};
|
|
220
|
+
const checkExpansion = (length: number) => {
|
|
221
|
+
if (source.length + length > maxLength) throw new TableActionLimitError();
|
|
222
|
+
};
|
|
223
|
+
const columnInsertion = (column: number) => {
|
|
224
|
+
let length = 0;
|
|
225
|
+
for (const [row, line] of table.lines.entries()) {
|
|
226
|
+
length +=
|
|
227
|
+
column >= line.cells.length
|
|
228
|
+
? (line.trailingPipe ? 0 : 2) + (column - line.cells.length + 1) * (row === 1 ? 6 : 4)
|
|
229
|
+
: (column === 0 && !line.leadingPipe ? 1 : 0) + (row === 1 ? 6 : 4);
|
|
230
|
+
checkExpansion(length);
|
|
231
|
+
}
|
|
232
|
+
return T.insertColumn(table, column);
|
|
233
|
+
};
|
|
234
|
+
const rowInsertion = (after: number) => {
|
|
235
|
+
checkExpansion((table.lines[0]?.lineBreak || "\n").length + 1 + 4 * table.columns);
|
|
236
|
+
return T.insertRow(table, after);
|
|
237
|
+
};
|
|
238
|
+
switch (intent.action) {
|
|
239
|
+
case "completeColumns": {
|
|
240
|
+
const columns = table.lines.reduce((width, line) => Math.max(width, line.cells.length), table.columns);
|
|
241
|
+
let length = 0;
|
|
242
|
+
for (const [row, line] of table.lines.entries()) {
|
|
243
|
+
const count = columns - line.cells.length;
|
|
244
|
+
if (count) length += (line.trailingPipe ? 0 : 2) + count * (row === 1 ? 6 : 4);
|
|
245
|
+
checkExpansion(length);
|
|
246
|
+
}
|
|
247
|
+
return T.completeColumns(table);
|
|
248
|
+
}
|
|
249
|
+
case "rowAbove":
|
|
250
|
+
return rowInsertion(Math.max(0, resolveRow(intent.row) - 1));
|
|
251
|
+
case "rowBelow":
|
|
252
|
+
return rowInsertion(resolveRow(intent.row));
|
|
253
|
+
case "deleteRow":
|
|
254
|
+
return T.deleteRow(table, resolveRow(intent.row));
|
|
255
|
+
case "moveRowUp": {
|
|
256
|
+
const row = resolveRow(intent.row);
|
|
257
|
+
const to = resolveRow(intent.neighbor);
|
|
258
|
+
if (to !== row - 1) return reject();
|
|
259
|
+
return T.moveRow(source, table, row, to);
|
|
260
|
+
}
|
|
261
|
+
case "moveRowDown": {
|
|
262
|
+
const row = resolveRow(intent.row);
|
|
263
|
+
const to = resolveRow(intent.neighbor);
|
|
264
|
+
if (to !== row + 1) return reject();
|
|
265
|
+
return T.moveRow(source, table, row, to);
|
|
266
|
+
}
|
|
267
|
+
case "moveColLeft": {
|
|
268
|
+
const column = resolveColumn(intent.column);
|
|
269
|
+
const to = resolveColumn(intent.neighbor);
|
|
270
|
+
if (to !== column - 1) return reject();
|
|
271
|
+
return moveColumns(source, table, column, to);
|
|
272
|
+
}
|
|
273
|
+
case "moveColRight": {
|
|
274
|
+
const column = resolveColumn(intent.column);
|
|
275
|
+
const to = resolveColumn(intent.neighbor);
|
|
276
|
+
if (to !== column + 1) return reject();
|
|
277
|
+
return moveColumns(source, table, column, to);
|
|
278
|
+
}
|
|
279
|
+
case "toggleHeader":
|
|
280
|
+
return T.toggleHeader(source, table);
|
|
281
|
+
case "deleteTable":
|
|
282
|
+
return T.deleteTable(table);
|
|
283
|
+
case "replaceSource": {
|
|
284
|
+
if (source.slice(table.from, table.to) !== intent.expected) return reject();
|
|
285
|
+
const replacement = T.findTables(intent.source);
|
|
286
|
+
if (
|
|
287
|
+
replacement.length !== 1 ||
|
|
288
|
+
replacement[0]!.from !== 0 ||
|
|
289
|
+
intent.source.slice(replacement[0]!.to).trim() !== "" ||
|
|
290
|
+
replacement[0]!.lines.some((line) => line.cells.length > replacement[0]!.columns)
|
|
291
|
+
)
|
|
292
|
+
return reject();
|
|
293
|
+
checkExpansion(intent.source.length - intent.expected.length);
|
|
294
|
+
return intent.source === intent.expected ? [] : [{ from: table.from, to: table.to, insert: intent.source }];
|
|
295
|
+
}
|
|
296
|
+
case "colLeft":
|
|
297
|
+
return columnInsertion(resolveColumn(intent.column));
|
|
298
|
+
case "colRight":
|
|
299
|
+
return columnInsertion(resolveColumn(intent.column) + 1);
|
|
300
|
+
case "deleteCol":
|
|
301
|
+
return T.deleteColumn(table, resolveColumn(intent.column));
|
|
302
|
+
case "alignLeft":
|
|
303
|
+
return T.setAlign(table, resolveColumn(intent.column), "left");
|
|
304
|
+
case "alignCenter":
|
|
305
|
+
return T.setAlign(table, resolveColumn(intent.column), "center");
|
|
306
|
+
case "alignRight":
|
|
307
|
+
return T.setAlign(table, resolveColumn(intent.column), "right");
|
|
308
|
+
default:
|
|
309
|
+
return reject();
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
if (error instanceof TableActionLimitError) throw error;
|
|
313
|
+
// Transport may supply invalid shapes/bytes despite the static type. Every
|
|
314
|
+
// rejected intent has the same public error, and planning never edits text.
|
|
315
|
+
return reject();
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/** A focus target is resolved against current identities immediately before use. */
|
|
320
|
+
export type TableFocusWitness = { table: TableWitness; row: RowWitness; column: ColumnWitness };
|
|
321
|
+
export const captureTableFocus = (text: Y.Text, target: TableTarget): TableFocusWitness => {
|
|
322
|
+
const row = captureTableAction(text, "rowBelow", target);
|
|
323
|
+
const column = captureTableAction(text, "alignLeft", target);
|
|
324
|
+
if (!("row" in row) || !("column" in column)) return reject();
|
|
325
|
+
return { table: row.table, row: row.row, column: column.column };
|
|
326
|
+
};
|
|
327
|
+
export const resolveTableFocus = (text: Y.Text, witness: TableFocusWitness): TableTarget | null => {
|
|
328
|
+
try {
|
|
329
|
+
const source = text.toString();
|
|
330
|
+
const at = (bytes: Uint8Array) => resolveTextAnchor(text, bytes);
|
|
331
|
+
const table = T.findTables(source).find(
|
|
332
|
+
(table) =>
|
|
333
|
+
table.lines[0]!.to === at(witness.table.headerBreak) &&
|
|
334
|
+
table.lines[1]!.cells.some((cell) => {
|
|
335
|
+
const hyphen = at(witness.table.separatorHyphen);
|
|
336
|
+
return hyphen !== null && hyphen >= cell.from && hyphen < cell.to;
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
if (!table) return null;
|
|
340
|
+
const hyphen = at(witness.column.hyphen);
|
|
341
|
+
const column = table.lines[1]!.cells.findIndex((cell) => hyphen !== null && hyphen >= cell.from && hyphen < cell.to);
|
|
342
|
+
if (column < 0 || source[hyphen!] !== "-") return null;
|
|
343
|
+
const edges = columnEdges(table, column);
|
|
344
|
+
if (at(witness.column.left) !== edges[0] || at(witness.column.right) !== edges[1]) return null;
|
|
345
|
+
const rowWitness = witness.row;
|
|
346
|
+
const row =
|
|
347
|
+
rowWitness.kind === "header"
|
|
348
|
+
? 0
|
|
349
|
+
: Array.from({ length: T.rowCount(table) - 1 }, (_, index) => index + 1).find((index) => {
|
|
350
|
+
const line = T.gridLine(table, index)!;
|
|
351
|
+
return (
|
|
352
|
+
table.lines[index]!.to === at(rowWitness.beforeBreak) &&
|
|
353
|
+
(rowWitness.insideKind === "pipe" ? line.pipes.includes(at(rowWitness.inside) ?? -1) : line.to === at(rowWitness.inside))
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
if (row === undefined) return null;
|
|
357
|
+
return { tableFrom: table.from, rowFrom: T.gridLine(table, row)!.from, columnFrom: table.lines[0]!.cells[column]!.from, row, column };
|
|
358
|
+
} catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/** Resolve the selection at the planned head, then capture the ACTUAL receipt IDs. */
|
|
364
|
+
export const prepareTableActionFocus = (
|
|
365
|
+
before: Y.Text,
|
|
366
|
+
intent: TableActionIntent,
|
|
367
|
+
focus: TableFocusWitness,
|
|
368
|
+
caret?: { row: number; column: number },
|
|
369
|
+
): ((after: Y.Text) => TableFocusWitness | null) => {
|
|
370
|
+
const target = resolveTableFocus(before, focus);
|
|
371
|
+
const table = target && T.findTables(before.toString()).find((table) => table.from === target.tableFrom);
|
|
372
|
+
if (!target || !table || resolveTextAnchor(before, intent.table.headerBreak) !== table.lines[0]!.to) return () => null;
|
|
373
|
+
let { row, column } = target;
|
|
374
|
+
// Row/column intents must identify the same selected identity. The request's
|
|
375
|
+
// optional focus is presentation, never authority to change another target.
|
|
376
|
+
if ("row" in intent) {
|
|
377
|
+
const selected = resolveTableFocus(before, { ...focus, row: intent.row });
|
|
378
|
+
if (!selected || selected.row !== row) return () => null;
|
|
379
|
+
}
|
|
380
|
+
if ("column" in intent) {
|
|
381
|
+
const selected = resolveTableFocus(before, { ...focus, column: intent.column });
|
|
382
|
+
if (!selected || selected.column !== column) return () => null;
|
|
383
|
+
}
|
|
384
|
+
switch (intent.action) {
|
|
385
|
+
case "deleteTable":
|
|
386
|
+
return () => null;
|
|
387
|
+
case "rowAbove":
|
|
388
|
+
row = Math.max(1, row);
|
|
389
|
+
break;
|
|
390
|
+
case "rowBelow":
|
|
391
|
+
row++;
|
|
392
|
+
break;
|
|
393
|
+
case "moveRowUp":
|
|
394
|
+
row--;
|
|
395
|
+
break;
|
|
396
|
+
case "moveRowDown":
|
|
397
|
+
row++;
|
|
398
|
+
break;
|
|
399
|
+
case "colRight":
|
|
400
|
+
case "moveColRight":
|
|
401
|
+
column++;
|
|
402
|
+
break;
|
|
403
|
+
case "moveColLeft":
|
|
404
|
+
column--;
|
|
405
|
+
break;
|
|
406
|
+
case "toggleHeader":
|
|
407
|
+
row = table.lines[0]!.cells.some((cell) => cell.text) ? row + 1 : Math.max(0, row - 1);
|
|
408
|
+
break;
|
|
409
|
+
case "replaceSource":
|
|
410
|
+
row = caret?.row ?? row;
|
|
411
|
+
column = caret?.column ?? column;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
const from = table.from;
|
|
415
|
+
return (after) => {
|
|
416
|
+
const table = T.findTables(after.toString()).find((table) => table.from === from);
|
|
417
|
+
if (!table) return null;
|
|
418
|
+
const selectedRow = Math.max(0, Math.min(row, T.rowCount(table) - 1));
|
|
419
|
+
const selectedColumn = Math.max(0, Math.min(column, table.columns - 1));
|
|
420
|
+
try {
|
|
421
|
+
return captureTableFocus(after, {
|
|
422
|
+
tableFrom: table.from,
|
|
423
|
+
rowFrom: T.gridLine(table, selectedRow)!.from,
|
|
424
|
+
columnFrom: table.lines[0]!.cells[selectedColumn]!.from,
|
|
425
|
+
row: selectedRow,
|
|
426
|
+
column: selectedColumn,
|
|
427
|
+
});
|
|
428
|
+
} catch {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { type ArrayChange, diffArrays } from "diff";
|
|
2
|
+
|
|
3
|
+
type TextDiff = { index: number; deleteCount: number; insert: string };
|
|
4
|
+
|
|
1
5
|
const splitsSurrogatePair = (text: string, index: number): boolean => {
|
|
2
6
|
const before = text.charCodeAt(index - 1);
|
|
3
7
|
const after = text.charCodeAt(index);
|
|
@@ -21,3 +25,74 @@ export const minimalTextDiff = (before: string, after: string): { index: number;
|
|
|
21
25
|
|
|
22
26
|
return { index, deleteCount: before.length - index - suffix, insert: after.slice(index, after.length - suffix) };
|
|
23
27
|
};
|
|
28
|
+
|
|
29
|
+
/** Convert adjacent additions/deletions into replacements in the original text's UTF-16 offsets. */
|
|
30
|
+
const replacements = (parts: ArrayChange<string>[], offset: number): TextDiff[] => {
|
|
31
|
+
const hunks: TextDiff[] = [];
|
|
32
|
+
let index = offset;
|
|
33
|
+
let pending: TextDiff | undefined;
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
const value = part.value.join("");
|
|
36
|
+
if (!part.added && !part.removed) {
|
|
37
|
+
if (pending) hunks.push(pending);
|
|
38
|
+
pending = undefined;
|
|
39
|
+
index += value.length;
|
|
40
|
+
} else {
|
|
41
|
+
pending ??= { index, deleteCount: 0, insert: "" };
|
|
42
|
+
if (part.removed) {
|
|
43
|
+
pending.deleteCount += value.length;
|
|
44
|
+
index += value.length;
|
|
45
|
+
} else pending.insert += value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (pending) hunks.push(pending);
|
|
49
|
+
return hunks;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Ascending, non-overlapping hunks. Unchanged lines are matched first; only
|
|
54
|
+
* changed blocks are refined into Unicode code points (never half a surrogate).
|
|
55
|
+
* The log's payload budget bounds comparisons across both passes; comparing two
|
|
56
|
+
* tokens of equal length costs one more unit per 64 characters, since only those
|
|
57
|
+
* compare beyond their length. The square root bounds Myers's quadratic edit frontier.
|
|
58
|
+
* When either budget runs out, retain the old prefix/suffix replacement for
|
|
59
|
+
* that region: text correctness takes precedence over best-effort item identity.
|
|
60
|
+
*/
|
|
61
|
+
export const textDiffHunks = (before: string, after: string, maxPayloadBytes: number): TextDiff[] => {
|
|
62
|
+
if (before === after) return [];
|
|
63
|
+
let remaining = maxPayloadBytes;
|
|
64
|
+
const exhausted = Symbol("diff budget exhausted");
|
|
65
|
+
const compare = (a: string, b: string) => {
|
|
66
|
+
remaining -= 1 + (a.length === b.length ? a.length >> 6 : 0);
|
|
67
|
+
if (remaining < 0) throw exhausted;
|
|
68
|
+
return a === b;
|
|
69
|
+
};
|
|
70
|
+
const diff = (a: string[], b: string[]) => {
|
|
71
|
+
if (remaining <= 0) return undefined;
|
|
72
|
+
try {
|
|
73
|
+
return diffArrays(a, b, { comparator: compare, maxEditLength: Math.floor(Math.sqrt(maxPayloadBytes)) });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error !== exhausted) throw error;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const lines = (text: string) => text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
80
|
+
// Lines both texts share at the start and end cost no budget, so a large note with small edits still diffs.
|
|
81
|
+
const a = lines(before);
|
|
82
|
+
const b = lines(after);
|
|
83
|
+
let start = 0;
|
|
84
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
85
|
+
let end = 0;
|
|
86
|
+
while (end < a.length - start && end < b.length - start && a[a.length - 1 - end] === b[b.length - 1 - end]) end++;
|
|
87
|
+
const offset = a.slice(0, start).reduce((length, line) => length + line.length, 0);
|
|
88
|
+
const lineParts = diff(a.slice(start, a.length - end), b.slice(start, b.length - end));
|
|
89
|
+
if (!lineParts) return [minimalTextDiff(before, after)];
|
|
90
|
+
return replacements(lineParts, offset).flatMap((block) => {
|
|
91
|
+
const oldText = before.slice(block.index, block.index + block.deleteCount);
|
|
92
|
+
const trimmed = minimalTextDiff(oldText, block.insert);
|
|
93
|
+
const index = block.index + trimmed.index;
|
|
94
|
+
const removed = oldText.slice(trimmed.index, trimmed.index + trimmed.deleteCount);
|
|
95
|
+
const charParts = remaining > 0 && removed && trimmed.insert ? diff(Array.from(removed), Array.from(trimmed.insert)) : undefined;
|
|
96
|
+
return charParts ? replacements(charParts, index) : [{ ...trimmed, index }];
|
|
97
|
+
});
|
|
98
|
+
};
|
package/src/shared-text/types.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import type { SharedTextStructuralHistory } from "./structural-history";
|
|
1
2
|
export type SharedTextActor = { kind: "user" | "service_account"; id: string };
|
|
2
3
|
|
|
3
4
|
/** Actor is optional so events retained from older deployments remain readable. */
|
|
4
5
|
export type SharedTextEvent = {
|
|
5
|
-
kind: "sync" | "awareness";
|
|
6
|
+
kind: "sync" | "awareness" | "freeze";
|
|
6
7
|
payload: string;
|
|
7
8
|
originNodeId: string;
|
|
8
9
|
originPeerId: string | null;
|
|
9
10
|
actor?: SharedTextActor;
|
|
11
|
+
/** Echoed only by authorized server actions, for the initiating browser's undo history. */
|
|
12
|
+
actionId?: string;
|
|
13
|
+
/** Authored only by the server structural writer/admission boundary; retained with the event. */
|
|
14
|
+
history?: SharedTextStructuralHistory;
|
|
15
|
+
/** Updates apply only to the store lineage from which they originated. */
|
|
16
|
+
restoreRevision?: string;
|
|
10
17
|
};
|
|
11
|
-
|
|
18
|
+
/** Ordered document events; a freeze marker carries no text update. */
|
|
19
|
+
export type SharedTextSyncEvent = SharedTextEvent & { kind: "sync" | "freeze" };
|
|
12
20
|
export type SharedTextContributor = SharedTextActor & { lastContributedAt: Date };
|
|
13
21
|
|
|
14
22
|
/** The store anchors legacy cursors to this log before returning a snapshot. */
|
|
@@ -27,13 +35,24 @@ export type SharedTextSaveInput = {
|
|
|
27
35
|
actor: SharedTextActor | null;
|
|
28
36
|
contributors: SharedTextContributor[];
|
|
29
37
|
};
|
|
30
|
-
export type SharedTextSnapshot =
|
|
38
|
+
export type SharedTextSnapshot = SharedTextStoredState;
|
|
39
|
+
/** A disjoint edit in the original document's UTF-16 coordinates. */
|
|
40
|
+
export type SharedTextChange = {
|
|
41
|
+
from: number;
|
|
42
|
+
to: number;
|
|
43
|
+
insert: string;
|
|
44
|
+
/** Authorized identity fence: recreate a nonempty range even when its text is equal. */
|
|
45
|
+
recreate?: true;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type SharedTextActionReceipt = { actionId: string; update: Uint8Array };
|
|
49
|
+
|
|
31
50
|
export type SharedTextEditResult<Saved> =
|
|
32
51
|
| { status: "notFound" }
|
|
33
52
|
| { status: "unchanged"; text: string }
|
|
34
53
|
| { status: "syncFailed"; error: unknown }
|
|
35
|
-
| { status: "persistFailed"; cursor: string; error: unknown }
|
|
36
|
-
| { status: "edited"; text: string; cursor: string; saved: Saved };
|
|
54
|
+
| { status: "persistFailed"; cursor: string; error: unknown; receipt?: SharedTextActionReceipt }
|
|
55
|
+
| { status: "edited"; text: string; cursor: string; saved: Saved; receipt?: SharedTextActionReceipt };
|
|
37
56
|
|
|
38
57
|
export type SharedTextStore<Saved, Context> = {
|
|
39
58
|
load(docId: string, options: { signal?: AbortSignal }): Promise<SharedTextStoredState | null>;
|