@tuturuuu/ai 0.0.10

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.
Files changed (130) hide show
  1. package/README.md +76 -0
  2. package/package.json +106 -0
  3. package/src/api-key-hash.ts +28 -0
  4. package/src/calendar/events.ts +34 -0
  5. package/src/calendar/route.ts +114 -0
  6. package/src/chat/credit-source.ts +1 -0
  7. package/src/chat/google/chat-request-schema.ts +150 -0
  8. package/src/chat/google/default-system-instruction.ts +198 -0
  9. package/src/chat/google/message-file-processing.ts +212 -0
  10. package/src/chat/google/mira-step-preparation.ts +221 -0
  11. package/src/chat/google/new/route.ts +368 -0
  12. package/src/chat/google/route-auth.ts +81 -0
  13. package/src/chat/google/route-chat-resolution.ts +98 -0
  14. package/src/chat/google/route-credits.ts +61 -0
  15. package/src/chat/google/route-message-preparation.ts +331 -0
  16. package/src/chat/google/route-mira-runtime.ts +206 -0
  17. package/src/chat/google/route.ts +632 -0
  18. package/src/chat/google/stream-finish-persistence.ts +722 -0
  19. package/src/chat/google/summary/route.ts +153 -0
  20. package/src/chat/mira-render-ui-policy.ts +540 -0
  21. package/src/chat/mira-system-instruction.ts +484 -0
  22. package/src/chat-sdk/adapters.ts +389 -0
  23. package/src/chat-sdk/registry.ts +197 -0
  24. package/src/chat-sdk.ts +33 -0
  25. package/src/core.ts +3 -0
  26. package/src/credits/cap-output-tokens.ts +90 -0
  27. package/src/credits/check-credits.ts +232 -0
  28. package/src/credits/constants.ts +30 -0
  29. package/src/credits/index.ts +46 -0
  30. package/src/credits/model-mapping.ts +92 -0
  31. package/src/credits/reservations.ts +514 -0
  32. package/src/credits/resolve-plan-model.ts +219 -0
  33. package/src/credits/sync-gateway-models.ts +351 -0
  34. package/src/credits/types.ts +109 -0
  35. package/src/credits/use-ai-credits.ts +3 -0
  36. package/src/embeddings/metered.ts +283 -0
  37. package/src/executions/route.ts +137 -0
  38. package/src/generate/route.ts +411 -0
  39. package/src/hooks.ts +7 -0
  40. package/src/meetings/summary/route.ts +7 -0
  41. package/src/meetings/transcription/route.ts +134 -0
  42. package/src/memory/client.ts +158 -0
  43. package/src/memory/config.ts +38 -0
  44. package/src/memory/index.ts +32 -0
  45. package/src/memory/ingest.ts +51 -0
  46. package/src/memory/middleware.ts +35 -0
  47. package/src/memory/operations.ts +480 -0
  48. package/src/memory/scope.ts +102 -0
  49. package/src/memory/settings.ts +121 -0
  50. package/src/memory/types.ts +101 -0
  51. package/src/memory/workspace.ts +36 -0
  52. package/src/memory.ts +1 -0
  53. package/src/mind/patch.ts +146 -0
  54. package/src/mind/route.ts +687 -0
  55. package/src/mind/tools.ts +1500 -0
  56. package/src/mind/types.ts +20 -0
  57. package/src/object/core.ts +3 -0
  58. package/src/object/flashcards/route.ts +140 -0
  59. package/src/object/quizzes/explanation/route.ts +145 -0
  60. package/src/object/quizzes/route.ts +142 -0
  61. package/src/object/types.ts +187 -0
  62. package/src/object/year-plan/route.ts +196 -0
  63. package/src/react.ts +1 -0
  64. package/src/scheduling/algorithm.ts +791 -0
  65. package/src/scheduling/default.ts +36 -0
  66. package/src/scheduling/duration-optimizer.ts +689 -0
  67. package/src/scheduling/index.ts +79 -0
  68. package/src/scheduling/priority-calculator.ts +187 -0
  69. package/src/scheduling/recurrence-calculator.ts +621 -0
  70. package/src/scheduling/templates.ts +892 -0
  71. package/src/scheduling/types.ts +136 -0
  72. package/src/scheduling/web-adapter.ts +308 -0
  73. package/src/scheduling.ts +6 -0
  74. package/src/supported-actions.ts +1 -0
  75. package/src/supported-providers.ts +6 -0
  76. package/src/tools/context-builder.ts +372 -0
  77. package/src/tools/core.ts +1 -0
  78. package/src/tools/definitions/calendar.ts +106 -0
  79. package/src/tools/definitions/finance.ts +197 -0
  80. package/src/tools/definitions/image.ts +74 -0
  81. package/src/tools/definitions/memory.ts +83 -0
  82. package/src/tools/definitions/meta.ts +154 -0
  83. package/src/tools/definitions/render-ui.ts +81 -0
  84. package/src/tools/definitions/tasks.ts +343 -0
  85. package/src/tools/definitions/time-tracking.ts +381 -0
  86. package/src/tools/definitions/workspace-context.ts +45 -0
  87. package/src/tools/definitions/workspace-user-chat.ts +111 -0
  88. package/src/tools/executors/calendar.ts +371 -0
  89. package/src/tools/executors/chat.ts +15 -0
  90. package/src/tools/executors/finance.ts +638 -0
  91. package/src/tools/executors/helpers/encryption.ts +107 -0
  92. package/src/tools/executors/image.ts +247 -0
  93. package/src/tools/executors/markitdown.ts +684 -0
  94. package/src/tools/executors/memory.ts +277 -0
  95. package/src/tools/executors/parallel-checks.ts +176 -0
  96. package/src/tools/executors/qr.ts +170 -0
  97. package/src/tools/executors/scope-helpers.ts +192 -0
  98. package/src/tools/executors/search.ts +149 -0
  99. package/src/tools/executors/settings.ts +40 -0
  100. package/src/tools/executors/tasks.ts +1087 -0
  101. package/src/tools/executors/theme.ts +23 -0
  102. package/src/tools/executors/timer/timer-categories-executor.ts +110 -0
  103. package/src/tools/executors/timer/timer-category-mutations.ts +240 -0
  104. package/src/tools/executors/timer/timer-goal-mutations.ts +323 -0
  105. package/src/tools/executors/timer/timer-goals-executor.ts +272 -0
  106. package/src/tools/executors/timer/timer-helpers.ts +372 -0
  107. package/src/tools/executors/timer/timer-mutation-schemas.ts +160 -0
  108. package/src/tools/executors/timer/timer-mutation-types.ts +212 -0
  109. package/src/tools/executors/timer/timer-mutations.ts +19 -0
  110. package/src/tools/executors/timer/timer-queries.ts +18 -0
  111. package/src/tools/executors/timer/timer-session-lifecycle.ts +299 -0
  112. package/src/tools/executors/timer/timer-session-mutations.ts +10 -0
  113. package/src/tools/executors/timer/timer-session-queries.ts +153 -0
  114. package/src/tools/executors/timer/timer-session-updates.ts +200 -0
  115. package/src/tools/executors/timer/timer-sessions-executor.ts +91 -0
  116. package/src/tools/executors/timer/timer-stats-executor.ts +157 -0
  117. package/src/tools/executors/timer.ts +22 -0
  118. package/src/tools/executors/user.ts +60 -0
  119. package/src/tools/executors/workspace.ts +135 -0
  120. package/src/tools/json-render-catalog.ts +875 -0
  121. package/src/tools/mira-tool-definitions.ts +55 -0
  122. package/src/tools/mira-tool-dispatcher.ts +265 -0
  123. package/src/tools/mira-tool-metadata.ts +164 -0
  124. package/src/tools/mira-tool-names.ts +95 -0
  125. package/src/tools/mira-tool-render-ui.ts +54 -0
  126. package/src/tools/mira-tool-types.ts +17 -0
  127. package/src/tools/mira-tools.ts +167 -0
  128. package/src/tools/normalize-render-ui-input.ts +321 -0
  129. package/src/tools/workspace-context.ts +233 -0
  130. package/src/types.ts +38 -0
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type {
3
+ AiMemoryMetadata,
4
+ AiMemoryScope,
5
+ AiMemoryScopeInput,
6
+ } from './types';
7
+
8
+ const MAX_SUPERMEMORY_ID_LENGTH = 100;
9
+ const SAFE_ID_PATTERN = /[^a-zA-Z0-9._-]+/g;
10
+
11
+ function safeId(value: string, fallback: string) {
12
+ const normalized = value.replace(SAFE_ID_PATTERN, '_').replace(/_+/g, '_');
13
+ const safe = normalized.replace(/^_+|_+$/g, '');
14
+ return safe || fallback;
15
+ }
16
+
17
+ function compactId(value: string, fallback: string) {
18
+ const safe = safeId(value, fallback);
19
+ if (safe.length <= MAX_SUPERMEMORY_ID_LENGTH) return safe;
20
+
21
+ const hash = createHash('sha256').update(safe).digest('hex').slice(0, 12);
22
+ const prefix = safe.slice(0, MAX_SUPERMEMORY_ID_LENGTH - hash.length - 1);
23
+ return `${prefix}.${hash}`;
24
+ }
25
+
26
+ function metadataValueToSafeValue(value: AiMemoryMetadata[string]) {
27
+ if (Array.isArray(value)) {
28
+ return value
29
+ .filter((entry) => typeof entry === 'string')
30
+ .map((entry) => entry.slice(0, 300));
31
+ }
32
+
33
+ if (typeof value === 'string') return value.slice(0, 1000);
34
+ return value;
35
+ }
36
+
37
+ function normalizeMetadata(metadata?: AiMemoryMetadata): AiMemoryMetadata {
38
+ const normalized: AiMemoryMetadata = {};
39
+ for (const [key, value] of Object.entries(metadata ?? {})) {
40
+ const safeKey = safeId(key, 'metadata').slice(0, 80);
41
+ normalized[safeKey] = metadataValueToSafeValue(value);
42
+ }
43
+ return normalized;
44
+ }
45
+
46
+ export function resolveAiMemoryScope(
47
+ input: AiMemoryScopeInput
48
+ ): AiMemoryScope | null {
49
+ const userId = input.userId?.trim();
50
+ const wsId = input.wsId?.trim();
51
+ if (!userId || !wsId) return null;
52
+
53
+ const containerTag = compactId(
54
+ `tuturuuu.user.${userId}.workspace.${wsId}`,
55
+ 'tuturuuu.user.workspace'
56
+ );
57
+ const customId = compactId(
58
+ [
59
+ 'tuturuuu',
60
+ input.product,
61
+ input.surface,
62
+ input.customId?.trim() || input.source?.trim() || 'request',
63
+ ].join('.'),
64
+ 'tuturuuu.request'
65
+ );
66
+
67
+ const metadata = normalizeMetadata({
68
+ ...(input.metadata ?? {}),
69
+ product: input.product,
70
+ source: input.source ?? input.surface,
71
+ surface: input.surface,
72
+ userId,
73
+ wsId,
74
+ });
75
+
76
+ return {
77
+ containerTag,
78
+ customId,
79
+ metadata,
80
+ product: input.product,
81
+ source: input.source,
82
+ surface: input.surface,
83
+ userId,
84
+ wsId,
85
+ };
86
+ }
87
+
88
+ export function buildProductFilter(product: string) {
89
+ return {
90
+ filterType: 'metadata' as const,
91
+ key: 'product',
92
+ value: product,
93
+ };
94
+ }
95
+
96
+ export function buildKeyFilter(key: string) {
97
+ return {
98
+ filterType: 'metadata' as const,
99
+ key: 'memoryKey',
100
+ value: key,
101
+ };
102
+ }
@@ -0,0 +1,121 @@
1
+ import type { AiMemoryProduct, AiMemorySettings } from './types';
2
+
3
+ const DEFAULT_SETTINGS: AiMemorySettings = {
4
+ enabled: true,
5
+ productEnabled: true,
6
+ products: {},
7
+ };
8
+
9
+ type RpcSettingsRow = {
10
+ enabled?: boolean | null;
11
+ product_enabled?: boolean | null;
12
+ products?: Record<string, boolean> | null;
13
+ };
14
+
15
+ type SupabaseRpcClient = {
16
+ schema: (schema: string) => {
17
+ rpc: (
18
+ fn: string,
19
+ args: Record<string, unknown>
20
+ ) => Promise<{ data: unknown; error: { message?: string } | null }>;
21
+ };
22
+ };
23
+
24
+ async function createDefaultRpcClient() {
25
+ const { createAdminClient } = await import('@tuturuuu/supabase/next/server');
26
+ return (await createAdminClient()) as unknown as SupabaseRpcClient;
27
+ }
28
+
29
+ function normalizeSettings(
30
+ row: RpcSettingsRow | null | undefined,
31
+ product: AiMemoryProduct
32
+ ): AiMemorySettings {
33
+ const products = (row?.products ?? {}) as AiMemorySettings['products'];
34
+ const enabled = row?.enabled ?? DEFAULT_SETTINGS.enabled;
35
+ const productEnabled =
36
+ row?.product_enabled ??
37
+ products[product] ??
38
+ DEFAULT_SETTINGS.productEnabled;
39
+
40
+ return {
41
+ enabled,
42
+ productEnabled,
43
+ products,
44
+ };
45
+ }
46
+
47
+ export async function getAiMemorySettings({
48
+ db,
49
+ product,
50
+ userId,
51
+ wsId,
52
+ }: {
53
+ db?: SupabaseRpcClient;
54
+ product: AiMemoryProduct;
55
+ userId: string;
56
+ wsId: string;
57
+ }): Promise<AiMemorySettings> {
58
+ try {
59
+ const client = db ?? (await createDefaultRpcClient());
60
+ const { data, error } = await client
61
+ .schema('private')
62
+ .rpc('get_ai_memory_settings', {
63
+ p_product: product,
64
+ p_user_id: userId,
65
+ p_ws_id: wsId,
66
+ });
67
+
68
+ if (error) return DEFAULT_SETTINGS;
69
+
70
+ const row = Array.isArray(data) ? data[0] : data;
71
+ return normalizeSettings(row as RpcSettingsRow | null, product);
72
+ } catch {
73
+ return DEFAULT_SETTINGS;
74
+ }
75
+ }
76
+
77
+ export async function isAiMemoryEnabledForScope(args: {
78
+ db?: SupabaseRpcClient;
79
+ product: AiMemoryProduct;
80
+ userId: string;
81
+ wsId: string;
82
+ }) {
83
+ const settings = await getAiMemorySettings(args);
84
+ return settings.enabled && settings.productEnabled;
85
+ }
86
+
87
+ export async function disableAiMemoryForMeteringFailure({
88
+ db,
89
+ reason,
90
+ userId,
91
+ wsId,
92
+ }: {
93
+ db?: SupabaseRpcClient;
94
+ reason: string;
95
+ userId: string;
96
+ wsId: string;
97
+ }) {
98
+ try {
99
+ const client = db ?? (await createDefaultRpcClient());
100
+ await client.schema('private').rpc('upsert_ai_memory_settings', {
101
+ p_actor_user_id: userId,
102
+ p_enabled: false,
103
+ p_product_settings: {},
104
+ p_user_id: userId,
105
+ p_ws_id: wsId,
106
+ });
107
+ await client.schema('private').rpc('record_ai_memory_audit', {
108
+ p_action: 'settings_update',
109
+ p_actor_user_id: userId,
110
+ p_metadata: {
111
+ disabledBy: 'metered_embedding',
112
+ reason,
113
+ },
114
+ p_product: null,
115
+ p_user_id: userId,
116
+ p_ws_id: wsId,
117
+ });
118
+ } catch {
119
+ // Memory settings are fail-open elsewhere; disabling is best effort.
120
+ }
121
+ }
@@ -0,0 +1,101 @@
1
+ import type { LanguageModel } from 'ai';
2
+
3
+ export const AI_MEMORY_PRODUCTS = [
4
+ 'ai_agents',
5
+ 'ai_chat',
6
+ 'calendar',
7
+ 'education',
8
+ 'finance',
9
+ 'hive',
10
+ 'live_assistant',
11
+ 'meetings',
12
+ 'memories',
13
+ 'mind',
14
+ 'mira',
15
+ 'native_chat',
16
+ 'object_generation',
17
+ 'playground',
18
+ 'rewise',
19
+ 'tasks',
20
+ 'teach',
21
+ ] as const;
22
+
23
+ export type AiMemoryProduct = (typeof AI_MEMORY_PRODUCTS)[number];
24
+
25
+ export type AiMemoryMetadataValue = string | number | boolean | Array<string>;
26
+
27
+ export type AiMemoryMetadata = Record<string, AiMemoryMetadataValue>;
28
+
29
+ export type AiMemoryScopeInput = {
30
+ customId?: string | null;
31
+ metadata?: AiMemoryMetadata;
32
+ product: AiMemoryProduct;
33
+ source?: string | null;
34
+ surface: string;
35
+ userId?: string | null;
36
+ wsId?: string | null;
37
+ };
38
+
39
+ export type AiMemoryScope = {
40
+ containerTag: string;
41
+ customId: string;
42
+ metadata: AiMemoryMetadata;
43
+ product: AiMemoryProduct;
44
+ source?: string | null;
45
+ surface: string;
46
+ userId: string;
47
+ wsId: string;
48
+ };
49
+
50
+ export type AiMemorySettings = {
51
+ enabled: boolean;
52
+ productEnabled: boolean;
53
+ products: Partial<Record<AiMemoryProduct, boolean>>;
54
+ };
55
+
56
+ export type AiMemoryConfig = {
57
+ apiKey: string;
58
+ baseUrl?: string;
59
+ enabled: boolean;
60
+ failOpen: boolean;
61
+ timeoutMs: number;
62
+ };
63
+
64
+ export type AiMemoryResult<T> =
65
+ | { ok: true; skipped?: false; value: T }
66
+ | { ok: true; reason: string; skipped: true; value?: T }
67
+ | { error: string; ok: false; skipped?: false };
68
+
69
+ export type AiMemorySearchResult = {
70
+ id: string;
71
+ key?: string | null;
72
+ metadata: AiMemoryMetadata | null;
73
+ score: number;
74
+ updatedAt: string;
75
+ value: string;
76
+ };
77
+
78
+ export type AiMemoryDocument = {
79
+ category?: string | null;
80
+ content?: string | null;
81
+ id: string;
82
+ key?: string | null;
83
+ metadata: AiMemoryMetadata | null;
84
+ status: string;
85
+ summary?: string | null;
86
+ title?: string | null;
87
+ updatedAt: string;
88
+ };
89
+
90
+ export type AiMemoryModelOptions<TModel extends LanguageModel = LanguageModel> =
91
+ {
92
+ addMemory?: 'always' | 'never';
93
+ customId?: string | null;
94
+ mode?: 'profile' | 'query' | 'full';
95
+ model: TModel;
96
+ product: AiMemoryProduct;
97
+ source?: string | null;
98
+ surface: string;
99
+ userId?: string | null;
100
+ wsId?: string | null;
101
+ };
@@ -0,0 +1,36 @@
1
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
2
+ import { ROOT_WORKSPACE_ID } from '@tuturuuu/utils/constants';
3
+
4
+ export async function resolveAiMemoryWorkspaceIdForUser({
5
+ fallbackWsId = ROOT_WORKSPACE_ID,
6
+ supabase,
7
+ userId,
8
+ }: {
9
+ fallbackWsId?: string;
10
+ supabase: TypedSupabaseClient;
11
+ userId: string;
12
+ }) {
13
+ try {
14
+ const { data: userPrivateDetails } = await supabase
15
+ .from('user_private_details')
16
+ .select('default_workspace_id')
17
+ .eq('user_id', userId)
18
+ .maybeSingle();
19
+
20
+ if (userPrivateDetails?.default_workspace_id) {
21
+ return userPrivateDetails.default_workspace_id;
22
+ }
23
+
24
+ const { data: personalWorkspace } = await supabase
25
+ .from('workspaces')
26
+ .select('id, workspace_members!inner(user_id)')
27
+ .eq('personal', true)
28
+ .eq('workspace_members.user_id', userId)
29
+ .limit(1)
30
+ .maybeSingle();
31
+
32
+ return personalWorkspace?.id ?? fallbackWsId;
33
+ } catch {
34
+ return fallbackWsId;
35
+ }
36
+ }
package/src/memory.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './memory/index';
@@ -0,0 +1,146 @@
1
+ import type {
2
+ MindAiPatch,
3
+ MindBoardSnapshot,
4
+ MindEdge,
5
+ MindNode,
6
+ } from './types';
7
+
8
+ const NOW = '1970-01-01T00:00:00.000Z';
9
+
10
+ function createNode(node: Parameters<typeof normalizeNode>[0]) {
11
+ return normalizeNode(node);
12
+ }
13
+
14
+ function normalizeNode(
15
+ node: Partial<MindNode> &
16
+ Pick<MindNode, 'id' | 'positionX' | 'positionY' | 'title'>
17
+ ): MindNode {
18
+ return {
19
+ body: node.body ?? null,
20
+ color: node.color ?? null,
21
+ createdAt: node.createdAt ?? NOW,
22
+ height: node.height ?? 120,
23
+ horizon: node.horizon ?? 'year',
24
+ id: node.id,
25
+ metadata: node.metadata ?? {},
26
+ nodeType: node.nodeType ?? 'idea',
27
+ parentNodeId: node.parentNodeId ?? null,
28
+ positionX: node.positionX,
29
+ positionY: node.positionY,
30
+ status: node.status ?? 'planned',
31
+ title: node.title,
32
+ updatedAt: node.updatedAt ?? NOW,
33
+ width: node.width ?? 240,
34
+ };
35
+ }
36
+
37
+ function normalizeEdge(
38
+ edge: Partial<MindEdge> &
39
+ Pick<MindEdge, 'id' | 'sourceNodeId' | 'targetNodeId'>
40
+ ): MindEdge {
41
+ return {
42
+ color: edge.color ?? null,
43
+ createdAt: edge.createdAt ?? NOW,
44
+ edgeType: edge.edgeType ?? 'relates_to',
45
+ id: edge.id,
46
+ label: edge.label ?? null,
47
+ metadata: edge.metadata ?? {},
48
+ sourceNodeId: edge.sourceNodeId,
49
+ targetNodeId: edge.targetNodeId,
50
+ updatedAt: edge.updatedAt ?? NOW,
51
+ weight: edge.weight ?? 1,
52
+ };
53
+ }
54
+
55
+ export function applyMindPatchToSnapshot(
56
+ snapshot: MindBoardSnapshot,
57
+ patch: MindAiPatch
58
+ ): MindBoardSnapshot {
59
+ let nodes: MindNode[] = snapshot.nodes.map((node: MindNode) => ({ ...node }));
60
+ let edges: MindEdge[] = snapshot.edges.map((edge: MindEdge) => ({ ...edge }));
61
+
62
+ for (const operation of patch.operations) {
63
+ if (operation.kind === 'create_node') {
64
+ const nextNode = createNode(operation.node);
65
+ nodes = [
66
+ ...nodes.filter((node: MindNode) => node.id !== nextNode.id),
67
+ nextNode,
68
+ ];
69
+ continue;
70
+ }
71
+
72
+ if (operation.kind === 'update_node') {
73
+ nodes = nodes.map((node: MindNode) =>
74
+ node.id === operation.nodeId
75
+ ? {
76
+ ...node,
77
+ ...Object.fromEntries(
78
+ Object.entries(operation).filter(
79
+ ([key, value]) =>
80
+ key !== 'id' &&
81
+ key !== 'kind' &&
82
+ key !== 'nodeId' &&
83
+ value !== undefined
84
+ )
85
+ ),
86
+ }
87
+ : node
88
+ );
89
+ continue;
90
+ }
91
+
92
+ if (operation.kind === 'delete_node') {
93
+ nodes = nodes.filter((node: MindNode) => node.id !== operation.nodeId);
94
+ edges = edges.filter(
95
+ (edge: MindEdge) =>
96
+ edge.sourceNodeId !== operation.nodeId &&
97
+ edge.targetNodeId !== operation.nodeId
98
+ );
99
+ continue;
100
+ }
101
+
102
+ if (operation.kind === 'create_edge') {
103
+ const nextEdge = normalizeEdge(operation.edge);
104
+ edges = [
105
+ ...edges.filter((edge: MindEdge) => edge.id !== nextEdge.id),
106
+ nextEdge,
107
+ ];
108
+ continue;
109
+ }
110
+
111
+ if (operation.kind === 'update_edge') {
112
+ edges = edges.map((edge: MindEdge) =>
113
+ edge.id === operation.edgeId
114
+ ? {
115
+ ...edge,
116
+ ...Object.fromEntries(
117
+ Object.entries(operation).filter(
118
+ ([key, value]) =>
119
+ key !== 'edgeId' &&
120
+ key !== 'id' &&
121
+ key !== 'kind' &&
122
+ value !== undefined
123
+ )
124
+ ),
125
+ }
126
+ : edge
127
+ );
128
+ continue;
129
+ }
130
+
131
+ if (operation.kind === 'delete_edge') {
132
+ edges = edges.filter((edge: MindEdge) => edge.id !== operation.edgeId);
133
+ }
134
+ }
135
+
136
+ const nodeIds = new Set(nodes.map((node: MindNode) => node.id));
137
+
138
+ return {
139
+ ...snapshot,
140
+ edges: edges.filter(
141
+ (edge: MindEdge) =>
142
+ nodeIds.has(edge.sourceNodeId) && nodeIds.has(edge.targetNodeId)
143
+ ),
144
+ nodes,
145
+ };
146
+ }