@evolu/common 6.0.1-preview.27 → 6.0.1-preview.28

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.
@@ -42,7 +42,7 @@ export interface EvoluConfig extends Partial<DbConfig> {
42
42
  */
43
43
  readonly reloadUrl?: string;
44
44
  }
45
- export interface Evolu<S extends EvoluSchema = EvoluSchema> {
45
+ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
46
46
  /**
47
47
  * Subscribe to {@link EvoluError} changes.
48
48
  *
@@ -399,15 +399,10 @@ export type EvoluDeps = ConsoleDep & CreateDbWorkerDep & Partial<FlushSyncDep> &
399
399
  *
400
400
  * ### Instance Caching
401
401
  *
402
- * Evolu caches instances by {@link EvoluConfig} name to enable hot reloading and
403
- * multitenancy. Multiple calls to `createEvolu` with the same name return the
404
- * same instance, preserving database connections and state across module
405
- * reloads during development. This ensures a seamless developer experience
406
- * where edits don't interrupt ongoing sync or lose in-memory state.
407
- *
408
- * For testing, either dispose of instances after each test (TODO: implement
409
- * dispose method) or use unique instance names to ensure proper isolation
410
- * between test cases.
402
+ * `createEvolu` caches instances using {@link Multiton} by {@link EvoluConfig}
403
+ * name to enable hot reloading and prevent database corruption from multiple
404
+ * connections. For testing, use unique instance names to ensure proper
405
+ * isolation.
411
406
  */
412
407
  export declare const createEvolu: (deps: EvoluDeps) => <S extends EvoluSchema>(schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>, config?: EvoluConfig) => Evolu<S>;
413
408
  export declare const createQuery: <R extends Row>(queryCallback: Parameters<CreateQuery<EvoluSchema>>[0], options?: Parameters<CreateQuery<EvoluSchema>>[1]) => Query<R>;
@@ -1 +1 @@
1
- {"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Evolu.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Evolu.d.ts","sourceRoot":"","sources":["../../../src/Evolu/Evolu.ts"],"names":[],"mappings":"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"}
@@ -4,6 +4,7 @@ import { assert, assertNonEmptyArray } from "../Assert.js";
4
4
  import { createCallbackRegistry } from "../CallbackRegistry.js";
5
5
  import { eqArrayNumber } from "../Eq.js";
6
6
  import { exhaustiveCheck } from "../Function.js";
7
+ import { createMultiton } from "../Multiton.js";
7
8
  import { err, ok } from "../Result.js";
8
9
  import { isSqlMutation } from "../Sqlite.js";
9
10
  import { createStore } from "../Store.js";
@@ -14,7 +15,11 @@ import { createSubscribedQueries, emptyRows, serializeQuery, } from "./Query.js"
14
15
  import { evoluSchemaToDbSchema, insertable, kysely, updateable, upsertable, } from "./Schema.js";
15
16
  import { DbChange } from "./Storage.js";
16
17
  import { initialSyncState } from "./Sync.js";
17
- const evoluInstances = new Map();
18
+ const evoluInstances = createMultiton();
19
+ /**
20
+ * Unique identifier for the current browser tab or app instance, lazily
21
+ * initialized on first use to distinguish between multiple tabs.
22
+ */
18
23
  let tabId = null;
19
24
  /**
20
25
  * Creates an {@link Evolu} instance for a platform configured with the specified
@@ -51,29 +56,15 @@ let tabId = null;
51
56
  *
52
57
  * ### Instance Caching
53
58
  *
54
- * Evolu caches instances by {@link EvoluConfig} name to enable hot reloading and
55
- * multitenancy. Multiple calls to `createEvolu` with the same name return the
56
- * same instance, preserving database connections and state across module
57
- * reloads during development. This ensures a seamless developer experience
58
- * where edits don't interrupt ongoing sync or lose in-memory state.
59
- *
60
- * For testing, either dispose of instances after each test (TODO: implement
61
- * dispose method) or use unique instance names to ensure proper isolation
62
- * between test cases.
59
+ * `createEvolu` caches instances using {@link Multiton} by {@link EvoluConfig}
60
+ * name to enable hot reloading and prevent database corruption from multiple
61
+ * connections. For testing, use unique instance names to ensure proper
62
+ * isolation.
63
63
  */
64
- export const createEvolu = (deps) => (schema, config) => {
65
- const name = config?.name ?? defaultDbConfig.name;
66
- let evolu = evoluInstances.get(name);
67
- if (evolu == null) {
68
- evolu = createEvoluInstance(deps)(schema, config);
69
- evoluInstances.set(name, evolu);
70
- }
71
- else {
72
- // Hot reloading. Note that indexes are intentionally omitted.
73
- evolu.ensureSchema(schema);
74
- }
75
- return evolu;
76
- };
64
+ export const createEvolu = (deps) => (schema, config) => evoluInstances.ensure(config?.name ?? defaultDbConfig.name, () => createEvoluInstance(deps)(schema, config), (evolu) => {
65
+ // Hot reloading. Note that indexes are intentionally omitted.
66
+ evolu.ensureSchema(schema);
67
+ });
77
68
  const createEvoluInstance = (deps) => (schema, config) => {
78
69
  deps.console.enabled = config?.enableLogging ?? false;
79
70
  const { indexes, reloadUrl = "/", ...partialDbConfig } = config ?? {};
@@ -376,6 +367,10 @@ const createEvoluInstance = (deps) => (schema, config) => {
376
367
  };
377
368
  return unuse;
378
369
  },
370
+ /** Disposal is not implemented yet. */
371
+ [Symbol.dispose]: () => {
372
+ throw new Error("Evolu instance disposal is not yet implemented");
373
+ },
379
374
  };
380
375
  return evolu;
381
376
  };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Manages multiple named instances using the Multiton pattern.
3
+ *
4
+ * Unlike Singleton (one instance globally), Multiton maintains one instance per
5
+ * unique key.
6
+ *
7
+ * **Note:** Multiton is generally considered an anti-pattern because it
8
+ * introduces hidden global state and makes testing harder. Use it only when
9
+ * there's a compelling reason, such as:
10
+ *
11
+ * - Supporting hot reloading while preserving state across module reloads
12
+ * - Enforcing physical constraints (e.g., preventing multiple SQLite connections
13
+ * to the same database, which causes corruption)
14
+ * - Managing resources where instance identity is intrinsic to correctness
15
+ *
16
+ * For most cases, prefer explicit dependency injection and instance management.
17
+ *
18
+ * Compatibility and future work:
19
+ *
20
+ * - We will adopt the ECMAScript `DisposableStack` for structured cleanup and
21
+ * robust error handling as runtimes converge (Node.js ≥ 24, Safari stable).
22
+ * Safari Technology Preview already includes support, so broad availability
23
+ * is expected soon.
24
+ * - Until then, this module uses a simple Map-based approach and calls
25
+ * `instance[Symbol.dispose]()` directly during disposal.
26
+ * - We don't use a polyfill because we avoid global mutation, keep bundles lean,
27
+ * and prefer explicit feature detection.
28
+ * - MDN reference:
29
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack
30
+ */
31
+ export interface Multiton<K extends string, T extends Disposable> extends Disposable {
32
+ /**
33
+ * Ensures an instance exists for the given key, creating it if necessary. If
34
+ * the instance already exists, the optional `onCacheHit` callback is invoked
35
+ * to update the existing instance.
36
+ */
37
+ readonly ensure: (key: K, create: () => T, onCacheHit?: (instance: T) => void) => T;
38
+ /** Gets an instance by key, or returns `null` if it doesn't exist. */
39
+ readonly get: (key: K) => T | null;
40
+ /** Checks if an instance exists for the given key. */
41
+ readonly has: (key: K) => boolean;
42
+ /**
43
+ * Removes and disposes an instance by key. Returns `true` if the instance
44
+ * existed and was disposed, `false` otherwise.
45
+ */
46
+ readonly disposeInstance: (key: K) => boolean;
47
+ }
48
+ /** Creates a {@link Multiton} instance manager. */
49
+ export declare const createMultiton: <K extends string, T extends Disposable>() => Multiton<K, T>;
50
+ //# sourceMappingURL=Multiton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Multiton.d.ts","sourceRoot":"","sources":["../../src/Multiton.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,QAAQ,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,UAAU,CAC9D,SAAQ,UAAU;IAClB;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,CACf,GAAG,EAAE,CAAC,EACN,MAAM,EAAE,MAAM,CAAC,EACf,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,KAC/B,CAAC,CAAC;IAEP,sEAAsE;IACtE,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAEnC,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;IAElC;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,CAAC;CAC/C;AAED,mDAAmD;AACnD,eAAO,MAAM,cAAc,GACzB,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS,UAAU,OACjB,QAAQ,CAAC,CAAC,EAAE,CAAC,CAqCjB,CAAC"}
@@ -0,0 +1,33 @@
1
+ /** Creates a {@link Multiton} instance manager. */
2
+ export const createMultiton = () => {
3
+ const instances = new Map();
4
+ return {
5
+ ensure: (key, create, onCacheHit) => {
6
+ let instance = instances.get(key);
7
+ if (instance == null) {
8
+ instance = create();
9
+ instances.set(key, instance);
10
+ }
11
+ else if (onCacheHit) {
12
+ onCacheHit(instance);
13
+ }
14
+ return instance;
15
+ },
16
+ get: (key) => instances.get(key) ?? null,
17
+ has: (key) => instances.has(key),
18
+ disposeInstance: (key) => {
19
+ const instance = instances.get(key);
20
+ if (instance) {
21
+ instance[Symbol.dispose]();
22
+ return instances.delete(key);
23
+ }
24
+ return false;
25
+ },
26
+ [Symbol.dispose]: () => {
27
+ for (const instance of instances.values()) {
28
+ instance[Symbol.dispose]();
29
+ }
30
+ instances.clear();
31
+ },
32
+ };
33
+ };
@@ -13,6 +13,7 @@ export * from "./Evolu/Public.js";
13
13
  export * from "./Function.js";
14
14
  export * from "./Identicon.js";
15
15
  export * from "./ManyToManyMap.js";
16
+ export * from "./Multiton.js";
16
17
  export * from "./Number.js";
17
18
  export * from "./Object.js";
18
19
  export * from "./Order.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,uBAAuB,CAAC;AACtC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
package/dist/src/index.js CHANGED
@@ -13,6 +13,7 @@ export * from "./Evolu/Public.js";
13
13
  export * from "./Function.js";
14
14
  export * from "./Identicon.js";
15
15
  export * from "./ManyToManyMap.js";
16
+ export * from "./Multiton.js";
16
17
  export * from "./Number.js";
17
18
  export * from "./Object.js";
18
19
  export * from "./Order.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "6.0.1-preview.27",
3
+ "version": "6.0.1-preview.28",
4
4
  "description": "TypeScript library and local-first framework",
5
5
  "keywords": [
6
6
  "evolu",
@@ -7,6 +7,7 @@ import { RandomBytesDep, SymmetricCryptoDecryptError } from "../Crypto.js";
7
7
  import { eqArrayNumber } from "../Eq.js";
8
8
  import { TransferableError } from "../Error.js";
9
9
  import { exhaustiveCheck } from "../Function.js";
10
+ import { createMultiton, Multiton } from "../Multiton.js";
10
11
  import { err, ok, Result } from "../Result.js";
11
12
  import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
12
13
  import { createStore, StoreSubscribe } from "../Store.js";
@@ -19,6 +20,7 @@ import {
19
20
  InferType,
20
21
  Mnemonic,
21
22
  ObjectType,
23
+ SimpleName,
22
24
  ValidMutationSize,
23
25
  ValidMutationSizeError,
24
26
  } from "../Type.js";
@@ -92,7 +94,7 @@ export interface EvoluConfig extends Partial<DbConfig> {
92
94
  readonly reloadUrl?: string;
93
95
  }
94
96
 
95
- export interface Evolu<S extends EvoluSchema = EvoluSchema> {
97
+ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
96
98
  /**
97
99
  * Subscribe to {@link EvoluError} changes.
98
100
  *
@@ -457,8 +459,12 @@ export type EvoluDeps = ConsoleDep &
457
459
  ReloadAppDep &
458
460
  TimeDep;
459
461
 
460
- const evoluInstances = new Map<string, InternalEvoluInstance>();
462
+ const evoluInstances = createMultiton<SimpleName, InternalEvoluInstance>();
461
463
 
464
+ /**
465
+ * Unique identifier for the current browser tab or app instance, lazily
466
+ * initialized on first use to distinguish between multiple tabs.
467
+ */
462
468
  let tabId: Id | null = null;
463
469
 
464
470
  /**
@@ -496,35 +502,25 @@ let tabId: Id | null = null;
496
502
  *
497
503
  * ### Instance Caching
498
504
  *
499
- * Evolu caches instances by {@link EvoluConfig} name to enable hot reloading and
500
- * multitenancy. Multiple calls to `createEvolu` with the same name return the
501
- * same instance, preserving database connections and state across module
502
- * reloads during development. This ensures a seamless developer experience
503
- * where edits don't interrupt ongoing sync or lose in-memory state.
504
- *
505
- * For testing, either dispose of instances after each test (TODO: implement
506
- * dispose method) or use unique instance names to ensure proper isolation
507
- * between test cases.
505
+ * `createEvolu` caches instances using {@link Multiton} by {@link EvoluConfig}
506
+ * name to enable hot reloading and prevent database corruption from multiple
507
+ * connections. For testing, use unique instance names to ensure proper
508
+ * isolation.
508
509
  */
509
510
  export const createEvolu =
510
511
  (deps: EvoluDeps) =>
511
512
  <S extends EvoluSchema>(
512
513
  schema: ValidateSchema<S> extends never ? S : ValidateSchema<S>,
513
514
  config?: EvoluConfig,
514
- ): Evolu<S> => {
515
- const name = config?.name ?? defaultDbConfig.name;
516
- let evolu = evoluInstances.get(name);
517
-
518
- if (evolu == null) {
519
- evolu = createEvoluInstance(deps)(schema as EvoluSchema, config);
520
- evoluInstances.set(name, evolu);
521
- } else {
522
- // Hot reloading. Note that indexes are intentionally omitted.
523
- evolu.ensureSchema(schema as EvoluSchema);
524
- }
525
-
526
- return evolu as Evolu<S>;
527
- };
515
+ ): Evolu<S> =>
516
+ evoluInstances.ensure(
517
+ config?.name ?? defaultDbConfig.name,
518
+ () => createEvoluInstance(deps)(schema as EvoluSchema, config),
519
+ (evolu) => {
520
+ // Hot reloading. Note that indexes are intentionally omitted.
521
+ evolu.ensureSchema(schema as EvoluSchema);
522
+ },
523
+ ) as Evolu<S>;
528
524
 
529
525
  const createEvoluInstance =
530
526
  (deps: EvoluDeps) =>
@@ -943,6 +939,11 @@ const createEvoluInstance =
943
939
 
944
940
  return unuse;
945
941
  },
942
+
943
+ /** Disposal is not implemented yet. */
944
+ [Symbol.dispose]: () => {
945
+ throw new Error("Evolu instance disposal is not yet implemented");
946
+ },
946
947
  };
947
948
 
948
949
  return evolu;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Manages multiple named instances using the Multiton pattern.
3
+ *
4
+ * Unlike Singleton (one instance globally), Multiton maintains one instance per
5
+ * unique key.
6
+ *
7
+ * **Note:** Multiton is generally considered an anti-pattern because it
8
+ * introduces hidden global state and makes testing harder. Use it only when
9
+ * there's a compelling reason, such as:
10
+ *
11
+ * - Supporting hot reloading while preserving state across module reloads
12
+ * - Enforcing physical constraints (e.g., preventing multiple SQLite connections
13
+ * to the same database, which causes corruption)
14
+ * - Managing resources where instance identity is intrinsic to correctness
15
+ *
16
+ * For most cases, prefer explicit dependency injection and instance management.
17
+ *
18
+ * Compatibility and future work:
19
+ *
20
+ * - We will adopt the ECMAScript `DisposableStack` for structured cleanup and
21
+ * robust error handling as runtimes converge (Node.js ≥ 24, Safari stable).
22
+ * Safari Technology Preview already includes support, so broad availability
23
+ * is expected soon.
24
+ * - Until then, this module uses a simple Map-based approach and calls
25
+ * `instance[Symbol.dispose]()` directly during disposal.
26
+ * - We don't use a polyfill because we avoid global mutation, keep bundles lean,
27
+ * and prefer explicit feature detection.
28
+ * - MDN reference:
29
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DisposableStack
30
+ */
31
+ export interface Multiton<K extends string, T extends Disposable>
32
+ extends Disposable {
33
+ /**
34
+ * Ensures an instance exists for the given key, creating it if necessary. If
35
+ * the instance already exists, the optional `onCacheHit` callback is invoked
36
+ * to update the existing instance.
37
+ */
38
+ readonly ensure: (
39
+ key: K,
40
+ create: () => T,
41
+ onCacheHit?: (instance: T) => void,
42
+ ) => T;
43
+
44
+ /** Gets an instance by key, or returns `null` if it doesn't exist. */
45
+ readonly get: (key: K) => T | null;
46
+
47
+ /** Checks if an instance exists for the given key. */
48
+ readonly has: (key: K) => boolean;
49
+
50
+ /**
51
+ * Removes and disposes an instance by key. Returns `true` if the instance
52
+ * existed and was disposed, `false` otherwise.
53
+ */
54
+ readonly disposeInstance: (key: K) => boolean;
55
+ }
56
+
57
+ /** Creates a {@link Multiton} instance manager. */
58
+ export const createMultiton = <
59
+ K extends string,
60
+ T extends Disposable,
61
+ >(): Multiton<K, T> => {
62
+ const instances = new Map<K, T>();
63
+
64
+ return {
65
+ ensure: (key, create, onCacheHit) => {
66
+ let instance = instances.get(key);
67
+
68
+ if (instance == null) {
69
+ instance = create();
70
+ instances.set(key, instance);
71
+ } else if (onCacheHit) {
72
+ onCacheHit(instance);
73
+ }
74
+
75
+ return instance;
76
+ },
77
+
78
+ get: (key) => instances.get(key) ?? null,
79
+
80
+ has: (key) => instances.has(key),
81
+
82
+ disposeInstance: (key) => {
83
+ const instance = instances.get(key);
84
+ if (instance) {
85
+ instance[Symbol.dispose]();
86
+ return instances.delete(key);
87
+ }
88
+ return false;
89
+ },
90
+
91
+ [Symbol.dispose]: () => {
92
+ for (const instance of instances.values()) {
93
+ instance[Symbol.dispose]();
94
+ }
95
+ instances.clear();
96
+ },
97
+ };
98
+ };
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export * from "./Evolu/Public.js";
13
13
  export * from "./Function.js";
14
14
  export * from "./Identicon.js";
15
15
  export * from "./ManyToManyMap.js";
16
+ export * from "./Multiton.js";
16
17
  export * from "./Number.js";
17
18
  export * from "./Object.js";
18
19
  export * from "./Order.js";