@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.cjs CHANGED
@@ -943,6 +943,8 @@ __export(index_exports, {
943
943
  PersistenceError: () => PersistenceError,
944
944
  QueryBuilder: () => QueryBuilder,
945
945
  QueryError: () => QueryError,
946
+ QueryStore: () => QueryStore,
947
+ QueryStoreCache: () => QueryStoreCache,
946
948
  RecordNotFoundError: () => RecordNotFoundError,
947
949
  SequenceManager: () => SequenceManager,
948
950
  Store: () => Store,
@@ -952,14 +954,18 @@ __export(index_exports, {
952
954
  TransactionContext: () => TransactionContext,
953
955
  WorkerInitError: () => WorkerInitError,
954
956
  WorkerTimeoutError: () => WorkerTimeoutError,
957
+ asRichTextSyncEngine: () => asRichTextSyncEngine,
958
+ assertQueryReady: () => assertQueryReady,
955
959
  collectOperationsAheadOfServer: () => collectOperationsAheadOfServer,
956
960
  compactOperationLog: () => compactOperationLog,
957
961
  computeAckCompactionWatermark: () => computeAckCompactionWatermark,
962
+ createRichTextController: () => createRichTextController,
958
963
  decodeAuditExport: () => decodeAuditExport,
959
964
  decodeRichtext: () => decodeRichtext,
960
965
  deserializeVersionVectorFromMeta: () => deserializeVersionVectorFromMeta,
961
966
  encodeRichtext: () => encodeRichtext,
962
967
  exportBackup: () => exportBackup,
968
+ getSharedQueryStoreCache: () => getSharedQueryStoreCache,
963
969
  mergeVersionVectors: () => mergeVersionVectors,
964
970
  persistedAuditTraceFromEvent: () => persistedAuditTraceFromEvent,
965
971
  pluralize: () => pluralize,
@@ -4074,6 +4080,483 @@ var Store = class {
4074
4080
  }
4075
4081
  };
4076
4082
 
4083
+ // src/reactivity/query-store.ts
4084
+ var EMPTY_ARRAY = Object.freeze([]);
4085
+ var QueryStore = class {
4086
+ snapshot = EMPTY_ARRAY;
4087
+ listeners = /* @__PURE__ */ new Set();
4088
+ unsubscribeQuery = null;
4089
+ active = false;
4090
+ queryBuilder;
4091
+ constructor(queryBuilder) {
4092
+ this.queryBuilder = queryBuilder;
4093
+ }
4094
+ /**
4095
+ * Subscribe to snapshot changes.
4096
+ *
4097
+ * @returns Unsubscribe function
4098
+ */
4099
+ subscribe = (onStoreChange) => {
4100
+ this.listeners.add(onStoreChange);
4101
+ if (!this.active) {
4102
+ this.startSubscription();
4103
+ }
4104
+ return () => {
4105
+ this.listeners.delete(onStoreChange);
4106
+ if (this.listeners.size === 0) {
4107
+ this.stopSubscription();
4108
+ }
4109
+ };
4110
+ };
4111
+ /** Synchronous read of the latest query results. */
4112
+ getSnapshot = () => {
4113
+ return this.snapshot;
4114
+ };
4115
+ /** Tear down listeners and the underlying query subscription. */
4116
+ destroy() {
4117
+ this.stopSubscription();
4118
+ this.listeners.clear();
4119
+ this.snapshot = EMPTY_ARRAY;
4120
+ }
4121
+ startSubscription() {
4122
+ this.active = true;
4123
+ this.unsubscribeQuery = this.queryBuilder.subscribe((results) => {
4124
+ if (!this.active) return;
4125
+ this.snapshot = Object.freeze([...results]);
4126
+ this.notifyListeners();
4127
+ });
4128
+ }
4129
+ stopSubscription() {
4130
+ this.active = false;
4131
+ if (this.unsubscribeQuery) {
4132
+ this.unsubscribeQuery();
4133
+ this.unsubscribeQuery = null;
4134
+ }
4135
+ }
4136
+ notifyListeners() {
4137
+ for (const listener of this.listeners) {
4138
+ listener();
4139
+ }
4140
+ }
4141
+ };
4142
+
4143
+ // src/reactivity/assert-query-ready.ts
4144
+ var import_core18 = require("@korajs/core");
4145
+ var LEGACY_PENDING_COLLECTION = "__pending__";
4146
+ function assertQueryReady(query) {
4147
+ const descriptor = query.getDescriptor();
4148
+ if (descriptor.collection === LEGACY_PENDING_COLLECTION) {
4149
+ throw new import_core18.AppNotReadyError(
4150
+ "Cannot use useQuery() before app.ready. Await app.ready or wrap your UI in <KoraProvider app={app}>."
4151
+ );
4152
+ }
4153
+ }
4154
+
4155
+ // src/reactivity/query-store-cache.ts
4156
+ var QueryStoreCache = class {
4157
+ constructor(scopeKey = "default") {
4158
+ this.scopeKey = scopeKey;
4159
+ }
4160
+ scopeKey;
4161
+ entries = /* @__PURE__ */ new Map();
4162
+ getOrCreate(queryBuilder) {
4163
+ const key = this.getKey(queryBuilder);
4164
+ const existing = this.entries.get(key);
4165
+ if (existing) {
4166
+ existing.refCount++;
4167
+ return existing.queryStore;
4168
+ }
4169
+ const queryStore = new QueryStore(queryBuilder);
4170
+ this.entries.set(key, {
4171
+ queryStore,
4172
+ refCount: 1
4173
+ });
4174
+ return queryStore;
4175
+ }
4176
+ release(queryBuilder) {
4177
+ const key = this.getKey(queryBuilder);
4178
+ const entry = this.entries.get(key);
4179
+ if (!entry) {
4180
+ return;
4181
+ }
4182
+ entry.refCount--;
4183
+ if (entry.refCount <= 0) {
4184
+ entry.queryStore.destroy();
4185
+ this.entries.delete(key);
4186
+ }
4187
+ }
4188
+ clear() {
4189
+ for (const entry of this.entries.values()) {
4190
+ entry.queryStore.destroy();
4191
+ }
4192
+ this.entries.clear();
4193
+ }
4194
+ get size() {
4195
+ return this.entries.size;
4196
+ }
4197
+ getKey(queryBuilder) {
4198
+ return `${this.scopeKey}:${JSON.stringify(queryBuilder.getDescriptor())}`;
4199
+ }
4200
+ };
4201
+ var sharedCache = null;
4202
+ function getSharedQueryStoreCache() {
4203
+ if (!sharedCache) {
4204
+ sharedCache = new QueryStoreCache();
4205
+ }
4206
+ return sharedCache;
4207
+ }
4208
+
4209
+ // src/richtext/create-richtext-controller.ts
4210
+ var Y2 = __toESM(require("yjs"), 1);
4211
+ init_richtext_serializer();
4212
+ var LOAD_ORIGIN = "kora-load";
4213
+ var REMOTE_ORIGIN = "kora-remote";
4214
+ var DOC_CHANNEL_ORIGIN = "kora-doc-channel";
4215
+ var TEXT_KEY2 = "content";
4216
+ var COMPACT_AFTER_DELTAS = 20;
4217
+ var PERSIST_DEBOUNCE_MS = 400;
4218
+ function createRichTextController(options) {
4219
+ const {
4220
+ collection,
4221
+ collectionName,
4222
+ recordId,
4223
+ fieldName,
4224
+ store,
4225
+ syncEngine = null,
4226
+ useDocChannel
4227
+ } = options;
4228
+ const doc = new Y2.Doc();
4229
+ const text = doc.getText(TEXT_KEY2);
4230
+ const undoManager = new Y2.UndoManager(text);
4231
+ let user = options.user;
4232
+ let disposed = false;
4233
+ let ready = false;
4234
+ let error = null;
4235
+ let canUndo = false;
4236
+ let canRedo = false;
4237
+ let cursors = [];
4238
+ const listeners = /* @__PURE__ */ new Set();
4239
+ let snapshot = {
4240
+ ready: false,
4241
+ error: null,
4242
+ canUndo: false,
4243
+ canRedo: false,
4244
+ cursors: []
4245
+ };
4246
+ const baseUpdateRef = { current: null };
4247
+ const pendingDeltasRef = { current: [] };
4248
+ const docChannelActiveRef = { current: false };
4249
+ let persistTimer = null;
4250
+ let recordUnsubscribe = null;
4251
+ let docChannelUnsubscribe = null;
4252
+ let awarenessUnsubscribe = null;
4253
+ const refreshSnapshot = () => {
4254
+ snapshot = {
4255
+ ready,
4256
+ error,
4257
+ canUndo,
4258
+ canRedo,
4259
+ cursors: [...cursors]
4260
+ };
4261
+ };
4262
+ const notify = () => {
4263
+ refreshSnapshot();
4264
+ for (const listener of listeners) {
4265
+ listener();
4266
+ }
4267
+ };
4268
+ const syncHistoryState = () => {
4269
+ canUndo = undoManager.undoStack.length > 0;
4270
+ canRedo = undoManager.redoStack.length > 0;
4271
+ notify();
4272
+ };
4273
+ const getSnapshot = () => snapshot;
4274
+ const resolveAwarenessUser = () => {
4275
+ if (user) {
4276
+ return user;
4277
+ }
4278
+ if (syncEngine) {
4279
+ const existing = syncEngine.getAwarenessManager().getLocalState()?.user;
4280
+ if (existing) {
4281
+ return existing;
4282
+ }
4283
+ }
4284
+ return {
4285
+ name: "Anonymous",
4286
+ color: "#6366f1"
4287
+ };
4288
+ };
4289
+ const setCursor = (anchor, head) => {
4290
+ if (!syncEngine) {
4291
+ return;
4292
+ }
4293
+ const awareness = syncEngine.getAwarenessManager();
4294
+ const state = {
4295
+ user: resolveAwarenessUser(),
4296
+ cursor: {
4297
+ collection: collectionName,
4298
+ recordId,
4299
+ field: fieldName,
4300
+ anchor,
4301
+ head
4302
+ }
4303
+ };
4304
+ awareness.setLocalState(state);
4305
+ };
4306
+ const clearCursor = () => {
4307
+ if (!syncEngine) {
4308
+ return;
4309
+ }
4310
+ const awareness = syncEngine.getAwarenessManager();
4311
+ const current = awareness.getLocalState();
4312
+ if (!current?.cursor) {
4313
+ return;
4314
+ }
4315
+ if (current.cursor.collection !== collectionName || current.cursor.recordId !== recordId || current.cursor.field !== fieldName) {
4316
+ return;
4317
+ }
4318
+ awareness.setLocalState({ user: current.user });
4319
+ };
4320
+ const applyRemoteSnapshot = (value) => {
4321
+ const encoded = encodeRichtextInput(value);
4322
+ if (!encoded) {
4323
+ return;
4324
+ }
4325
+ const currentSnapshot = Y2.encodeStateAsUpdate(doc);
4326
+ if (updatesEqual(currentSnapshot, encoded)) {
4327
+ return;
4328
+ }
4329
+ Y2.applyUpdate(doc, encoded, REMOTE_ORIGIN);
4330
+ baseUpdateRef.current = encoded;
4331
+ pendingDeltasRef.current = [];
4332
+ syncHistoryState();
4333
+ };
4334
+ const flushPersist = async () => {
4335
+ const snapshot2 = composeRichtextSnapshot(baseUpdateRef.current, pendingDeltasRef.current);
4336
+ if (pendingDeltasRef.current.length >= COMPACT_AFTER_DELTAS) {
4337
+ baseUpdateRef.current = snapshot2;
4338
+ pendingDeltasRef.current = [];
4339
+ }
4340
+ try {
4341
+ await collection.update(recordId, {
4342
+ [fieldName]: snapshot2
4343
+ });
4344
+ } catch (cause) {
4345
+ if (!disposed) {
4346
+ error = cause instanceof Error ? cause : new Error(String(cause));
4347
+ notify();
4348
+ }
4349
+ }
4350
+ };
4351
+ const schedulePersist = () => {
4352
+ if (persistTimer) {
4353
+ clearTimeout(persistTimer);
4354
+ }
4355
+ persistTimer = setTimeout(() => {
4356
+ persistTimer = null;
4357
+ void flushPersist();
4358
+ }, PERSIST_DEBOUNCE_MS);
4359
+ };
4360
+ const onDocUpdate = (update, origin) => {
4361
+ syncHistoryState();
4362
+ if (origin === LOAD_ORIGIN || origin === REMOTE_ORIGIN || origin === DOC_CHANNEL_ORIGIN) {
4363
+ return;
4364
+ }
4365
+ pendingDeltasRef.current.push(update);
4366
+ if (docChannelActiveRef.current && syncEngine) {
4367
+ syncEngine.getRichtextDocChannel?.().send(collectionName, recordId, fieldName, update);
4368
+ schedulePersist();
4369
+ return;
4370
+ }
4371
+ void flushPersist();
4372
+ };
4373
+ const subscribeRecordChanges = () => {
4374
+ recordUnsubscribe?.();
4375
+ recordUnsubscribe = store.collection(collectionName).where({ id: recordId }).subscribe((results) => {
4376
+ const record = results[0];
4377
+ if (!record) {
4378
+ return;
4379
+ }
4380
+ applyRemoteSnapshot(record[fieldName]);
4381
+ });
4382
+ };
4383
+ const subscribeDocChannel = () => {
4384
+ docChannelUnsubscribe?.();
4385
+ if (!ready || !syncEngine || !docChannelActiveRef.current) {
4386
+ return;
4387
+ }
4388
+ const channel = syncEngine.getRichtextDocChannel?.();
4389
+ if (!channel) {
4390
+ return;
4391
+ }
4392
+ docChannelUnsubscribe = channel.subscribe(collectionName, recordId, fieldName, (update) => {
4393
+ Y2.applyUpdate(doc, update, DOC_CHANNEL_ORIGIN);
4394
+ syncHistoryState();
4395
+ });
4396
+ };
4397
+ const updateCursors = () => {
4398
+ if (!syncEngine) {
4399
+ cursors = [];
4400
+ notify();
4401
+ return;
4402
+ }
4403
+ const awareness = syncEngine.getAwarenessManager();
4404
+ const localClientId = awareness.clientId;
4405
+ const states = awareness.getStates();
4406
+ const fieldCursors = [];
4407
+ for (const [clientId, state] of states) {
4408
+ if (clientId === localClientId) continue;
4409
+ if (!state.cursor) continue;
4410
+ if (state.cursor.collection !== collectionName || state.cursor.recordId !== recordId || state.cursor.field !== fieldName) {
4411
+ continue;
4412
+ }
4413
+ fieldCursors.push({
4414
+ clientId,
4415
+ userName: state.user.name,
4416
+ color: state.user.color,
4417
+ anchor: state.cursor.anchor,
4418
+ head: state.cursor.head
4419
+ });
4420
+ }
4421
+ cursors = fieldCursors;
4422
+ notify();
4423
+ };
4424
+ const subscribeAwareness = () => {
4425
+ awarenessUnsubscribe?.();
4426
+ if (!syncEngine) {
4427
+ cursors = [];
4428
+ notify();
4429
+ return;
4430
+ }
4431
+ const awareness = syncEngine.getAwarenessManager();
4432
+ awarenessUnsubscribe = awareness.on("change", () => {
4433
+ updateCursors();
4434
+ });
4435
+ updateCursors();
4436
+ };
4437
+ const initialize = async () => {
4438
+ ready = false;
4439
+ error = null;
4440
+ notify();
4441
+ try {
4442
+ const record = await collection.findById(recordId);
4443
+ if (disposed) return;
4444
+ doc.transact(() => {
4445
+ const target = doc.getText(TEXT_KEY2);
4446
+ target.delete(0, target.length);
4447
+ }, LOAD_ORIGIN);
4448
+ const encoded = encodeRichtextInput(record?.[fieldName]);
4449
+ baseUpdateRef.current = encoded;
4450
+ pendingDeltasRef.current = [];
4451
+ if (encoded) {
4452
+ Y2.applyUpdate(doc, encoded, LOAD_ORIGIN);
4453
+ }
4454
+ const channel = syncEngine?.getRichtextDocChannel?.();
4455
+ docChannelActiveRef.current = channel?.shouldUseChannel(encoded?.length ?? 0, useDocChannel) ?? false;
4456
+ ready = true;
4457
+ syncHistoryState();
4458
+ subscribeRecordChanges();
4459
+ subscribeDocChannel();
4460
+ subscribeAwareness();
4461
+ notify();
4462
+ } catch (cause) {
4463
+ if (disposed) return;
4464
+ error = cause instanceof Error ? cause : new Error(String(cause));
4465
+ notify();
4466
+ }
4467
+ };
4468
+ doc.on("update", onDocUpdate);
4469
+ void initialize();
4470
+ return {
4471
+ doc,
4472
+ text,
4473
+ getSnapshot,
4474
+ subscribe(listener) {
4475
+ listeners.add(listener);
4476
+ return () => {
4477
+ listeners.delete(listener);
4478
+ };
4479
+ },
4480
+ undo() {
4481
+ undoManager.undo();
4482
+ syncHistoryState();
4483
+ },
4484
+ redo() {
4485
+ undoManager.redo();
4486
+ syncHistoryState();
4487
+ },
4488
+ setCursor,
4489
+ clearCursor,
4490
+ setUser(nextUser) {
4491
+ user = nextUser;
4492
+ },
4493
+ destroy() {
4494
+ if (disposed) {
4495
+ return;
4496
+ }
4497
+ disposed = true;
4498
+ if (persistTimer) {
4499
+ clearTimeout(persistTimer);
4500
+ persistTimer = null;
4501
+ }
4502
+ doc.off("update", onDocUpdate);
4503
+ recordUnsubscribe?.();
4504
+ docChannelUnsubscribe?.();
4505
+ awarenessUnsubscribe?.();
4506
+ clearCursor();
4507
+ undoManager.destroy();
4508
+ baseUpdateRef.current = null;
4509
+ pendingDeltasRef.current = [];
4510
+ docChannelActiveRef.current = false;
4511
+ listeners.clear();
4512
+ }
4513
+ };
4514
+ }
4515
+ function encodeRichtextInput(value) {
4516
+ if (value === null || value === void 0) {
4517
+ return null;
4518
+ }
4519
+ try {
4520
+ return encodeRichtext(value);
4521
+ } catch {
4522
+ if (typeof value === "string") {
4523
+ const fallbackDoc = new Y2.Doc();
4524
+ fallbackDoc.getText(TEXT_KEY2).insert(0, value);
4525
+ return Y2.encodeStateAsUpdate(fallbackDoc);
4526
+ }
4527
+ throw new Error("Richtext record value must be a string, Uint8Array, ArrayBuffer, or null.");
4528
+ }
4529
+ }
4530
+ function composeRichtextSnapshot(base, deltas) {
4531
+ const mergedDoc = new Y2.Doc();
4532
+ if (base) {
4533
+ Y2.applyUpdate(mergedDoc, base);
4534
+ }
4535
+ for (const delta of deltas) {
4536
+ Y2.applyUpdate(mergedDoc, delta);
4537
+ }
4538
+ return Y2.encodeStateAsUpdate(mergedDoc);
4539
+ }
4540
+ function updatesEqual(left, right) {
4541
+ if (left.length !== right.length) {
4542
+ return false;
4543
+ }
4544
+ for (let index = 0; index < left.length; index++) {
4545
+ if (left[index] !== right[index]) {
4546
+ return false;
4547
+ }
4548
+ }
4549
+ return true;
4550
+ }
4551
+
4552
+ // src/richtext/as-rich-text-sync-engine.ts
4553
+ function asRichTextSyncEngine(engine) {
4554
+ if (engine == null) {
4555
+ return null;
4556
+ }
4557
+ return engine;
4558
+ }
4559
+
4077
4560
  // src/index.ts
4078
4561
  init_richtext_serializer();
4079
4562
 
@@ -4094,6 +4577,8 @@ init_export_audit();
4094
4577
  PersistenceError,
4095
4578
  QueryBuilder,
4096
4579
  QueryError,
4580
+ QueryStore,
4581
+ QueryStoreCache,
4097
4582
  RecordNotFoundError,
4098
4583
  SequenceManager,
4099
4584
  Store,
@@ -4103,14 +4588,18 @@ init_export_audit();
4103
4588
  TransactionContext,
4104
4589
  WorkerInitError,
4105
4590
  WorkerTimeoutError,
4591
+ asRichTextSyncEngine,
4592
+ assertQueryReady,
4106
4593
  collectOperationsAheadOfServer,
4107
4594
  compactOperationLog,
4108
4595
  computeAckCompactionWatermark,
4596
+ createRichTextController,
4109
4597
  decodeAuditExport,
4110
4598
  decodeRichtext,
4111
4599
  deserializeVersionVectorFromMeta,
4112
4600
  encodeRichtext,
4113
4601
  exportBackup,
4602
+ getSharedQueryStoreCache,
4114
4603
  mergeVersionVectors,
4115
4604
  persistedAuditTraceFromEvent,
4116
4605
  pluralize,