kritzel-stencil 0.4.21 → 0.4.22

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 (29) hide show
  1. package/dist/cjs/index.cjs.js +50 -1
  2. package/dist/cjs/kritzel-active-users_47.cjs.entry.js +128 -31
  3. package/dist/collection/classes/providers/sync/hocuspocus-sync-provider.class.js +21 -0
  4. package/dist/collection/classes/providers/sync/websocket-sync-provider.class.js +29 -1
  5. package/dist/collection/classes/structures/app-state-map.structure.js +4 -1
  6. package/dist/collection/classes/structures/object-map.structure.js +66 -4
  7. package/dist/collection/components/core/kritzel-editor/kritzel-editor.js +42 -9
  8. package/dist/collection/components/core/kritzel-engine/kritzel-engine.js +44 -17
  9. package/dist/collection/constants/version.js +1 -1
  10. package/dist/components/index.js +51 -2
  11. package/dist/components/kritzel-editor.js +16 -11
  12. package/dist/components/kritzel-engine.js +1 -1
  13. package/dist/components/kritzel-settings.js +1 -1
  14. package/dist/components/{p-C_f_QElb.js → p-BNeE3SiZ.js} +113 -21
  15. package/dist/components/{p-BdCrcQ0_.js → p-Dx5NfJ0U.js} +1 -1
  16. package/dist/esm/index.js +50 -1
  17. package/dist/esm/kritzel-active-users_47.entry.js +128 -31
  18. package/dist/stencil/index.esm.js +50 -1
  19. package/dist/stencil/{p-832b80e9.entry.js → p-ab5c4292.entry.js} +128 -31
  20. package/dist/stencil/stencil.esm.js +1 -1
  21. package/dist/types/classes/providers/sync/hocuspocus-sync-provider.class.d.ts +5 -0
  22. package/dist/types/classes/providers/sync/websocket-sync-provider.class.d.ts +6 -0
  23. package/dist/types/classes/structures/object-map.structure.d.ts +26 -2
  24. package/dist/types/components/core/kritzel-editor/kritzel-editor.d.ts +3 -0
  25. package/dist/types/components/core/kritzel-engine/kritzel-engine.d.ts +9 -1
  26. package/dist/types/components.d.ts +8 -1
  27. package/dist/types/constants/version.d.ts +1 -1
  28. package/dist/types/interfaces/sync-provider.interface.d.ts +13 -0
  29. package/package.json +1 -1
@@ -43319,6 +43319,10 @@ class KritzelObjectMap {
43319
43319
  _awarenessChangeHandler = null;
43320
43320
  _awarenessChangeCallbacks = [];
43321
43321
  _objectsChangeCallbacks = [];
43322
+ _remoteChangesCallbacks = [];
43323
+ _docUpdateHandler = null;
43324
+ _remoteOrigins = new Set();
43325
+ _appliedRemoteChanges = false;
43322
43326
  _lastAwarenessEmitTime = 0;
43323
43327
  _awarenessEmitTimeout = null;
43324
43328
  AWARENESS_THROTTLE_INTERVAL = 100; // milliseconds
@@ -43342,12 +43346,44 @@ class KritzelObjectMap {
43342
43346
  return this._isReady;
43343
43347
  }
43344
43348
  /**
43345
- * Promise that settles when network providers finish connecting.
43346
- * Resolves immediately if no network providers. Awaited by the loading overlay.
43349
+ * Promise that settles when network providers finish connecting and complete
43350
+ * their Yjs sync handshake. Resolves immediately if no network providers.
43347
43351
  */
43348
43352
  whenNetworkSynced() {
43349
43353
  return this._networkSyncPromise ?? Promise.resolve();
43350
43354
  }
43355
+ /**
43356
+ * Whether any configured provider synchronizes over the network.
43357
+ */
43358
+ get hasNetworkSyncProvider() {
43359
+ return this._providers.some(provider => provider.type === 'network');
43360
+ }
43361
+ /**
43362
+ * Whether remote-originated changes can be told apart from local writes.
43363
+ * Requires every network provider to expose its Yjs update origin.
43364
+ */
43365
+ get supportsRemoteChangeDetection() {
43366
+ const networkProviders = this._providers.filter(provider => provider.type === 'network');
43367
+ return networkProviders.length > 0 && networkProviders.every(provider => provider.updateOrigin !== undefined);
43368
+ }
43369
+ /**
43370
+ * Whether at least one update originating from a network provider has been
43371
+ * applied to this document. Stays `false` when the remote held no data the
43372
+ * local document was missing, because Yjs suppresses empty updates.
43373
+ */
43374
+ get hasAppliedRemoteChanges() {
43375
+ return this._appliedRemoteChanges;
43376
+ }
43377
+ /**
43378
+ * Registers a callback invoked whenever a network provider applies changes to this document.
43379
+ * @returns A function that unregisters the callback.
43380
+ */
43381
+ onRemoteChangesApplied(callback) {
43382
+ this._remoteChangesCallbacks.push(callback);
43383
+ return () => {
43384
+ this._remoteChangesCallbacks = this._remoteChangesCallbacks.filter(registered => registered !== callback);
43385
+ };
43386
+ }
43351
43387
  /**
43352
43388
  * Returns the Yjs Awareness instance, if a network provider is available.
43353
43389
  */
@@ -43590,6 +43626,22 @@ class KritzelObjectMap {
43590
43626
  const providerList = this._providers.map(p => `${p.constructor.name} (${p.type})`).join(', ');
43591
43627
  console.info(`[Kritzel] Workspace sync providers initialized for ${docName}: ${providerList || 'none'}`);
43592
43628
  }
43629
+ // Registered before any provider connects so no remote update can be missed.
43630
+ for (const provider of this._providers) {
43631
+ if (provider.type === 'network' && provider.updateOrigin !== undefined) {
43632
+ this._remoteOrigins.add(provider.updateOrigin);
43633
+ }
43634
+ }
43635
+ this._docUpdateHandler = (_update, origin) => {
43636
+ if (!this._remoteOrigins.has(origin)) {
43637
+ return;
43638
+ }
43639
+ this._appliedRemoteChanges = true;
43640
+ for (const callback of [...this._remoteChangesCallbacks]) {
43641
+ callback();
43642
+ }
43643
+ };
43644
+ this._ydoc.on('update', this._docUpdateHandler);
43593
43645
  // captureTimeout is effectively infinite — undo-step boundaries are
43594
43646
  // marked explicitly via markUndoBoundary() in insert/remove/reset and
43595
43647
  // at stroke/gesture boundaries in the tools. This prevents a single
@@ -43643,12 +43695,15 @@ class KritzelObjectMap {
43643
43695
  if (showSyncProviderInfo && networkProviders.length > 0) {
43644
43696
  console.info(`[Kritzel] Workspace network providers connecting in background: ${networkProviders.length}`);
43645
43697
  }
43646
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
43698
+ const networkConnectPromises = networkProviders.map(provider => provider
43699
+ .connect()
43700
+ .then(() => provider.whenSynced?.())
43701
+ .catch(err => {
43647
43702
  if (showSyncProviderInfo) {
43648
43703
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
43649
43704
  }
43650
43705
  }));
43651
- // Network sync awaited by loading overlay to show remote workspace hydration
43706
+ // Settles once remote data has been reconciled, not merely once the transport connected
43652
43707
  this._networkSyncPromise = networkConnectPromises.length > 0 ? Promise.allSettled(networkConnectPromises).then(() => undefined) : null;
43653
43708
  this._isReady = true;
43654
43709
  // Find the first provider that exposes awareness (network providers)
@@ -44306,6 +44361,13 @@ class KritzelObjectMap {
44306
44361
  this._objectsMap.unobserve(this._objectsObserver);
44307
44362
  this._objectsObserver = null;
44308
44363
  }
44364
+ if (this._ydoc && this._docUpdateHandler) {
44365
+ this._ydoc.off('update', this._docUpdateHandler);
44366
+ }
44367
+ this._docUpdateHandler = null;
44368
+ this._remoteOrigins.clear();
44369
+ this._remoteChangesCallbacks = [];
44370
+ this._appliedRemoteChanges = false;
44309
44371
  // Remove undo manager event listeners
44310
44372
  if (this._undoManager) {
44311
44373
  if (this._stackItemAddedHandler) {
@@ -44801,7 +44863,10 @@ class KritzelAppStateMap {
44801
44863
  if (showSyncProviderInfo && networkProviders.length > 0) {
44802
44864
  console.info(`[Kritzel] App-state network providers connecting in background: ${networkProviders.length}`);
44803
44865
  }
44804
- const networkConnectPromises = networkProviders.map(provider => provider.connect().catch(err => {
44866
+ const networkConnectPromises = networkProviders.map(provider => provider
44867
+ .connect()
44868
+ .then(() => provider.whenSynced?.())
44869
+ .catch(err => {
44805
44870
  if (showSyncProviderInfo) {
44806
44871
  console.error(`[Kritzel] Network sync provider "${provider.constructor.name}" failed to connect:`, err);
44807
44872
  }
@@ -48905,7 +48970,7 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
48905
48970
  contextMenuStateChange;
48906
48971
  /** Emitted when the combined loading state (external `isLoading` prop or internal workspace loading) changes. */
48907
48972
  loadingChange;
48908
- /** Emitted when remote workspace synchronization is in progress. */
48973
+ /** Emitted while remote changes are being applied to the active workspace. Stays silent when the remote holds nothing new. */
48909
48974
  syncingChange;
48910
48975
  forceUpdate = 0;
48911
48976
  isSyncing = false;
@@ -50492,6 +50557,7 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
50492
50557
  _sceneBootstrapOpacity = 1;
50493
50558
  _sceneBootstrapTransitionEnabled = false;
50494
50559
  _sceneBootstrapTransitionTimer = null;
50560
+ _remoteChangesUnsubscribe = null;
50495
50561
  _defaultUndoState = {
50496
50562
  canUndo: false,
50497
50563
  canRedo: false,
@@ -50509,9 +50575,16 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
50509
50575
  this.loadingChange.emit(this.core.store.state.isLoading);
50510
50576
  }
50511
50577
  syncSyncingState(isSyncing) {
50578
+ if (this.isSyncing === isSyncing) {
50579
+ return;
50580
+ }
50512
50581
  this.isSyncing = isSyncing;
50513
50582
  this.syncingChange.emit(isSyncing);
50514
50583
  }
50584
+ clearRemoteChangesSubscription() {
50585
+ this._remoteChangesUnsubscribe?.();
50586
+ this._remoteChangesUnsubscribe = null;
50587
+ }
50515
50588
  isNetworkAvailable() {
50516
50589
  return typeof navigator !== 'undefined' && navigator.onLine;
50517
50590
  }
@@ -50627,6 +50700,7 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
50627
50700
  disconnectedCallback() {
50628
50701
  this.throttledPointerMoveMulti.cancel();
50629
50702
  this._revealedImageKeys.clear();
50703
+ this.clearRemoteChangesSubscription();
50630
50704
  if (this._sceneBootstrapTransitionTimer !== null) {
50631
50705
  clearTimeout(this._sceneBootstrapTransitionTimer);
50632
50706
  this._sceneBootstrapTransitionTimer = null;
@@ -50881,21 +50955,7 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
50881
50955
  this._workspaceInitializationTargetKey = targetKey;
50882
50956
  try {
50883
50957
  await initializationPromise;
50884
- // Track remote sync in the background without blocking the loading overlay.
50885
- const initializedObjects = this.core.store.objects;
50886
- const networkSyncPromise = initializedObjects?.whenNetworkSynced();
50887
- if (networkSyncPromise && this.isNetworkAvailable()) {
50888
- this.syncSyncingState(true);
50889
- void networkSyncPromise.finally(() => {
50890
- // Only clear syncing for the currently active object map.
50891
- if (this.core.store.objects === initializedObjects) {
50892
- this.syncSyncingState(false);
50893
- }
50894
- });
50895
- }
50896
- else {
50897
- this.syncSyncingState(false);
50898
- }
50958
+ this.trackRemoteSynchronization();
50899
50959
  }
50900
50960
  finally {
50901
50961
  // Only the most recent init clears loading state to prevent race conditions
@@ -50907,6 +50967,38 @@ const KritzelEngine = /*@__PURE__*/ proxyCustomElement(class KritzelEngine exten
50907
50967
  }
50908
50968
  }
50909
50969
  }
50970
+ /**
50971
+ * Reports syncing only while remote data is actually being reconciled into the
50972
+ * active workspace. Yjs suppresses updates that carry nothing new, so a workspace
50973
+ * that already matches the remote never reports syncing at all.
50974
+ */
50975
+ trackRemoteSynchronization() {
50976
+ this.clearRemoteChangesSubscription();
50977
+ this.syncSyncingState(false);
50978
+ const initializedObjects = this.core.store.objects;
50979
+ if (!initializedObjects?.hasNetworkSyncProvider || !this.isNetworkAvailable()) {
50980
+ return;
50981
+ }
50982
+ if (initializedObjects.supportsRemoteChangeDetection) {
50983
+ this._remoteChangesUnsubscribe = initializedObjects.onRemoteChangesApplied(() => {
50984
+ if (this.core.store.objects !== initializedObjects) {
50985
+ return;
50986
+ }
50987
+ this.syncSyncingState(true);
50988
+ });
50989
+ }
50990
+ else {
50991
+ // Custom providers without an update origin cannot be diffed, so fall back to reporting the whole handshake.
50992
+ this.syncSyncingState(true);
50993
+ }
50994
+ void initializedObjects.whenNetworkSynced().finally(() => {
50995
+ if (this.core.store.objects !== initializedObjects) {
50996
+ return;
50997
+ }
50998
+ this.clearRemoteChangesSubscription();
50999
+ this.syncSyncingState(false);
51000
+ });
51001
+ }
50910
51002
  emitObjectsChange() {
50911
51003
  const objectsMap = this.core.store.objects;
50912
51004
  if (!objectsMap) {
@@ -12,7 +12,7 @@ import { d as defineCustomElement$1 } from './p-BozrueXV.js';
12
12
  * This file is auto-generated by the version bump scripts.
13
13
  * Do not modify manually.
14
14
  */
15
- const KRITZEL_VERSION = '0.4.21';
15
+ const KRITZEL_VERSION = '0.4.22';
16
16
 
17
17
  const kritzelSettingsCss = () => `:host{display:contents}kritzel-dialog{--kritzel-dialog-body-padding:0;--kritzel-dialog-width-large:800px;--kritzel-dialog-height-large:500px}.footer-button{padding:8px 16px;border-radius:6px;cursor:pointer;font-size:14px}.cancel-button{border:1px solid #ebebeb;background:#fff;color:inherit}.cancel-button:hover{background:#f5f5f5}.settings-tab-panel{display:none}.settings-tab-panel.is-active{display:block}.settings-content{padding:0}.settings-content h3{margin:0 0 16px 0;font-size:18px;font-weight:600;color:var(--kritzel-settings-content-heading-color, #333333)}.settings-content p{margin:0;font-size:14px;color:var(--kritzel-settings-content-text-color, #666666);line-height:1.5}.settings-group{display:flex;flex-direction:column;gap:24px}.settings-item{display:flex;flex-direction:column;gap:8px}.settings-row{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-label{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.settings-description{font-size:12px;color:var(--kritzel-settings-description-color, #888888);margin:0;line-height:1.4}.shortcuts-list{display:flex;flex-direction:column;gap:24px}.shortcuts-category{display:flex;flex-direction:column;gap:8px}.shortcuts-category-title{font-size:14px;font-weight:600;color:var(--kritzel-settings-label-color, #333333);margin:0 0 4px 0}.shortcuts-group{display:flex;flex-direction:column;gap:4px}.shortcut-item{display:flex;justify-content:space-between;align-items:center;padding:6px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-item-bg, rgba(0, 0, 0, 0.02))}.shortcut-label{font-size:14px;color:var(--kritzel-settings-content-text-color, #666666)}.shortcut-key{font-family:monospace;font-size:12px;padding:2px 8px;border-radius:4px;background:var(--kritzel-settings-shortcut-key-bg, #f0f0f0);color:var(--kritzel-settings-shortcut-key-color, #333333);border:1px solid var(--kritzel-settings-shortcut-key-border, #ddd)}`;
18
18
 
package/dist/esm/index.js CHANGED
@@ -549,9 +549,15 @@ class WebSocketSyncProvider {
549
549
  provider;
550
550
  isConnected = false;
551
551
  _quiet = false;
552
+ _isSynced = false;
553
+ _syncedResolvers = [];
552
554
  get awareness() {
553
555
  return this.provider.awareness;
554
556
  }
557
+ /** y-websocket applies remote updates with the underlying provider as the Yjs transaction origin. */
558
+ get updateOrigin() {
559
+ return this.provider;
560
+ }
555
561
  constructor(docName, doc, options) {
556
562
  const url = options?.url || 'ws://localhost:1234';
557
563
  const roomName = options?.roomName || docName;
@@ -598,11 +604,29 @@ class WebSocketSyncProvider {
598
604
  }
599
605
  });
600
606
  this.provider.on('sync', (synced) => {
601
- if (synced && !this._quiet) {
607
+ if (!synced) {
608
+ return;
609
+ }
610
+ this._isSynced = true;
611
+ this.releaseSyncedWaiters();
612
+ if (!this._quiet) {
602
613
  console.info('WebSocket synced');
603
614
  }
604
615
  });
605
616
  }
617
+ releaseSyncedWaiters() {
618
+ const resolvers = this._syncedResolvers;
619
+ this._syncedResolvers = [];
620
+ resolvers.forEach(resolve => resolve());
621
+ }
622
+ async whenSynced() {
623
+ if (this._isSynced || this.provider.synced) {
624
+ return;
625
+ }
626
+ return new Promise(resolve => {
627
+ this._syncedResolvers.push(resolve);
628
+ });
629
+ }
606
630
  async connect() {
607
631
  if (this.isConnected) {
608
632
  return;
@@ -634,6 +658,8 @@ class WebSocketSyncProvider {
634
658
  this.provider.disconnect();
635
659
  }
636
660
  this.isConnected = false;
661
+ this._isSynced = false;
662
+ this.releaseSyncedWaiters();
637
663
  }
638
664
  async reconnect() {
639
665
  this.disconnect();
@@ -644,6 +670,8 @@ class WebSocketSyncProvider {
644
670
  this.provider.destroy();
645
671
  }
646
672
  this.isConnected = false;
673
+ this._isSynced = false;
674
+ this.releaseSyncedWaiters();
647
675
  }
648
676
  }
649
677
 
@@ -665,9 +693,14 @@ class HocuspocusSyncProvider {
665
693
  _connectionStatus = 'disconnected';
666
694
  visibilityHandler = null;
667
695
  onlineHandler = null;
696
+ syncedResolvers = [];
668
697
  get awareness() {
669
698
  return this.provider.awareness;
670
699
  }
700
+ /** HocuspocusProvider applies remote updates with itself as the Yjs transaction origin. */
701
+ get updateOrigin() {
702
+ return this.provider;
703
+ }
671
704
  get connectionStatus() {
672
705
  return this._connectionStatus;
673
706
  }
@@ -724,6 +757,7 @@ class HocuspocusSyncProvider {
724
757
  }
725
758
  this.isSynced = true;
726
759
  this._connectionStatus = 'synced';
760
+ this.releaseSyncedWaiters();
727
761
  if (!options?.quiet) {
728
762
  console.info(`Hocuspocus synced: ${name}`);
729
763
  }
@@ -934,6 +968,19 @@ class HocuspocusSyncProvider {
934
968
  this.disconnect();
935
969
  return this.connect();
936
970
  }
971
+ releaseSyncedWaiters() {
972
+ const resolvers = this.syncedResolvers;
973
+ this.syncedResolvers = [];
974
+ resolvers.forEach(resolve => resolve());
975
+ }
976
+ async whenSynced() {
977
+ if (this.isSynced || this.isDestroyed || this.provider.isSynced) {
978
+ return;
979
+ }
980
+ return new Promise(resolve => {
981
+ this.syncedResolvers.push(resolve);
982
+ });
983
+ }
937
984
  disconnect() {
938
985
  // Cancel any pending connection attempt
939
986
  if (this.connectTimeout) {
@@ -955,6 +1002,7 @@ class HocuspocusSyncProvider {
955
1002
  this.isConnected = false;
956
1003
  this.isSynced = false;
957
1004
  this._connectionStatus = 'disconnected';
1005
+ this.releaseSyncedWaiters();
958
1006
  }
959
1007
  destroy() {
960
1008
  // Mark as destroyed first to prevent any callbacks from doing work
@@ -974,6 +1022,7 @@ class HocuspocusSyncProvider {
974
1022
  this.isConnected = false;
975
1023
  this.isSynced = false;
976
1024
  this._connectionStatus = 'disconnected';
1025
+ this.releaseSyncedWaiters();
977
1026
  }
978
1027
  }
979
1028