jazz-tools 0.7.3 → 0.7.8

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.
@@ -24,7 +24,6 @@ import {
24
24
  makeRefs,
25
25
  subscriptionsScopes,
26
26
  ItemsSym,
27
- InitValues,
28
27
  isRefEncoded,
29
28
  loadCoValue,
30
29
  loadCoValueEf,
@@ -42,17 +41,14 @@ type CoMapEdit<V> = {
42
41
  madeAt: Date;
43
42
  };
44
43
 
45
- type InitValuesFor<C extends CoMap> = {
46
- init: Simplify<CoMapInit<C>>;
47
- owner: Account | Group;
48
- };
49
-
50
44
  /**
51
45
  * CoMaps are collaborative versions of plain objects, mapping string-like keys to values.
52
46
  *
53
47
  * @categoryDescription Declaration
54
48
  * Declare your own CoMap schemas by subclassing `CoMap` and assigning field schemas with `co`.
55
49
  *
50
+ * Optional `co.ref(...)` fields must be marked with `{ optional: true }`.
51
+ *
56
52
  * ```ts
57
53
  * import { co, CoMap } from "jazz-tools";
58
54
  *
@@ -60,6 +56,7 @@ type InitValuesFor<C extends CoMap> = {
60
56
  * name = co.string;
61
57
  * age = co.number;
62
58
  * pet = co.ref(Animal);
59
+ * car = co.ref(Car, { optional: true });
63
60
  * }
64
61
  * ```
65
62
  *
@@ -103,17 +100,19 @@ export class CoMap extends CoValueBase implements CoValue {
103
100
  /**
104
101
  * If property `prop` is a `co.ref(...)`, you can use `coMaps._refs.prop` to access
105
102
  * the `Ref` instead of the potentially loaded/null value.
103
+ *
106
104
  * This allows you to always get the ID or load the value manually.
107
105
  *
108
106
  * @example
109
107
  * ```ts
110
108
  * person._refs.pet.id; // => ID<Animal>
111
109
  * person._refs.pet.value;
112
- * // => Animal | undefined
110
+ * // => Animal | null
113
111
  * const pet = await person._refs.pet.load();
114
112
  * ```
115
113
  *
116
- * @category Content */
114
+ * @category Content
115
+ **/
117
116
  get _refs(): {
118
117
  [Key in CoKeys<this>]: IfCo<this[Key], RefIfCoValue<this[Key]>>;
119
118
  } {
@@ -196,44 +195,66 @@ export class CoMap extends CoValueBase implements CoValue {
196
195
  return Account.fromNode(this._raw.core.node);
197
196
  }
198
197
 
199
- /** @internal */
200
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
201
- [InitValues]?: any;
202
-
203
198
  /** @internal */
204
199
  constructor(
205
200
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
206
- options: { fromRaw: RawCoMap } | { init: any; owner: Account | Group },
201
+ options: { fromRaw: RawCoMap } | undefined
207
202
  ) {
208
203
  super();
209
204
 
210
- if ("owner" in options) {
211
- this[InitValues] = {
212
- init: options.init,
213
- owner: options.owner,
214
- } as InitValuesFor<this>;
215
- } else if ("fromRaw" in options) {
216
- Object.defineProperties(this, {
217
- id: {
218
- value: options.fromRaw.id as unknown as ID<this>,
219
- enumerable: false,
220
- },
221
- _raw: { value: options.fromRaw, enumerable: false },
222
- });
223
- } else {
224
- throw new Error("Invalid CoMap constructor arguments");
205
+ if (options) {
206
+ if ("fromRaw" in options) {
207
+ Object.defineProperties(this, {
208
+ id: {
209
+ value: options.fromRaw.id as unknown as ID<this>,
210
+ enumerable: false,
211
+ },
212
+ _raw: { value: options.fromRaw, enumerable: false },
213
+ });
214
+ } else {
215
+ throw new Error("Invalid CoMap constructor arguments");
216
+ }
225
217
  }
226
218
 
227
219
  return new Proxy(this, CoMapProxyHandler as ProxyHandler<this>);
228
220
  }
229
221
 
230
- /** @category Creation */
222
+ /**
223
+ * Create a new CoMap with the given initial values and owner.
224
+ *
225
+ * The owner (a Group or Account) determines access rights to the CoMap.
226
+ *
227
+ * The CoMap will immediately be persisted and synced to connected peers.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * const person = Person.create({
232
+ * name: "Alice",
233
+ * age: 42,
234
+ * pet: cat,
235
+ * }, { owner: friendGroup });
236
+ * ```
237
+ *
238
+ * @category Creation
239
+ **/
231
240
  static create<M extends CoMap>(
232
241
  this: CoValueClass<M>,
233
242
  init: Simplify<CoMapInit<M>>,
234
243
  options: { owner: Account | Group },
235
244
  ) {
236
- return new this({ init, owner: options.owner });
245
+ const instance = new this();
246
+ const raw = instance.rawFromInit(
247
+ init,
248
+ options.owner,
249
+ );
250
+ Object.defineProperties(instance, {
251
+ id: {
252
+ value: raw.id,
253
+ enumerable: false,
254
+ },
255
+ _raw: { value: raw, enumerable: false },
256
+ });
257
+ return instance;
237
258
  }
238
259
 
239
260
  toJSON() {
@@ -284,6 +305,10 @@ export class CoMap extends CoValueBase implements CoValue {
284
305
  key as keyof typeof this._schema
285
306
  ] || this._schema[ItemsSym]) as Schema;
286
307
 
308
+ if (!descriptor) {
309
+ continue;
310
+ }
311
+
287
312
  if (descriptor === "json") {
288
313
  rawInit[key] = initValue as JsonValue;
289
314
  } else if (isRefEncoded(descriptor)) {
@@ -301,7 +326,24 @@ export class CoMap extends CoValueBase implements CoValue {
301
326
  return rawOwner.createMap(rawInit);
302
327
  }
303
328
 
304
- /** @category Declaration */
329
+ /**
330
+ * Declare a Record-like CoMap schema, by extending `CoMap.Record(...)` and passing the value schema using `co`. Keys are always `string`.
331
+ *
332
+ * @example
333
+ * ```ts
334
+ * import { co, CoMap } from "jazz-tools";
335
+ *
336
+ * class ColorToFruitMap extends CoMap.Record(
337
+ * co.ref(Fruit)
338
+ * ) {}
339
+ *
340
+ * // assume we have map: ColorToFruitMap
341
+ * // and strawberry: Fruit
342
+ * map["red"] = strawberry;
343
+ * ```
344
+ *
345
+ * @category Declaration
346
+ */
305
347
  static Record<Value>(value: IfCo<Value, Value>) {
306
348
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
307
349
  class RecordLikeCoMap extends CoMap {
@@ -313,7 +355,27 @@ export class CoMap extends CoValueBase implements CoValue {
313
355
  return RecordLikeCoMap;
314
356
  }
315
357
 
316
- /** @category Subscription & Loading */
358
+ /**
359
+ * Load a `CoMap` with a given ID, as a given account.
360
+ *
361
+ * `depth` specifies which (if any) fields that reference other CoValues to load as well before resolving.
362
+ * The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
363
+ *
364
+ * You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
365
+ *
366
+ * Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * const person = await Person.load(
371
+ * "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
372
+ * me,
373
+ * { pet: {} }
374
+ * );
375
+ * ```
376
+ *
377
+ * @category Subscription & Loading
378
+ */
317
379
  static load<M extends CoMap, Depth>(
318
380
  this: CoValueClass<M>,
319
381
  id: ID<M>,
@@ -323,7 +385,13 @@ export class CoMap extends CoValueBase implements CoValue {
323
385
  return loadCoValue(this, id, as, depth);
324
386
  }
325
387
 
326
- /** @category Subscription & Loading */
388
+ /**
389
+ * Effectful version of `CoMap.load()`.
390
+ *
391
+ * Needs to be run inside an `AccountCtx` context.
392
+ *
393
+ * @category Subscription & Loading
394
+ */
327
395
  static loadEf<M extends CoMap, Depth>(
328
396
  this: CoValueClass<M>,
329
397
  id: ID<M>,
@@ -332,7 +400,34 @@ export class CoMap extends CoValueBase implements CoValue {
332
400
  return loadCoValueEf<M, Depth>(this, id, depth);
333
401
  }
334
402
 
335
- /** @category Subscription & Loading */
403
+ /**
404
+ * Load and subscribe to a `CoMap` with a given ID, as a given account.
405
+ *
406
+ * Automatically also subscribes to updates to all referenced/nested CoValues as soon as they are accessed in the listener.
407
+ *
408
+ * `depth` specifies which (if any) fields that reference other CoValues to load as well before calling `listener` for the first time.
409
+ * The `DeeplyLoaded` return type guarantees that corresponding referenced CoValues are loaded to the specified depth.
410
+ *
411
+ * You can pass `[]` or `{}` for shallowly loading only this CoMap, or `{ fieldA: depthA, fieldB: depthB }` for recursively loading referenced CoValues.
412
+ *
413
+ * Check out the `load` methods on `CoMap`/`CoList`/`CoStream`/`Group`/`Account` to see which depth structures are valid to nest.
414
+ *
415
+ * Returns an unsubscribe function that you should call when you no longer need updates.
416
+ *
417
+ * Also see the `useCoState` hook to reactively subscribe to a CoValue in a React component.
418
+ *
419
+ * @example
420
+ * ```ts
421
+ * const unsub = Person.subscribe(
422
+ * "co_zdsMhHtfG6VNKt7RqPUPvUtN2Ax",
423
+ * me,
424
+ * { pet: {} },
425
+ * (person) => console.log(person)
426
+ * );
427
+ * ```
428
+ *
429
+ * @category Subscription & Loading
430
+ */
336
431
  static subscribe<M extends CoMap, Depth>(
337
432
  this: CoValueClass<M>,
338
433
  id: ID<M>,
@@ -343,7 +438,13 @@ export class CoMap extends CoValueBase implements CoValue {
343
438
  return subscribeToCoValue<M, Depth>(this, id, as, depth, listener);
344
439
  }
345
440
 
346
- /** @category Subscription & Loading */
441
+ /**
442
+ * Effectful version of `CoMap.subscribe()` that returns a stream of updates.
443
+ *
444
+ * Needs to be run inside an `AccountCtx` context.
445
+ *
446
+ * @category Subscription & Loading
447
+ */
347
448
  static subscribeEf<M extends CoMap, Depth>(
348
449
  this: CoValueClass<M>,
349
450
  id: ID<M>,
@@ -352,7 +453,13 @@ export class CoMap extends CoValueBase implements CoValue {
352
453
  return subscribeToCoValueEf<M, Depth>(this, id, depth);
353
454
  }
354
455
 
355
- /** @category Subscription & Loading */
456
+ /**
457
+ * Given an already loaded `CoMap`, ensure that the specified fields are loaded to the specified depth.
458
+ *
459
+ * Works like `CoMap.load()`, but you don't need to pass the ID or the account to load as again.
460
+ *
461
+ * @category Subscription & Loading
462
+ */
356
463
  ensureLoaded<M extends CoMap, Depth>(
357
464
  this: M,
358
465
  depth: Depth & DepthsIn<M>,
@@ -360,7 +467,15 @@ export class CoMap extends CoValueBase implements CoValue {
360
467
  return ensureCoValueLoaded(this, depth);
361
468
  }
362
469
 
363
- /** @category Subscription & Loading */
470
+ /**
471
+ * Given an already loaded `CoMap`, subscribe to updates to the `CoMap` and ensure that the specified fields are loaded to the specified depth.
472
+ *
473
+ * Works like `CoMap.subscribe()`, but you don't need to pass the ID or the account to load as again.
474
+ *
475
+ * Returns an unsubscribe function that you should call when you no longer need updates.
476
+ *
477
+ * @category Subscription & Loading
478
+ **/
364
479
  subscribe<M extends CoMap, Depth>(
365
480
  this: M,
366
481
  depth: Depth & DepthsIn<M>,
@@ -381,30 +496,6 @@ export type CoMapInit<Map extends object> = {
381
496
  : IfCo<Map[Key], Key>]: Map[Key];
382
497
  } & { [Key in CoKeys<Map> as IfCo<Map[Key], Key>]?: Map[Key] };
383
498
 
384
- function tryInit(map: CoMap) {
385
- if (
386
- map[InitValues] &&
387
- (map._schema[ItemsSym] ||
388
- Object.keys(map[InitValues].init).every(
389
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
- (key) => (map._schema as any)[key],
391
- ))
392
- ) {
393
- const raw = map.rawFromInit(
394
- map[InitValues].init,
395
- map[InitValues].owner,
396
- );
397
- Object.defineProperties(map, {
398
- id: {
399
- value: raw.id,
400
- enumerable: false,
401
- },
402
- _raw: { value: raw, enumerable: false },
403
- });
404
- delete map[InitValues];
405
- }
406
- }
407
-
408
499
  // TODO: cache handlers per descriptor for performance?
409
500
  const CoMapProxyHandler: ProxyHandler<CoMap> = {
410
501
  get(target, key, receiver) {
@@ -447,7 +538,6 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
447
538
  (target.constructor as typeof CoMap)._schema ||= {};
448
539
  (target.constructor as typeof CoMap)._schema[key] =
449
540
  value[SchemaInit];
450
- tryInit(target);
451
541
  return true;
452
542
  }
453
543
 
@@ -478,7 +568,6 @@ const CoMapProxyHandler: ProxyHandler<CoMap> = {
478
568
  (target.constructor as typeof CoMap)._schema ||= {};
479
569
  (target.constructor as typeof CoMap)._schema[key as string] =
480
570
  attributes.value[SchemaInit];
481
- tryInit(target);
482
571
  return true;
483
572
  } else {
484
573
  return Reflect.defineProperty(target, key, attributes);
@@ -30,7 +30,6 @@ import {
30
30
  Ref,
31
31
  inspect,
32
32
  co,
33
- InitValues,
34
33
  SchemaInit,
35
34
  isRefEncoded,
36
35
  loadCoValue,
@@ -93,9 +92,6 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
93
92
  return this.perSession[this._loadedAs.sessionID!];
94
93
  }
95
94
 
96
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
- [InitValues]?: any;
98
-
99
95
  constructor(
100
96
  options:
101
97
  | { init: Item[]; owner: Account | Group }
@@ -103,7 +99,7 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
103
99
  ) {
104
100
  super();
105
101
 
106
- if ("fromRaw" in options) {
102
+ if (options && "fromRaw" in options) {
107
103
  Object.defineProperties(this, {
108
104
  id: {
109
105
  value: options.fromRaw.id,
@@ -111,11 +107,6 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
111
107
  },
112
108
  _raw: { value: options.fromRaw, enumerable: false },
113
109
  });
114
- } else {
115
- this[InitValues] = {
116
- init: options.init,
117
- owner: options.owner,
118
- };
119
110
  }
120
111
 
121
112
  return new Proxy(this, CoStreamProxyHandler as ProxyHandler<this>);
@@ -126,7 +117,21 @@ export class CoStream<Item = any> extends CoValueBase implements CoValue {
126
117
  init: S extends CoStream<infer Item> ? UnCo<Item>[] : never,
127
118
  options: { owner: Account | Group },
128
119
  ) {
129
- return new this({ init, owner: options.owner });
120
+ const instance = new this({ init, owner: options.owner });
121
+ const raw = options.owner._raw.createStream();
122
+
123
+ Object.defineProperties(instance, {
124
+ id: {
125
+ value: raw.id,
126
+ enumerable: false,
127
+ },
128
+ _raw: { value: raw, enumerable: false },
129
+ });
130
+
131
+ if (init) {
132
+ instance.push(...init);
133
+ }
134
+ return instance;
130
135
  }
131
136
 
132
137
  push(...items: Item[]) {
@@ -317,27 +322,6 @@ function entryFromRawEntry<Item>(
317
322
  };
318
323
  }
319
324
 
320
- function init(stream: CoStream) {
321
- const init = stream[InitValues];
322
- if (!init) return;
323
-
324
- const raw = init.owner._raw.createStream();
325
-
326
- Object.defineProperties(stream, {
327
- id: {
328
- value: raw.id,
329
- enumerable: false,
330
- },
331
- _raw: { value: raw, enumerable: false },
332
- });
333
-
334
- if (init.init) {
335
- stream.push(...init.init);
336
- }
337
-
338
- delete stream[InitValues];
339
- }
340
-
341
325
  export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
342
326
  get(target, key, receiver) {
343
327
  if (typeof key === "string" && key.startsWith("co_")) {
@@ -391,7 +375,6 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
391
375
  (target.constructor as typeof CoStream)._schema ||= {};
392
376
  (target.constructor as typeof CoStream)._schema[ItemsSym] =
393
377
  value[SchemaInit];
394
- init(target);
395
378
  return true;
396
379
  } else {
397
380
  return Reflect.set(target, key, value, receiver);
@@ -407,7 +390,6 @@ export const CoStreamProxyHandler: ProxyHandler<CoStream> = {
407
390
  (target.constructor as typeof CoStream)._schema ||= {};
408
391
  (target.constructor as typeof CoStream)._schema[ItemsSym] =
409
392
  descriptor.value[SchemaInit];
410
- init(target);
411
393
  return true;
412
394
  } else {
413
395
  return Reflect.defineProperty(target, key, descriptor);
@@ -114,7 +114,7 @@ export class Group extends CoValueBase implements CoValue {
114
114
  const initOwner = options.owner;
115
115
  if (!initOwner) throw new Error("No owner provided");
116
116
  if (
117
- initOwner instanceof Account &&
117
+ initOwner._type === "Account" &&
118
118
  isControlledAccount(initOwner)
119
119
  ) {
120
120
  const rawOwner = initOwner._raw;
@@ -68,6 +68,7 @@ export class CoValueBase implements CoValue {
68
68
  id!: ID<this>;
69
69
  _type!: string;
70
70
  _raw!: RawCoValue;
71
+ /** @category Internals */
71
72
  _instanceID!: string;
72
73
 
73
74
  get _owner(): Account | Group {
@@ -1,9 +1,6 @@
1
1
  export const SchemaInit = Symbol.for("SchemaInit");
2
2
  export type SchemaInit = typeof SchemaInit;
3
3
 
4
- export const InitValues = Symbol.for("InitValues");
5
- export type InitValues = typeof InitValues;
6
-
7
4
  export const ItemsSym = Symbol.for("items");
8
5
  export type ItemsSym = typeof ItemsSym;
9
6
 
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ export type { ID, CoValue } from "./internal.js";
17
17
 
18
18
  export { Encoders, co } from "./internal.js";
19
19
 
20
- export { CoMap } from "./internal.js";
20
+ export { CoMap, type CoMapInit } from "./internal.js";
21
21
  export { CoList } from "./internal.js";
22
22
  export { CoStream, BinaryCoStream } from "./internal.js";
23
23
  export { Group, Profile } from "./internal.js";
@@ -52,6 +52,22 @@ describe("Simple CoMap operations", async () => {
52
52
  expect(Object.keys(map)).toEqual(["color", "_height", "birthday"]);
53
53
  });
54
54
 
55
+ test("Construction with too many things provided", () => {
56
+ const mapWithExtra = TestMap.create(
57
+ {
58
+ color: "red",
59
+ _height: 10,
60
+ birthday: birthday,
61
+ name: "Hermes",
62
+ extra: "extra",
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ } as any,
65
+ { owner: me },
66
+ );
67
+
68
+ expect(mapWithExtra.color).toEqual("red");
69
+ })
70
+
55
71
  describe("Mutation", () => {
56
72
  test("assignment & deletion", () => {
57
73
  map.color = "blue";