jazz-tools 0.7.3 → 0.7.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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";