@koolbase/core 12.11.1 → 12.12.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.
@@ -3,4 +3,6 @@ export interface UseRecordResult extends RecordState {
3
3
  /** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
4
4
  refresh: () => Promise<void>;
5
5
  }
6
- export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'>): (collection: string, id: string | null | undefined) => UseRecordResult;
6
+ export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'>): (collection: string, id: string | null | undefined, options?: {
7
+ live?: boolean;
8
+ }) => UseRecordResult;
@@ -11,13 +11,15 @@ const index_js_1 = require("../index.js");
11
11
  const noop = () => { };
12
12
  const done = () => Promise.resolve();
13
13
  function createUseRecord(getDb) {
14
- return function useRecord(collection, id) {
15
- const key = JSON.stringify([collection, id ?? '']);
14
+ return function useRecord(collection, id, options = {}) {
15
+ // live is part of the key: switching it starts a fresh controller.
16
+ const live = options.live === true;
17
+ const key = JSON.stringify([collection, id ?? '', ...(live ? ['live'] : [])]);
16
18
  const [current, setCurrent] = (0, react_1.useState)(null);
17
19
  (0, react_1.useEffect)(() => {
18
20
  // Created here, not during render: under React strict mode an effect
19
21
  // runs, cleans up and runs again, and each run gets its own controller.
20
- const controller = new index_js_1.KoolbaseRecordController(getDb(), collection, id ?? '');
22
+ const controller = new index_js_1.KoolbaseRecordController(getDb(), collection, id ?? '', { live });
21
23
  setCurrent({ key, controller });
22
24
  void controller.load();
23
25
  return () => controller.dispose();
@@ -19,7 +19,13 @@ export declare class KoolbaseRecordController {
19
19
  private readonly listeners;
20
20
  private generation;
21
21
  private disposed;
22
- constructor(db: RecordReader, collection: string, id: string);
22
+ /** Follow this record through Koolbase realtime (see onLive). */
23
+ private readonly live;
24
+ private unlive;
25
+ private liveTimer;
26
+ constructor(db: RecordReader, collection: string, id: string, options?: {
27
+ live?: boolean;
28
+ });
23
29
  getState(): RecordState;
24
30
  /** Called on every change. Returns the unsubscribe. */
25
31
  subscribe(listener: () => void): () => void;
@@ -29,6 +35,13 @@ export declare class KoolbaseRecordController {
29
35
  refresh(): Promise<void>;
30
36
  /** Stops all updates. A result still in flight is dropped when it lands. */
31
37
  dispose(): void;
38
+ /**
39
+ * A realtime event in this record's collection. Only one naming THIS record
40
+ * counts: an update (or a create, for an id that reappears) re-reads it,
41
+ * silently -- no `refreshing` -- and once per burst (250 ms); a delete is
42
+ * notFound at once, and a read still in flight is dropped.
43
+ */
44
+ private onLive;
32
45
  private fetch;
33
46
  private isStale;
34
47
  private set;
@@ -13,11 +13,14 @@
13
13
  // - refresh() keeps the record shown while it runs, and if it fails.
14
14
  // - every fetch has a generation: a result superseded by a later refresh,
15
15
  // or landing after dispose(), is dropped.
16
+ // - live (the option): realtime events for THIS record only. An update
17
+ // re-reads it silently, once per burst; a delete is notFound at once.
16
18
  // Not yet: an offline fallback from the record cache, which keeps data and
17
19
  // revision but not createdAt or createdBy.
18
20
  Object.defineProperty(exports, "__esModule", { value: true });
19
21
  exports.KoolbaseRecordController = exports.initialRecordState = void 0;
20
22
  const database_errors_js_1 = require("./database-errors.js");
23
+ const live_source_js_1 = require("./live-source.js");
21
24
  exports.initialRecordState = Object.freeze({
22
25
  status: 'loading',
23
26
  record: null,
@@ -25,14 +28,18 @@ exports.initialRecordState = Object.freeze({
25
28
  refreshing: false,
26
29
  });
27
30
  class KoolbaseRecordController {
28
- constructor(db, collection, id) {
31
+ constructor(db, collection, id, options = {}) {
29
32
  this.state = exports.initialRecordState;
30
33
  this.listeners = new Set();
31
34
  this.generation = 0;
32
35
  this.disposed = false;
36
+ // The realtime subscription, and its pending debounced re-read.
37
+ this.unlive = null;
38
+ this.liveTimer = null;
33
39
  this.db = db;
34
40
  this.collection = collection;
35
41
  this.id = id;
42
+ this.live = options.live === true;
36
43
  }
37
44
  getState() {
38
45
  return this.state;
@@ -50,6 +57,10 @@ class KoolbaseRecordController {
50
57
  async load() {
51
58
  if (this.disposed)
52
59
  return;
60
+ // Live needs an id: with none there is nothing to follow.
61
+ if (this.live && this.id) {
62
+ this.unlive ?? (this.unlive = (0, live_source_js_1.liveSource)()?.subscribe(this.collection, (event) => this.onLive(event)) ?? null);
63
+ }
53
64
  await this.fetch();
54
65
  }
55
66
  /** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
@@ -64,8 +75,45 @@ class KoolbaseRecordController {
64
75
  /** Stops all updates. A result still in flight is dropped when it lands. */
65
76
  dispose() {
66
77
  this.disposed = true;
78
+ this.unlive?.();
79
+ this.unlive = null;
80
+ if (this.liveTimer) {
81
+ clearTimeout(this.liveTimer);
82
+ this.liveTimer = null;
83
+ }
67
84
  this.listeners.clear();
68
85
  }
86
+ /**
87
+ * A realtime event in this record's collection. Only one naming THIS record
88
+ * counts: an update (or a create, for an id that reappears) re-reads it,
89
+ * silently -- no `refreshing` -- and once per burst (250 ms); a delete is
90
+ * notFound at once, and a read still in flight is dropped.
91
+ */
92
+ onLive(event) {
93
+ if (this.disposed)
94
+ return;
95
+ const e = (event ?? {});
96
+ const target = e.type === 'deleted' ? e.recordId : e.record?.id;
97
+ if (target !== this.id)
98
+ return;
99
+ if (e.type === 'deleted') {
100
+ if (this.liveTimer) {
101
+ clearTimeout(this.liveTimer);
102
+ this.liveTimer = null;
103
+ }
104
+ this.generation += 1;
105
+ this.set({ status: 'notFound', record: null, error: null, refreshing: false });
106
+ return;
107
+ }
108
+ if (this.liveTimer)
109
+ return;
110
+ this.liveTimer = setTimeout(() => {
111
+ this.liveTimer = null;
112
+ void this.fetch();
113
+ }, 250);
114
+ // A pending re-read must not be what keeps a Node process alive.
115
+ this.liveTimer.unref?.();
116
+ }
69
117
  async fetch() {
70
118
  const gen = ++this.generation;
71
119
  if (!this.id) {
@@ -3,4 +3,6 @@ export interface UseRecordResult extends RecordState {
3
3
  /** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
4
4
  refresh: () => Promise<void>;
5
5
  }
6
- export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'>): (collection: string, id: string | null | undefined) => UseRecordResult;
6
+ export declare function createUseRecord(getDb: () => Pick<KoolbaseDatabase, 'get'>): (collection: string, id: string | null | undefined, options?: {
7
+ live?: boolean;
8
+ }) => UseRecordResult;
@@ -8,13 +8,15 @@ import { KoolbaseRecordController, initialRecordState, } from '../index.js';
8
8
  const noop = () => { };
9
9
  const done = () => Promise.resolve();
10
10
  export function createUseRecord(getDb) {
11
- return function useRecord(collection, id) {
12
- const key = JSON.stringify([collection, id ?? '']);
11
+ return function useRecord(collection, id, options = {}) {
12
+ // live is part of the key: switching it starts a fresh controller.
13
+ const live = options.live === true;
14
+ const key = JSON.stringify([collection, id ?? '', ...(live ? ['live'] : [])]);
13
15
  const [current, setCurrent] = useState(null);
14
16
  useEffect(() => {
15
17
  // Created here, not during render: under React strict mode an effect
16
18
  // runs, cleans up and runs again, and each run gets its own controller.
17
- const controller = new KoolbaseRecordController(getDb(), collection, id ?? '');
19
+ const controller = new KoolbaseRecordController(getDb(), collection, id ?? '', { live });
18
20
  setCurrent({ key, controller });
19
21
  void controller.load();
20
22
  return () => controller.dispose();
@@ -19,7 +19,13 @@ export declare class KoolbaseRecordController {
19
19
  private readonly listeners;
20
20
  private generation;
21
21
  private disposed;
22
- constructor(db: RecordReader, collection: string, id: string);
22
+ /** Follow this record through Koolbase realtime (see onLive). */
23
+ private readonly live;
24
+ private unlive;
25
+ private liveTimer;
26
+ constructor(db: RecordReader, collection: string, id: string, options?: {
27
+ live?: boolean;
28
+ });
23
29
  getState(): RecordState;
24
30
  /** Called on every change. Returns the unsubscribe. */
25
31
  subscribe(listener: () => void): () => void;
@@ -29,6 +35,13 @@ export declare class KoolbaseRecordController {
29
35
  refresh(): Promise<void>;
30
36
  /** Stops all updates. A result still in flight is dropped when it lands. */
31
37
  dispose(): void;
38
+ /**
39
+ * A realtime event in this record's collection. Only one naming THIS record
40
+ * counts: an update (or a create, for an id that reappears) re-reads it,
41
+ * silently -- no `refreshing` -- and once per burst (250 ms); a delete is
42
+ * notFound at once, and a read still in flight is dropped.
43
+ */
44
+ private onLive;
32
45
  private fetch;
33
46
  private isStale;
34
47
  private set;
@@ -12,9 +12,12 @@
12
12
  // - refresh() keeps the record shown while it runs, and if it fails.
13
13
  // - every fetch has a generation: a result superseded by a later refresh,
14
14
  // or landing after dispose(), is dropped.
15
+ // - live (the option): realtime events for THIS record only. An update
16
+ // re-reads it silently, once per burst; a delete is notFound at once.
15
17
  // Not yet: an offline fallback from the record cache, which keeps data and
16
18
  // revision but not createdAt or createdBy.
17
19
  import { KoolbaseNotFoundError } from './database-errors.js';
20
+ import { liveSource } from './live-source.js';
18
21
  export const initialRecordState = Object.freeze({
19
22
  status: 'loading',
20
23
  record: null,
@@ -22,14 +25,18 @@ export const initialRecordState = Object.freeze({
22
25
  refreshing: false,
23
26
  });
24
27
  export class KoolbaseRecordController {
25
- constructor(db, collection, id) {
28
+ constructor(db, collection, id, options = {}) {
26
29
  this.state = initialRecordState;
27
30
  this.listeners = new Set();
28
31
  this.generation = 0;
29
32
  this.disposed = false;
33
+ // The realtime subscription, and its pending debounced re-read.
34
+ this.unlive = null;
35
+ this.liveTimer = null;
30
36
  this.db = db;
31
37
  this.collection = collection;
32
38
  this.id = id;
39
+ this.live = options.live === true;
33
40
  }
34
41
  getState() {
35
42
  return this.state;
@@ -47,6 +54,10 @@ export class KoolbaseRecordController {
47
54
  async load() {
48
55
  if (this.disposed)
49
56
  return;
57
+ // Live needs an id: with none there is nothing to follow.
58
+ if (this.live && this.id) {
59
+ this.unlive ?? (this.unlive = liveSource()?.subscribe(this.collection, (event) => this.onLive(event)) ?? null);
60
+ }
50
61
  await this.fetch();
51
62
  }
52
63
  /** The record again, from the server. What is shown stays while it runs, and stays if it fails. */
@@ -61,8 +72,45 @@ export class KoolbaseRecordController {
61
72
  /** Stops all updates. A result still in flight is dropped when it lands. */
62
73
  dispose() {
63
74
  this.disposed = true;
75
+ this.unlive?.();
76
+ this.unlive = null;
77
+ if (this.liveTimer) {
78
+ clearTimeout(this.liveTimer);
79
+ this.liveTimer = null;
80
+ }
64
81
  this.listeners.clear();
65
82
  }
83
+ /**
84
+ * A realtime event in this record's collection. Only one naming THIS record
85
+ * counts: an update (or a create, for an id that reappears) re-reads it,
86
+ * silently -- no `refreshing` -- and once per burst (250 ms); a delete is
87
+ * notFound at once, and a read still in flight is dropped.
88
+ */
89
+ onLive(event) {
90
+ if (this.disposed)
91
+ return;
92
+ const e = (event ?? {});
93
+ const target = e.type === 'deleted' ? e.recordId : e.record?.id;
94
+ if (target !== this.id)
95
+ return;
96
+ if (e.type === 'deleted') {
97
+ if (this.liveTimer) {
98
+ clearTimeout(this.liveTimer);
99
+ this.liveTimer = null;
100
+ }
101
+ this.generation += 1;
102
+ this.set({ status: 'notFound', record: null, error: null, refreshing: false });
103
+ return;
104
+ }
105
+ if (this.liveTimer)
106
+ return;
107
+ this.liveTimer = setTimeout(() => {
108
+ this.liveTimer = null;
109
+ void this.fetch();
110
+ }, 250);
111
+ // A pending re-read must not be what keeps a Node process alive.
112
+ this.liveTimer.unref?.();
113
+ }
66
114
  async fetch() {
67
115
  const gen = ++this.generation;
68
116
  if (!this.id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koolbase/core",
3
- "version": "12.11.1",
3
+ "version": "12.12.0",
4
4
  "description": "Koolbase SDK core \u2014 shared behaviour behind @koolbase/react-native and @koolbase/js. Install one of those, not this.",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "types": "./dist/esm/index.d.ts",