@abraca/dabra 2.6.0 → 2.8.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/dist/index.d.ts CHANGED
@@ -4985,8 +4985,13 @@ declare class ContentManager {
4985
4985
  constructor(dm: DocumentManager);
4986
4986
  /**
4987
4987
  * Read document content as markdown.
4988
- * Returns the title extracted from the TipTap documentHeader, the markdown
4989
- * body, tree metadata, and immediate children.
4988
+ *
4989
+ * Returns the markdown body, tree-derived label/type/meta, and immediate
4990
+ * children. `title` mirrors `label` (the tree entry's display name) — it
4991
+ * is *not* derived from a TipTap `documentHeader`, and the markdown body
4992
+ * does NOT include YAML frontmatter. Callers that want frontmatter-style
4993
+ * round-tripping should serialise `meta`/`type` themselves on top of the
4994
+ * returned markdown.
4990
4995
  */
4991
4996
  read(docId: string): Promise<DocumentContent>;
4992
4997
  /**
@@ -5259,6 +5264,15 @@ declare function toPlain(val: unknown): unknown;
5259
5264
  * omitted; `null` is kept (a real value, e.g. top-level `parentId`).
5260
5265
  */
5261
5266
  declare function makeEntryMap(fields: Record<string, unknown>): Y.Map<unknown>;
5267
+ /**
5268
+ * A label is a "placeholder" — i.e. carries no real user title — when it is
5269
+ * empty/whitespace, null/undefined, or the literal `"Untitled"` sentinel
5270
+ * (case-insensitive, so `"untitled"` from a `labelToFilename` round-trip is
5271
+ * caught too). The whole tree-label corruption class boils down to a
5272
+ * placeholder being allowed to overwrite a real title; `patchEntry` refuses
5273
+ * exactly that. Mirrors cou-sh's `isEmptyTreeLabel`.
5274
+ */
5275
+ declare function isPlaceholderLabel(label: unknown): boolean;
5262
5276
  /**
5263
5277
  * Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
5264
5278
  * concurrent edit to a *different* field by a peer is preserved instead
@@ -5274,7 +5288,21 @@ declare function makeEntryMap(fields: Record<string, unknown>): Y.Map<unknown>;
5274
5288
  * Self-transacting: it batches its writes in one `Y.Doc` transaction
5275
5289
  * (a safe reentrant no-op join when already inside one), so callers
5276
5290
  * don't need to pass or own a transaction.
5291
+ *
5292
+ * ── NO-DESTROY LABEL INVARIANT ──────────────────────────────────────────
5293
+ * A `label` patch that is a placeholder (empty/whitespace/"Untitled") is
5294
+ * DROPPED when the entry already holds a real (non-placeholder) label —
5295
+ * regardless of which consumer (cou-sh title-sync, fs-sync rename
5296
+ * detection, MCP, table renderers, a stale snapshot) tried it. This is the
5297
+ * source-of-truth guard against the "card title silently becomes Untitled
5298
+ * / files renamed to untitled.md" corruption: a placeholder must never win
5299
+ * over a real title. Creating a brand-new entry with an empty label (e.g.
5300
+ * a fresh kanban card) is still allowed — the guard only fires when a real
5301
+ * label already exists. Pass `{ allowLabelClear: true }` to override (the
5302
+ * single legitimate "user explicitly cleared it" path).
5277
5303
  */
5278
- declare function patchEntry(treeMap: Y.Map<unknown>, id: string, patch: Record<string, unknown>, removeKeys?: string[]): void;
5304
+ declare function patchEntry(treeMap: Y.Map<unknown>, id: string, patch: Record<string, unknown>, removeKeys?: string[], opts?: {
5305
+ allowLabelClear?: boolean;
5306
+ }): void;
5279
5307
  //#endregion
5280
- export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AdminConfigField, AdminConfigOriginKind, AuditLogEntry, AuditQueryOpts, AuditVerifyResult, AuthFailureContext, AuthFailureReason, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, type ChatChannel, ChatClient, type ChatClientTransport, type MarkReadInput as ChatMarkReadInput, type ChatMessage, type ChatReadCursor, type ChatReadReceipt, type ChatTypingEvent, type ChildrenPage, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, ContentManager, type CreateNotificationInput, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, type DeleteMessageInput, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceSessionRecord, type DeviceSessionStorage, type DeviceTier, type DocEncryptionInfo, DocKeyManager, DocSearchHit, type DocSyncState, type DocumentBlock, DocumentCache, type DocumentCacheOptions, type DocumentContent, DocumentManager, type DocumentManagerConfig, DocumentMeta, type DocumentMetaInfo, type DocumentMetaWire, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, type EditMessageInput, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedChatClient, EncryptedYMap, EncryptedYText, EnvSnapshotExtension, EnvSnapshotItem, EnvSnapshotResponse, type FetchInboxInput, type FetchNotificationsInput, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, type FoldedMessage, Forbidden, GEO_TYPE_META_SCHEMAS, type GetChatHistoryInput, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, type InboxEntry, type MarkReadInput$1 as InboxMarkReadInput, InviteRow, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, type ManualSignalingBlob, type MessageRecord, MessageTooBig, MessageType, MetaFieldType, MetaManager, MetaValidationError, type NotificationReadUpdate, type NotificationRecord, NotificationsClient, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PAGE_TYPES, PageMeta, PageTypeInfo, PageTypeMetaField, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, QUERY_PREFIX, QueryClient, QueryError, type QueryFrame, type QueryKind, type QuerySpec, type QuerySubscriptionHandle, type QuerySubscriptionHandlers, type QueryTransport, RPC_PREFIX, ReadyzStatus, ResetConnection, type RpcCallHandle, type RpcCallOptions, RpcClient, RpcError, type RpcErrorCode, type RpcErrorPayload, type RpcFrame, type RpcHandler, type RpcHandlerContext, type RpcKind, type RpcTarget, type RpcTransport, SERVER_ROOT_ID, type SchemaDocTypeName, type SchemaMetaOf, type SchemaRegistryLike, type SchemaValidatorLike, SearchIndex, SearchResult, type SendChatMessageInput, type SendMessageInput, type SendMessageResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotFileEntry, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, StatesArray, SubdocMessage, SubdocRegisteredEvent, TYPE_ALIASES, TokenManager, type TokenManagerOptions, TreeEntry, TreeManager, TreeNode, TreeSearchResult, TypedDocTypeMismatchError, type TypedDocsClient, type TypedTreeEntry, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserMetaField, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onCompactedParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onServerErrorParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
5308
+ export { AbracadabraBaseProvider, AbracadabraBaseProviderConfiguration, AbracadabraClient, AbracadabraClientConfig, AbracadabraOutgoingMessageArguments, AbracadabraProvider, AbracadabraProviderConfiguration, AbracadabraWS, AbracadabraWSConfiguration, AbracadabraWebRTC, type AbracadabraWebRTCConfiguration, AbracadabraWebSocketConn, AdminConfigField, AdminConfigOriginKind, AuditLogEntry, AuditQueryOpts, AuditVerifyResult, AuthFailureContext, AuthFailureReason, AuthMessageType, AuthorizedScope, AwarenessError, BackgroundSyncManager, type BackgroundSyncManagerOptions, BackgroundSyncPersistence, BroadcastChannelSync, CHANNEL_NAMES, ChannelKeyResolver, type ChatChannel, ChatClient, type ChatClientTransport, type MarkReadInput as ChatMarkReadInput, type ChatMessage, type ChatReadCursor, type ChatReadReceipt, type ChatTypingEvent, type ChildrenPage, CloseEvent, CompleteAbracadabraBaseProviderConfiguration, CompleteAbracadabraWSConfiguration, CompleteHocuspocusProviderConfiguration, CompleteHocuspocusProviderWebsocketConfiguration, ConnectionTimeout, Constructable, ConstructableOutgoingMessage, ContentManager, type CreateNotificationInput, CryptoIdentity, CryptoIdentityKeystore, DEFAULT_FILE_CHUNK_SIZE, DEFAULT_ICE_SERVERS, DataChannelRouter, type DeleteMessageInput, DevicePairingChannel, type DevicePairingConfig, DeviceRegistrationService, type DeviceServerStatus, type DeviceSessionRecord, type DeviceSessionStorage, type DeviceTier, type DocEncryptionInfo, DocKeyManager, DocSearchHit, type DocSyncState, type DocumentBlock, DocumentCache, type DocumentCacheOptions, type DocumentContent, DocumentManager, type DocumentManagerConfig, DocumentMeta, type DocumentMetaInfo, type DocumentMetaWire, E2EAbracadabraProvider, E2EEChannel, type E2EEIdentity, E2EOfflineStore, type EditMessageInput, EffectivePermissionEntry, EffectivePermissionsResponse, EffectiveRole, EncryptedChatClient, EncryptedYMap, EncryptedYText, EnvSnapshotExtension, EnvSnapshotItem, EnvSnapshotResponse, type FetchInboxInput, type FetchNotificationsInput, FileBlobStore, FileTransferChannel, FileTransferHandle, type FileTransferMeta, type FileTransferStatus, type FoldedMessage, Forbidden, GEO_TYPE_META_SCHEMAS, type GetChatHistoryInput, HealthStatus, HocusPocusWebSocket, HocuspocusProvider, HocuspocusProviderConfiguration, HocuspocusProviderWebsocket, HocuspocusProviderWebsocketConfiguration, HocuspocusWebSocket, type IdentityDeviceEntry, type IdentityDocConfiguration, IdentityDocProvider, type IdentityProfile, type IdentityServerEntry, type IdentitySpaceEntry, type InboxEntry, type MarkReadInput$1 as InboxMarkReadInput, InviteRow, KEY_EXCHANGE_CHANNEL, Kind, LocalStorageDeviceSessionStorage, ManualSignaling, type ManualSignalingBlob, type MessageRecord, MessageTooBig, MessageType, MetaFieldType, MetaManager, MetaValidationError, type NotificationReadUpdate, type NotificationRecord, NotificationsClient, OfflineStore, OutgoingMessageArguments, OutgoingMessageInterface, PAGE_TYPES, PageMeta, PageTypeInfo, PageTypeMetaField, type PairingRequest, type PairingResult, PeerConnection, type PeerInfo, type PeerState, PendingSubdoc, PermissionEntry, PublicKeyInfo, QUERY_PREFIX, QueryClient, QueryError, type QueryFrame, type QueryKind, type QuerySpec, type QuerySubscriptionHandle, type QuerySubscriptionHandlers, type QueryTransport, RPC_PREFIX, ReadyzStatus, ResetConnection, type RpcCallHandle, type RpcCallOptions, RpcClient, RpcError, type RpcErrorCode, type RpcErrorPayload, type RpcFrame, type RpcHandler, type RpcHandlerContext, type RpcKind, type RpcTarget, type RpcTransport, SERVER_ROOT_ID, type SchemaDocTypeName, type SchemaMetaOf, type SchemaRegistryLike, type SchemaValidatorLike, SearchIndex, SearchResult, type SendChatMessageInput, type SendMessageInput, type SendMessageResult, ServerInfo, type SignalingIncoming, type SignalingOutgoing, SignalingSocket, SnapshotCreateResult, SnapshotData, SnapshotFileEntry, SnapshotForkResult, SnapshotMeta, SnapshotRestoreResult, StatesArray, SubdocMessage, SubdocRegisteredEvent, TYPE_ALIASES, TokenManager, type TokenManagerOptions, TreeEntry, TreeManager, TreeNode, TreeSearchResult, TypedDocTypeMismatchError, type TypedDocsClient, type TypedTreeEntry, Unauthorized, UploadInfo, UploadMeta, UploadQueueEntry, UploadQueueStatus, UserMetaField, UserProfile, WebSocketStatus, WsReadyStates, YjsDataChannel, attachUpdatedAtObserver, awarenessStatesToArray, wordlist as bip39Wordlist, buildBlockquoteElement, buildBlocksFromMarkdown, buildBulletListElement, buildCodeBlockElement, buildHeadingElement, buildHorizontalRuleElement, buildOrderedListElement, buildParagraphElement, buildTaskListElement, decryptChatContent, decryptField, deriveIdentityDocId, deriveSeedWrappingKey, encryptChatContent, encryptField, filenameToLabel, foldRecords, generateMnemonic, isEncryptedContent, isPlaceholderLabel, makeEncryptedYMap, makeEncryptedYText, makeEntryMap, mnemonicToEd25519Seed, mnemonicToKeyPair, normalizeRootId, onAuthenticatedParameters, onAuthenticationFailedParameters, onAwarenessChangeParameters, onAwarenessUpdateParameters, onCloseParameters, onCompactedParameters, onDisconnectParameters, onMessageParameters, onOpenParameters, onOutgoingMessageParameters, onServerErrorParameters, onStatelessParameters, onStatusParameters, onSubdocLoadedParameters, onSubdocRegisteredParameters, onSyncedParameters, onUnsyncedChangesParameters, parseFrontmatter, patchEntry, populateYDocFromMarkdown, readAuthMessage, readBlocksFromFragment, recordFromYAny, resolvePageType, toPlain, unwrapSeed, validateMnemonic, waitForSync, withTimeout, wrapSeed, writeAuthenticated, writeAuthentication, writePermissionDenied, writeTokenSyncRequest, yjsToMarkdown };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/dabra",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "abracadabra provider",
5
5
  "keywords": [
6
6
  "abracadabra",
@@ -41,7 +41,7 @@
41
41
  "yjs": "^13.6.8"
42
42
  },
43
43
  "devDependencies": {
44
- "@abraca/schema": "2.6.0"
44
+ "@abraca/schema": "2.8.0"
45
45
  },
46
46
  "scripts": {
47
47
  "test": "node --no-warnings --conditions=source --experimental-transform-types --test 'tests/*.test.ts'"
@@ -14,6 +14,7 @@ import { StatelessMessage } from "./OutgoingMessages/StatelessMessage.ts";
14
14
  import { RpcClient } from "./RpcClient.ts";
15
15
  import { QueryClient } from "./QueryClient.ts";
16
16
  import type { QuerySpec, QuerySubscriptionHandlers, QuerySubscriptionHandle } from "./QueryClient.ts";
17
+ import { QueryAwarenessMessage } from "./OutgoingMessages/QueryAwarenessMessage.ts";
17
18
  import { SyncStepOneMessage } from "./OutgoingMessages/SyncStepOneMessage.ts";
18
19
  import { UpdateMessage } from "./OutgoingMessages/UpdateMessage.ts";
19
20
  import type {
@@ -516,6 +517,20 @@ export class AbracadabraBaseProvider extends EventEmitter {
516
517
  documentName: this.configuration.name,
517
518
  });
518
519
  }
520
+
521
+ // Pull existing peers' awareness on (re)connect. The server keeps no
522
+ // awareness cache and never replays presence to a fresh joiner, so without
523
+ // this a late-joining client only learns about a peer once that peer next
524
+ // *changes* its own awareness — producing asymmetric presence ("A sees B
525
+ // but B can't see A"). The server re-broadcasts this query to all peers,
526
+ // who answer with their full state (MessageReceiver.applyQueryAwareness),
527
+ // and those replies fan back to us. Matches stock y-websocket on-open
528
+ // behaviour. Only meaningful when an awareness instance exists.
529
+ if (this.awareness) {
530
+ this.send(QueryAwarenessMessage, {
531
+ documentName: this.configuration.name,
532
+ });
533
+ }
519
534
  }
520
535
 
521
536
  send(message: ConstructableOutgoingMessage, args: any) {
@@ -44,8 +44,13 @@ export class ContentManager {
44
44
 
45
45
  /**
46
46
  * Read document content as markdown.
47
- * Returns the title extracted from the TipTap documentHeader, the markdown
48
- * body, tree metadata, and immediate children.
47
+ *
48
+ * Returns the markdown body, tree-derived label/type/meta, and immediate
49
+ * children. `title` mirrors `label` (the tree entry's display name) — it
50
+ * is *not* derived from a TipTap `documentHeader`, and the markdown body
51
+ * does NOT include YAML frontmatter. Callers that want frontmatter-style
52
+ * round-tripping should serialise `meta`/`type` themselves on top of the
53
+ * returned markdown.
49
54
  */
50
55
  async read(docId: string): Promise<DocumentContent> {
51
56
  const provider = await this.dm.getChildProvider(docId);
@@ -95,10 +100,7 @@ export class ContentManager {
95
100
  meta,
96
101
  }));
97
102
 
98
- // yjsToMarkdown returns a string and takes the resolved label/meta/type
99
- // so frontmatter round-trips. (Older code destructured an object and
100
- // passed only the fragment — that silently produced `undefined`.)
101
- const markdown = yjsToMarkdown(fragment, label, meta, type);
103
+ const { markdown } = yjsToMarkdown(fragment);
102
104
  const title = label;
103
105
 
104
106
  return { label, type, meta, title, markdown, children };
package/src/DocUtils.ts CHANGED
@@ -104,6 +104,20 @@ export function makeEntryMap(
104
104
  return m;
105
105
  }
106
106
 
107
+ /**
108
+ * A label is a "placeholder" — i.e. carries no real user title — when it is
109
+ * empty/whitespace, null/undefined, or the literal `"Untitled"` sentinel
110
+ * (case-insensitive, so `"untitled"` from a `labelToFilename` round-trip is
111
+ * caught too). The whole tree-label corruption class boils down to a
112
+ * placeholder being allowed to overwrite a real title; `patchEntry` refuses
113
+ * exactly that. Mirrors cou-sh's `isEmptyTreeLabel`.
114
+ */
115
+ export function isPlaceholderLabel(label: unknown): boolean {
116
+ if (typeof label !== "string") return label == null;
117
+ const t = label.trim();
118
+ return t === "" || t.toLowerCase() === "untitled";
119
+ }
120
+
107
121
  /**
108
122
  * Patch an EXISTING entry's fields per-key on its nested `Y.Map`, so a
109
123
  * concurrent edit to a *different* field by a peer is preserved instead
@@ -119,17 +133,62 @@ export function makeEntryMap(
119
133
  * Self-transacting: it batches its writes in one `Y.Doc` transaction
120
134
  * (a safe reentrant no-op join when already inside one), so callers
121
135
  * don't need to pass or own a transaction.
136
+ *
137
+ * ── NO-DESTROY LABEL INVARIANT ──────────────────────────────────────────
138
+ * A `label` patch that is a placeholder (empty/whitespace/"Untitled") is
139
+ * DROPPED when the entry already holds a real (non-placeholder) label —
140
+ * regardless of which consumer (cou-sh title-sync, fs-sync rename
141
+ * detection, MCP, table renderers, a stale snapshot) tried it. This is the
142
+ * source-of-truth guard against the "card title silently becomes Untitled
143
+ * / files renamed to untitled.md" corruption: a placeholder must never win
144
+ * over a real title. Creating a brand-new entry with an empty label (e.g.
145
+ * a fresh kanban card) is still allowed — the guard only fires when a real
146
+ * label already exists. Pass `{ allowLabelClear: true }` to override (the
147
+ * single legitimate "user explicitly cleared it" path).
122
148
  */
123
149
  export function patchEntry(
124
150
  treeMap: Y.Map<unknown>,
125
151
  id: string,
126
152
  patch: Record<string, unknown>,
127
153
  removeKeys: string[] = [],
154
+ opts: { allowLabelClear?: boolean } = {},
128
155
  ): void {
129
156
  const apply = (): void => {
130
157
  const raw = treeMap.get(id);
158
+
159
+ // NO-DESTROY label guard: strip a placeholder `label` from the patch
160
+ // when a real label already exists on the entry. The existing-label
161
+ // read is DUCK-TYPED (not `instanceof Y.Map`): under a host that
162
+ // resolves a second physical yjs (cou-sh's local file-link), a
163
+ // foreign Y.Map fails `instanceof` and would read `undefined` —
164
+ // disabling the guard in the very environment that needs it. `.get`
165
+ // / `.toJSON` work regardless of which yjs constructed the map.
166
+ let effectivePatch = patch;
167
+ if (
168
+ !opts.allowLabelClear &&
169
+ Object.hasOwn(patch, "label") &&
170
+ isPlaceholderLabel(patch.label)
171
+ ) {
172
+ let existingLabel: unknown;
173
+ if (raw != null && typeof (raw as { get?: unknown }).get === "function") {
174
+ existingLabel = (raw as Y.Map<unknown>).get("label");
175
+ } else if (
176
+ raw != null &&
177
+ typeof (raw as { toJSON?: unknown }).toJSON === "function"
178
+ ) {
179
+ existingLabel = ((raw as { toJSON: () => Record<string, unknown> }).toJSON())
180
+ ?.label;
181
+ } else if (raw != null && typeof raw === "object") {
182
+ existingLabel = (raw as Record<string, unknown>).label;
183
+ }
184
+ if (!isPlaceholderLabel(existingLabel)) {
185
+ const { label: _dropped, ...rest } = patch;
186
+ effectivePatch = rest;
187
+ }
188
+ }
189
+
131
190
  if (raw instanceof Y.Map) {
132
- for (const [k, v] of Object.entries(patch)) {
191
+ for (const [k, v] of Object.entries(effectivePatch)) {
133
192
  if (v === undefined) raw.delete(k);
134
193
  else raw.set(k, v);
135
194
  }
@@ -138,8 +197,8 @@ export function patchEntry(
138
197
  }
139
198
  const base =
140
199
  raw == null ? {} : (toPlain(raw) as Record<string, unknown>);
141
- const merged: Record<string, unknown> = { ...base, ...patch };
142
- for (const [k, v] of Object.entries(patch)) {
200
+ const merged: Record<string, unknown> = { ...base, ...effectivePatch };
201
+ for (const [k, v] of Object.entries(effectivePatch)) {
143
202
  if (v === undefined) delete merged[k];
144
203
  }
145
204
  for (const k of removeKeys) delete merged[k];
package/src/index.ts CHANGED
@@ -152,6 +152,7 @@ export {
152
152
  toPlain,
153
153
  makeEntryMap,
154
154
  patchEntry,
155
+ isPlaceholderLabel,
155
156
  } from "./DocUtils.ts";
156
157
  export {
157
158
  yjsToMarkdown,