@korajs/store 0.5.0 → 0.6.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.js CHANGED
@@ -2568,6 +2568,482 @@ var Store = class {
2568
2568
  }
2569
2569
  }
2570
2570
  };
2571
+
2572
+ // src/reactivity/query-store.ts
2573
+ var EMPTY_ARRAY = Object.freeze([]);
2574
+ var QueryStore = class {
2575
+ snapshot = EMPTY_ARRAY;
2576
+ listeners = /* @__PURE__ */ new Set();
2577
+ unsubscribeQuery = null;
2578
+ active = false;
2579
+ queryBuilder;
2580
+ constructor(queryBuilder) {
2581
+ this.queryBuilder = queryBuilder;
2582
+ }
2583
+ /**
2584
+ * Subscribe to snapshot changes.
2585
+ *
2586
+ * @returns Unsubscribe function
2587
+ */
2588
+ subscribe = (onStoreChange) => {
2589
+ this.listeners.add(onStoreChange);
2590
+ if (!this.active) {
2591
+ this.startSubscription();
2592
+ }
2593
+ return () => {
2594
+ this.listeners.delete(onStoreChange);
2595
+ if (this.listeners.size === 0) {
2596
+ this.stopSubscription();
2597
+ }
2598
+ };
2599
+ };
2600
+ /** Synchronous read of the latest query results. */
2601
+ getSnapshot = () => {
2602
+ return this.snapshot;
2603
+ };
2604
+ /** Tear down listeners and the underlying query subscription. */
2605
+ destroy() {
2606
+ this.stopSubscription();
2607
+ this.listeners.clear();
2608
+ this.snapshot = EMPTY_ARRAY;
2609
+ }
2610
+ startSubscription() {
2611
+ this.active = true;
2612
+ this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
2613
+ if (!this.active) return;
2614
+ this.snapshot = Object.freeze([...results]);
2615
+ this.notifyListeners();
2616
+ });
2617
+ }
2618
+ stopSubscription() {
2619
+ this.active = false;
2620
+ if (this.unsubscribeQuery) {
2621
+ this.unsubscribeQuery();
2622
+ this.unsubscribeQuery = null;
2623
+ }
2624
+ }
2625
+ notifyListeners() {
2626
+ for (const listener of this.listeners) {
2627
+ listener();
2628
+ }
2629
+ }
2630
+ };
2631
+
2632
+ // src/reactivity/assert-query-ready.ts
2633
+ import { AppNotReadyError } from "@korajs/core";
2634
+ var LEGACY_PENDING_COLLECTION = "__pending__";
2635
+ function assertQueryReady(query) {
2636
+ const descriptor = query.getDescriptor();
2637
+ if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
2638
+ throw new AppNotReadyError(
2639
+ "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
2640
+ );
2641
+ }
2642
+ }
2643
+
2644
+ // src/reactivity/query-store-cache.ts
2645
+ var QueryStoreCache = class {
2646
+ constructor(scopeKey = "default") {
2647
+ this.scopeKey = scopeKey;
2648
+ }
2649
+ scopeKey;
2650
+ entries = /* @__PURE__ */ new Map();
2651
+ getOrCreate(queryBuilder) {
2652
+ const key = this.getKey(queryBuilder);
2653
+ const existing = this.entries.get(key);
2654
+ if (existing) {
2655
+ existing.refCount++;
2656
+ return existing.queryStore;
2657
+ }
2658
+ const queryStore = new QueryStore(queryBuilder);
2659
+ this.entries.set(key, {
2660
+ queryStore,
2661
+ refCount: 1
2662
+ });
2663
+ return queryStore;
2664
+ }
2665
+ release(queryBuilder) {
2666
+ const key = this.getKey(queryBuilder);
2667
+ const entry = this.entries.get(key);
2668
+ if (!entry) {
2669
+ return;
2670
+ }
2671
+ entry.refCount--;
2672
+ if (entry.refCount <= 0) {
2673
+ entry.queryStore.destroy();
2674
+ this.entries.delete(key);
2675
+ }
2676
+ }
2677
+ clear() {
2678
+ for (const entry of this.entries.values()) {
2679
+ entry.queryStore.destroy();
2680
+ }
2681
+ this.entries.clear();
2682
+ }
2683
+ get size() {
2684
+ return this.entries.size;
2685
+ }
2686
+ getKey(queryBuilder) {
2687
+ return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
2688
+ }
2689
+ };
2690
+ var sharedCache = null;
2691
+ function getSharedQueryStoreCache() {
2692
+ if (!sharedCache) {
2693
+ sharedCache = new QueryStoreCache();
2694
+ }
2695
+ return sharedCache;
2696
+ }
2697
+
2698
+ // src/richtext/create-richtext-controller.ts
2699
+ import * as Y from "yjs";
2700
+ var LOAD_ORIGIN = "kora-load";
2701
+ var REMOTE_ORIGIN = "kora-remote";
2702
+ var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
2703
+ var TEXT_KEY = "content";
2704
+ var COMPACT_AFTER_DELTAS = 20;
2705
+ var PERSIST_DEBOUNCE_MS = 400;
2706
+ function createRichTextController(options) {
2707
+ const {
2708
+ collection,
2709
+ collectionName,
2710
+ recordId,
2711
+ fieldName,
2712
+ store,
2713
+ syncEngine = null,
2714
+ useDocChannel
2715
+ } = options;
2716
+ const doc = new Y.Doc();
2717
+ const text = doc.getText(TEXT_KEY);
2718
+ const undoManager = new Y.UndoManager(text);
2719
+ let user = options.user;
2720
+ let disposed = false;
2721
+ let ready = false;
2722
+ let error = null;
2723
+ let canUndo = false;
2724
+ let canRedo = false;
2725
+ let cursors = [];
2726
+ const listeners = /* @__PURE__ */ new Set();
2727
+ let snapshot = {
2728
+ ready: false,
2729
+ error: null,
2730
+ canUndo: false,
2731
+ canRedo: false,
2732
+ cursors: []
2733
+ };
2734
+ const baseUpdateRef = { current: null };
2735
+ const pendingDeltasRef = { current: [] };
2736
+ const docChannelActiveRef = { current: false };
2737
+ let persistTimer = null;
2738
+ let recordUnsubscribe = null;
2739
+ let docChannelUnsubscribe = null;
2740
+ let awarenessUnsubscribe = null;
2741
+ const refreshSnapshot = () => {
2742
+ snapshot = {
2743
+ ready,
2744
+ error,
2745
+ canUndo,
2746
+ canRedo,
2747
+ cursors: [...cursors]
2748
+ };
2749
+ };
2750
+ const notify = () => {
2751
+ refreshSnapshot();
2752
+ for (const listener of listeners) {
2753
+ listener();
2754
+ }
2755
+ };
2756
+ const syncHistoryState = () => {
2757
+ canUndo = undoManager.undoStack.length > 0;
2758
+ canRedo = undoManager.redoStack.length > 0;
2759
+ notify();
2760
+ };
2761
+ const getSnapshot = () => snapshot;
2762
+ const resolveAwarenessUser = () => {
2763
+ if (user) {
2764
+ return user;
2765
+ }
2766
+ if (syncEngine) {
2767
+ const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
2768
+ if (existing) {
2769
+ return existing;
2770
+ }
2771
+ }
2772
+ return {
2773
+ name: "Anonymous",
2774
+ color: "#6366f1"
2775
+ };
2776
+ };
2777
+ const setCursor = (anchor, head) => {
2778
+ if (!syncEngine) {
2779
+ return;
2780
+ }
2781
+ const awareness = syncEngine.getAwarenessManager();
2782
+ const state = {
2783
+ user: resolveAwarenessUser(),
2784
+ cursor: {
2785
+ collection: collectionName,
2786
+ recordId,
2787
+ field: fieldName,
2788
+ anchor,
2789
+ head
2790
+ }
2791
+ };
2792
+ awareness.setLocalState(state);
2793
+ };
2794
+ const clearCursor = () => {
2795
+ if (!syncEngine) {
2796
+ return;
2797
+ }
2798
+ const awareness = syncEngine.getAwarenessManager();
2799
+ const current = awareness.getLocalState();
2800
+ if (!current?.cursor) {
2801
+ return;
2802
+ }
2803
+ if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
2804
+ return;
2805
+ }
2806
+ awareness.setLocalState({ user: current.user });
2807
+ };
2808
+ const applyRemoteSnapshot = (value) => {
2809
+ const encoded = encodeRichtextInput(value);
2810
+ if (!encoded) {
2811
+ return;
2812
+ }
2813
+ const currentSnapshot = Y.encodeStateAsUpdate(doc);
2814
+ if (updatesEqual(currentSnapshot, encoded)) {
2815
+ return;
2816
+ }
2817
+ Y.applyUpdate(doc, encoded, REMOTE_ORIGIN);
2818
+ baseUpdateRef.current = encoded;
2819
+ pendingDeltasRef.current = [];
2820
+ syncHistoryState();
2821
+ };
2822
+ const flushPersist = async () => {
2823
+ const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
2824
+ if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
2825
+ baseUpdateRef.current = snapshot2;
2826
+ pendingDeltasRef.current = [];
2827
+ }
2828
+ try {
2829
+ await collection.update(recordId, {
2830
+ [fieldName]: snapshot2
2831
+ });
2832
+ } catch (cause) {
2833
+ if (!disposed) {
2834
+ error = cause instanceof Error ? cause : new Error(String(cause));
2835
+ notify();
2836
+ }
2837
+ }
2838
+ };
2839
+ const schedulePersist = () => {
2840
+ if (persistTimer) {
2841
+ clearTimeout(persistTimer);
2842
+ }
2843
+ persistTimer = setTimeout(() => {
2844
+ persistTimer = null;
2845
+ void flushPersist();
2846
+ }, PERSIST_DEBOUNCE_MS);
2847
+ };
2848
+ const onDocUpdate = (update, origin) => {
2849
+ syncHistoryState();
2850
+ if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
2851
+ return;
2852
+ }
2853
+ pendingDeltasRef.current.push(update);
2854
+ if (docChannelActiveRef.current && syncEngine) {
2855
+ syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
2856
+ schedulePersist();
2857
+ return;
2858
+ }
2859
+ void flushPersist();
2860
+ };
2861
+ const subscribeRecordChanges = () => {
2862
+ recordUnsubscribe?.();
2863
+ recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
2864
+ const record = results[0];
2865
+ if (!record) {
2866
+ return;
2867
+ }
2868
+ applyRemoteSnapshot(record[fieldName]);
2869
+ });
2870
+ };
2871
+ const subscribeDocChannel = () => {
2872
+ docChannelUnsubscribe?.();
2873
+ if (!ready || !syncEngine || !docChannelActiveRef.current) {
2874
+ return;
2875
+ }
2876
+ const channel = syncEngine.getRichtextDocChannel?.();
2877
+ if (!channel) {
2878
+ return;
2879
+ }
2880
+ docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
2881
+ Y.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
2882
+ syncHistoryState();
2883
+ });
2884
+ };
2885
+ const updateCursors = () => {
2886
+ if (!syncEngine) {
2887
+ cursors = [];
2888
+ notify();
2889
+ return;
2890
+ }
2891
+ const awareness = syncEngine.getAwarenessManager();
2892
+ const localClientId = awareness.clientId;
2893
+ const states = awareness.getStates();
2894
+ const fieldCursors = [];
2895
+ for (const [clientId, state] of states) {
2896
+ if (clientId === localClientId) continue;
2897
+ if (!state.cursor) continue;
2898
+ if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
2899
+ continue;
2900
+ }
2901
+ fieldCursors.push({
2902
+ clientId,
2903
+ userName: state.user.name,
2904
+ color: state.user.color,
2905
+ anchor: state.cursor.anchor,
2906
+ head: state.cursor.head
2907
+ });
2908
+ }
2909
+ cursors = fieldCursors;
2910
+ notify();
2911
+ };
2912
+ const subscribeAwareness = () => {
2913
+ awarenessUnsubscribe?.();
2914
+ if (!syncEngine) {
2915
+ cursors = [];
2916
+ notify();
2917
+ return;
2918
+ }
2919
+ const awareness = syncEngine.getAwarenessManager();
2920
+ awarenessUnsubscribe = awareness.on("change", () => {
2921
+ updateCursors();
2922
+ });
2923
+ updateCursors();
2924
+ };
2925
+ const initialize = async () => {
2926
+ ready = false;
2927
+ error = null;
2928
+ notify();
2929
+ try {
2930
+ const record = await collection.findById(recordId);
2931
+ if (disposed) return;
2932
+ doc.transact(() => {
2933
+ const target = doc.getText(TEXT_KEY);
2934
+ target.delete(0, target.length);
2935
+ }, LOAD_ORIGIN);
2936
+ const encoded = encodeRichtextInput(record?.[fieldName]);
2937
+ baseUpdateRef.current = encoded;
2938
+ pendingDeltasRef.current = [];
2939
+ if (encoded) {
2940
+ Y.applyUpdate(doc, encoded, LOAD_ORIGIN);
2941
+ }
2942
+ const channel = syncEngine?.getRichtextDocChannel?.();
2943
+ docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
2944
+ ready = true;
2945
+ syncHistoryState();
2946
+ subscribeRecordChanges();
2947
+ subscribeDocChannel();
2948
+ subscribeAwareness();
2949
+ notify();
2950
+ } catch (cause) {
2951
+ if (disposed) return;
2952
+ error = cause instanceof Error ? cause : new Error(String(cause));
2953
+ notify();
2954
+ }
2955
+ };
2956
+ doc.on("update", onDocUpdate);
2957
+ void initialize();
2958
+ return {
2959
+ doc,
2960
+ text,
2961
+ getSnapshot,
2962
+ subscribe(listener) {
2963
+ listeners.add(listener);
2964
+ return () => {
2965
+ listeners.delete(listener);
2966
+ };
2967
+ },
2968
+ undo() {
2969
+ undoManager.undo();
2970
+ syncHistoryState();
2971
+ },
2972
+ redo() {
2973
+ undoManager.redo();
2974
+ syncHistoryState();
2975
+ },
2976
+ setCursor,
2977
+ clearCursor,
2978
+ setUser(nextUser) {
2979
+ user = nextUser;
2980
+ },
2981
+ destroy() {
2982
+ if (disposed) {
2983
+ return;
2984
+ }
2985
+ disposed = true;
2986
+ if (persistTimer) {
2987
+ clearTimeout(persistTimer);
2988
+ persistTimer = null;
2989
+ }
2990
+ doc.off("update", onDocUpdate);
2991
+ recordUnsubscribe?.();
2992
+ docChannelUnsubscribe?.();
2993
+ awarenessUnsubscribe?.();
2994
+ clearCursor();
2995
+ undoManager.destroy();
2996
+ baseUpdateRef.current = null;
2997
+ pendingDeltasRef.current = [];
2998
+ docChannelActiveRef.current = false;
2999
+ listeners.clear();
3000
+ }
3001
+ };
3002
+ }
3003
+ function encodeRichtextInput(value) {
3004
+ if (value === null || value === void 0) {
3005
+ return null;
3006
+ }
3007
+ try {
3008
+ return encodeRichtext(value);
3009
+ } catch {
3010
+ if (typeof value === "string") {
3011
+ const fallbackDoc = new Y.Doc();
3012
+ fallbackDoc.getText(TEXT_KEY).insert(0, value);
3013
+ return Y.encodeStateAsUpdate(fallbackDoc);
3014
+ }
3015
+ throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
3016
+ }
3017
+ }
3018
+ function composeRichtextSnapshot(base, deltas) {
3019
+ const mergedDoc = new Y.Doc();
3020
+ if (base) {
3021
+ Y.applyUpdate(mergedDoc, base);
3022
+ }
3023
+ for (const delta of deltas) {
3024
+ Y.applyUpdate(mergedDoc, delta);
3025
+ }
3026
+ return Y.encodeStateAsUpdate(mergedDoc);
3027
+ }
3028
+ function updatesEqual(left, right) {
3029
+ if (left.length !== right.length) {
3030
+ return false;
3031
+ }
3032
+ for (let index = 0; index < left.length; index++) {
3033
+ if (left[index] !== right[index]) {
3034
+ return false;
3035
+ }
3036
+ }
3037
+ return true;
3038
+ }
3039
+
3040
+ // src/richtext/as-rich-text-sync-engine.ts
3041
+ function asRichTextSyncEngine(engine) {
3042
+ if (engine == null) {
3043
+ return null;
3044
+ }
3045
+ return engine;
3046
+ }
2571
3047
  export {
2572
3048
  AdapterError,
2573
3049
  COMPACTION_BASELINE_META_KEY,
@@ -2577,6 +3053,8 @@ export {
2577
3053
  PersistenceError,
2578
3054
  QueryBuilder,
2579
3055
  QueryError,
3056
+ QueryStore,
3057
+ QueryStoreCache,
2580
3058
  RecordNotFoundError,
2581
3059
  SequenceManager,
2582
3060
  Store,
@@ -2586,14 +3064,18 @@ export {
2586
3064
  TransactionContext,
2587
3065
  WorkerInitError,
2588
3066
  WorkerTimeoutError,
3067
+ asRichTextSyncEngine,
3068
+ assertQueryReady,
2589
3069
  collectOperationsAheadOfServer,
2590
3070
  compactOperationLog,
2591
3071
  computeAckCompactionWatermark,
3072
+ createRichTextController,
2592
3073
  decodeAuditExport,
2593
3074
  decodeRichtext,
2594
3075
  deserializeVersionVectorFromMeta,
2595
3076
  encodeRichtext,
2596
3077
  exportBackup,
3078
+ getSharedQueryStoreCache,
2597
3079
  mergeVersionVectors,
2598
3080
  persistedAuditTraceFromEvent,
2599
3081
  pluralize,