@graffiti-garden/api 0.5.1 → 0.6.1

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/src/1-api.ts CHANGED
@@ -5,8 +5,10 @@ import type {
5
5
  GraffitiPatch,
6
6
  GraffitiSession,
7
7
  GraffitiPutObject,
8
- GraffitiStream,
8
+ GraffitiObjectStream,
9
9
  ChannelStats,
10
+ GraffitiChannelStatsStream,
11
+ GraffitiObjectStreamContinue,
10
12
  } from "./2-types";
11
13
  import type { JSONSchema } from "json-schema-to-ts";
12
14
 
@@ -195,13 +197,19 @@ export abstract class Graffiti {
195
197
  * Replacement occurs when the {@link GraffitiObjectBase.url | `url`} of
196
198
  * the replaced object exactly matches an existing object's URL.
197
199
  *
198
- * @returns The object that was replaced if one exists or an object with
200
+ * @throws {@link GraffitiErrorNotFound} if a {@link GraffitiObjectBase.url | `url`}
201
+ * is provided that has not been created yet or the {@link GraffitiObjectBase.actor | `actor`}
202
+ * is not {@link GraffitiObjectBase.allowed | `allowed`} to see it.
203
+ *
204
+ * @throws {@link GraffitiErrorForbidden} if the {@link GraffitiObjectBase.actor | `actor`}
205
+ * is not the same `actor` as the one who created the object.
206
+ *
207
+ * @returns The object that was replaced if one one exists, otherwise an object with
199
208
  * with an empty {@link GraffitiObjectBase.value | `value`},
200
209
  * {@link GraffitiObjectBase.channels | `channels`}, and {@link GraffitiObjectBase.allowed | `allowed`}
201
- * list if the method created a new object.
202
- * In either case, the object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
203
- * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
204
- * field updated to the time of replacement/creation.
210
+ * list.
211
+ * The {@link GraffitiObjectBase.lastModified | `lastModified`} property of the returned object
212
+ * will be updated to the time of replacement/creation.
205
213
  *
206
214
  * @group CRUD Methods
207
215
  */
@@ -221,7 +229,7 @@ export abstract class Graffiti {
221
229
  ): Promise<GraffitiObjectBase>;
222
230
 
223
231
  /**
224
- * Retrieves an object from a given location.
232
+ * Retrieves an object from a given {@link GraffitiObjectBase.url | `url`}.
225
233
  *
226
234
  * The retrieved object is type-checked against the provided [JSON schema](https://json-schema.org/)
227
235
  * otherwise a {@link GraffitiErrorSchemaMismatch} is thrown.
@@ -232,22 +240,10 @@ export abstract class Graffiti {
232
240
  * {@link GraffitiObjectBase.channels | `channels`} properties are
233
241
  * not revealed, similar to a BCC.
234
242
  *
235
- * If the object existed but has since been deleted,
236
- * or the retrieving {@link GraffitiObjectBase.actor | `actor`}
237
- * was {@link GraffitiObjectBase.allowed | `allowed`} to access
238
- * the object but now isn't, this method may return the latest
239
- * version of the object that the {@link GraffitiObjectBase.actor | `actor`}
240
- * was allowed to access with its {@link GraffitiObjectBase.tombstone | `tombstone`}
241
- * set to `true`. This allows for the invalidation of the object
242
- * in a client-side cache.
243
- *
244
- * Otherwise, if the object never existed, or the
245
- * retrieving {@link GraffitiObjectBase.actor | `actor`} was never
246
- * {@link GraffitiObjectBase.allowed | `allowed`} to access it, or if
247
- * the object was changed long enough ago that it has been permanently deleted,
248
- * a {@link GraffitiErrorNotFound} is thrown.
249
- * The rate at which permanent deletions happen is implementation dependent.
250
- * See the `tombstoneRetention` property returned by {@link discover}.
243
+ * @throws {@link GraffitiErrorNotFound} if the object does not exist, has been deleted, or the user is not
244
+ * {@link GraffitiObjectBase.allowed | `allowed`} to access it.
245
+ *
246
+ * @throws {@link GraffitiErrorSchemaMismatch} if the retrieved object does not match the provided schema.
251
247
  *
252
248
  * @group CRUD Methods
253
249
  */
@@ -270,16 +266,19 @@ export abstract class Graffiti {
270
266
  ): Promise<GraffitiObject<Schema>>;
271
267
 
272
268
  /**
273
- * Patches an existing object at a given location.
269
+ * Patches an existing object at a given {@link GraffitiObjectBase.url | `url`}.
274
270
  * The patching {@link GraffitiObjectBase.actor | `actor`} must be the same as the
275
271
  * `actor` that created the object.
276
272
  *
277
- * @returns The original object prior to the patch
278
- * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
279
- * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
280
- * field updated to the time of deletion.
273
+ * @returns The original object prior to the patch with its
274
+ * {@link GraffitiObjectBase.lastModified | `lastModified`}
275
+ * property updated to the time of deletion.
276
+ *
277
+ * @throws {@link GraffitiErrorNotFound} if the object does not exist, has already been deleted,
278
+ * or the user is not {@link GraffitiObjectBase.allowed | `allowed`} to access it.
281
279
  *
282
- * @throws {@link GraffitiErrorNotFound} if the object does not exist or has already been deleted.
280
+ * @throws {@link GraffitiErrorForbidden} if the {@link GraffitiObjectBase.actor | `actor`}
281
+ * is not the same `actor` as the one who created the object.
283
282
  *
284
283
  * @group CRUD Methods
285
284
  */
@@ -301,16 +300,25 @@ export abstract class Graffiti {
301
300
  ): Promise<GraffitiObjectBase>;
302
301
 
303
302
  /**
304
- * Deletes an object from a given location.
303
+ * Deletes an object from a given {@link GraffitiObjectBase.url | `url`}.
305
304
  * The deleting {@link GraffitiObjectBase.actor | `actor`} must be the same as the
306
305
  * `actor` that created the object.
307
306
  *
308
- * @returns The object that was deleted if one exists.
309
- * The object will have a {@link GraffitiObjectBase.tombstone | `tombstone`}
310
- * field set to `true` and a {@link GraffitiObjectBase.lastModified | `lastModified`}
311
- * field updated to the time of deletion.
307
+ * It is not possible to re-{@link put} an object that has been deleted
308
+ * to ensure a user's [right to be forgotten](https://en.wikipedia.org/wiki/Right_to_be_forgotten).
309
+ * In cases where deleting and restoring an object is useful, an object's
310
+ * {@link GraffitiObjectBase.allowed | `allowed`} property can be set to
311
+ * an empty list to hide it from all users except the creator.
312
312
  *
313
- * @throws {@link GraffitiErrorNotFound} if the object does not exist or has already been deleted.
313
+ * @returns The object that was deleted with its
314
+ * {@link GraffitiObjectBase.lastModified | `lastModified`}
315
+ * property updated to the time of deletion.
316
+ *
317
+ * @throws {@link GraffitiErrorNotFound} if the object does not exist, has already been deleted,
318
+ * or the user is not {@link GraffitiObjectBase.allowed | `allowed`} to access it.
319
+ *
320
+ * @throws {@link GraffitiErrorForbidden} if the {@link GraffitiObjectBase.actor | `actor`}
321
+ * is not the same `actor` as the one who created the object.
314
322
  *
315
323
  * @group CRUD Methods
316
324
  */
@@ -333,54 +341,27 @@ export abstract class Graffiti {
333
341
  *
334
342
  * Objects are returned asynchronously as they are discovered but the stream
335
343
  * will end once all leads have been exhausted.
336
- * The method must be polled again for new objects.
344
+ * The {@link GraffitiObjectStream} ends by returning a `continue`
345
+ * method and a `cursor` string, each of which can be be used to poll for new objects.
346
+ * The `continue` method preserves the type safety of the stream and the `cursor`
347
+ * string can be serialized to continue the stream after an application is closed
348
+ * and reopened.
349
+ * See the {@link continueObjectStream} method for more information.
337
350
  *
338
351
  * `discover` will not return objects that the {@link GraffitiObjectBase.actor | `actor`}
339
352
  * is not {@link GraffitiObjectBase.allowed | `allowed`} to access.
340
- * If the actor is not the creator of a discovered object,
353
+ * If the `actor` is not the creator of a discovered object,
341
354
  * the allowed list will be masked to only contain the querying actor if the
342
355
  * allowed list is not `undefined` (public). Additionally, if the actor is not the
343
356
  * creator of a discovered object, any {@link GraffitiObjectBase.channels | `channels`}
344
357
  * not specified by the `discover` method will not be revealed. This masking happens
345
- * before the supplied schema is applied.
358
+ * before the object is validated against the supplied `schema`.
346
359
  *
347
360
  * Since different implementations may fetch data from multiple sources there is
348
- * no guarentee on the order that objects are returned in. Additionally, the method
349
- * will return objects that have been deleted but with a
350
- * {@link GraffitiObjectBase.tombstone | `tombstone`} field set to `true` for
351
- * client-side cache invalidation purposes.
352
- * The final `return()` value of the stream includes a `tombstoneRetention`
353
- * property that represents the minimum amount of time,
354
- * in milliseconds, that an application will retain and return tombstones for objects that
355
- * have been deleted.
356
- *
357
- * When repolling, the {@link GraffitiObjectBase.lastModified | `lastModified`}
358
- * field can be queried via the schema to
359
- * only fetch objects that have been modified since the last poll.
360
- * Such queries should only be done if the time since the last poll
361
- * is less than the `tombstoneRetention` value of that poll, otherwise the tombstones
362
- * for objects that have been deleted may not be returned.
363
- *
364
- * ```json
365
- * {
366
- * "properties": {
367
- * "lastModified": {
368
- * "minimum": LAST_RETRIEVED_TIME
369
- * }
370
- * }
371
- * }
372
- * ```
373
- *
374
- * `discover` needs to be polled for new data because live updates to
375
- * an application can be visually distracting or lead to toxic engagement.
376
- * If and when an application wants real-time updates, such as in a chat
377
- * application, application authors must be intentional about their polling.
378
- *
379
- * Implementers should be aware that some users may applications may try to poll
380
- * {@link discover} repetitively. They can deal with this by rate limiting or
381
- * preemptively fetching data via a bidirectional channel, like a WebSocket.
382
- * Additionally, implementers should probably index the `lastModified` field
383
- * to speed up responses to schemas like the one above.
361
+ * no guarentee on the order that objects are returned in.
362
+ * It is also possible that duplicate objects are returned and their
363
+ * {@link GraffitiObjectBase.lastModified | `lastModified`} fields must be used
364
+ * to determine which object is the most recent.
384
365
  *
385
366
  * @returns A stream of objects that match the given {@link GraffitiObjectBase.channels | `channels`}
386
367
  * and [JSON Schema](https://json-schema.org).
@@ -403,12 +384,7 @@ export abstract class Graffiti {
403
384
  * property will be returned.
404
385
  */
405
386
  session?: GraffitiSession | null,
406
- ): GraffitiStream<
407
- GraffitiObject<Schema>,
408
- {
409
- tombstoneRetention: number;
410
- }
411
- >;
387
+ ): GraffitiObjectStream<Schema>;
412
388
 
413
389
  /**
414
390
  * Discovers objects **not** contained in any
@@ -421,7 +397,13 @@ export abstract class Graffiti {
421
397
  * getting a global view of all a user's Graffiti data or debugging
422
398
  * channel usage.
423
399
  *
424
- * It's return value is the same as {@link discover}.
400
+ * Like {@link discover}, objects are returned asynchronously as they are discovered,
401
+ * the stream will end once all leads have been exhausted, and the stream
402
+ * can be continued using the `continue` method or `cursor` string.
403
+ *
404
+ * @returns A stream of objects created by the querying {@link GraffitiObjectBase.actor | `actor`}
405
+ * that do not belong to any {@link GraffitiObjectBase.channels | `channels`}
406
+ * and match the given [JSON Schema](https://json-schema.org).
425
407
  *
426
408
  * @group Query Methods
427
409
  */
@@ -435,12 +417,7 @@ export abstract class Graffiti {
435
417
  * {@link GraffitiObjectBase.actor | `actor`}.
436
418
  */
437
419
  session: GraffitiSession,
438
- ): GraffitiStream<
439
- GraffitiObject<Schema>,
440
- {
441
- tombstoneRetention: number;
442
- }
443
- >;
420
+ ): GraffitiObjectStream<Schema>;
444
421
 
445
422
  /**
446
423
  * Returns statistics about all the {@link GraffitiObjectBase.channels | `channels`}
@@ -450,6 +427,9 @@ export abstract class Graffiti {
450
427
  * global view of all their Graffiti data or to debug
451
428
  * channel usage.
452
429
  *
430
+ * Like {@link discover}, objects are returned asynchronously as they are discovered,
431
+ * the stream will end once all leads have been exhausted.
432
+ *
453
433
  * @group Query Methods
454
434
  *
455
435
  * @returns A stream of statistics for each {@link GraffitiObjectBase.channels | `channel`}
@@ -461,7 +441,37 @@ export abstract class Graffiti {
461
441
  * {@link GraffitiObjectBase.actor | `actor`}.
462
442
  */
463
443
  session: GraffitiSession,
464
- ): GraffitiStream<ChannelStats>;
444
+ ): GraffitiChannelStatsStream;
445
+
446
+ /**
447
+ * Continues a {@link GraffitiObjectStream} from a given `cursor` string.
448
+ * The continuation will return new objects that have been created
449
+ * that match the original stream, and also returns the
450
+ * {@link GraffitiObjectBase.url | `url`}s of objects that
451
+ * have been deleted, as marked by a `tombstone`.
452
+ *
453
+ * The continuation may also include duplicates of objects that
454
+ * were already returned by the original stream. This is dependent
455
+ * on how much state the underlying implementation maintains.
456
+ *
457
+ * The `cursor` allows the client to
458
+ * serialize the state of the stream and continue it later.
459
+ * However this method loses any typing information that was
460
+ * present in the original stream. For better type safety
461
+ * and when serializing is not necessary, use the `continue`
462
+ * method instead, returned along with the `cursor` at the
463
+ * end of the original stream.
464
+ *
465
+ * @throws {@link GraffitiErrorForbidden} if the {@link GraffitiObjectBase.actor | `actor`}
466
+ * provided in the `session` is not the same as the `actor`
467
+ * that initiated the original stream.
468
+ *
469
+ * @group Query Methods
470
+ */
471
+ abstract continueObjectStream(
472
+ cursor: string,
473
+ session?: GraffitiSession | null,
474
+ ): GraffitiObjectStreamContinue<{}>;
465
475
 
466
476
  /**
467
477
  * Begins the login process. Depending on the implementation, this may
package/src/2-types.ts CHANGED
@@ -17,8 +17,7 @@ import type { Operation as JSONPatchOperation } from "fast-json-patch";
17
17
  * The {@link channels | `channels`} and {@link allowed | `allowed`} properties
18
18
  * enable the object's creator to shape the visibility of and access to their object.
19
19
  *
20
- * The {@link tombstone | `tombstone`} and {@link lastModified | `lastModified`} properties are for
21
- * caching and synchronization.
20
+ * The {@link lastModified | `lastModified`} property can be used to compare object versions.
22
21
  */
23
22
  export interface GraffitiObjectBase {
24
23
  /**
@@ -81,9 +80,9 @@ export interface GraffitiObjectBase {
81
80
  * If an object is {@link Graffiti.put | put} with the same URL
82
81
  * as an existing object, the existing object will be replaced with the new object.
83
82
  *
84
- * An object's URL is generated when the object is first creation and
83
+ * An object's URL is generated when the object is first created and
85
84
  * should include sufficient randomness to prevent collisions
86
- * and guessing. The URL starts with a "scheme", just like web URLs start with `http` or `https`, to indicate
85
+ * and guessing. The URL starts with a "scheme," just like web URLs start with `http` or `https`, to indicate
87
86
  * to indicate the particular Graffiti implementation. This allows for applications
88
87
  * to pull from multiple coexisting Graffiti implementations without collision.
89
88
  * Existing schemes include `graffiti:local:` for objects stored locally
@@ -97,7 +96,7 @@ export interface GraffitiObjectBase {
97
96
 
98
97
  /**
99
98
  * The time the object was last modified, measured in milliseconds since January 1, 1970.
100
- * This is used for client-side caching and synchronization.
99
+ * It can be used to compare object versions.
101
100
  * A number, rather than an ISO string or Date object, is used for easy comparison, sorting,
102
101
  * and JSON Schema [range queries](https://json-schema.org/understanding-json-schema/reference/numeric#range).
103
102
  *
@@ -107,13 +106,6 @@ export interface GraffitiObjectBase {
107
106
  * rather than when it was modified.
108
107
  */
109
108
  lastModified: number;
110
-
111
- /**
112
- * A boolean indicating whether the object has been deleted.
113
- * Depending on implementation, objects stay available for some time
114
- * after deletion to allow for synchronization and client-side caching
115
- */
116
- tombstone: boolean;
117
109
  }
118
110
 
119
111
  /**
@@ -141,10 +133,9 @@ export const GraffitiObjectJSONSchema = {
141
133
  url: { type: "string" },
142
134
  actor: { type: "string" },
143
135
  lastModified: { type: "number" },
144
- tombstone: { type: "boolean" },
145
136
  },
146
137
  additionalProperties: false,
147
- required: ["value", "channels", "actor", "url", "lastModified", "tombstone"],
138
+ required: ["value", "channels", "actor", "url", "lastModified"],
148
139
  } as const satisfies JSONSchema;
149
140
 
150
141
  /**
@@ -167,8 +158,8 @@ export type GraffitiObjectUrl = Pick<GraffitiObjectBase, "url">;
167
158
  * It may also include a {@link GraffitiObjectBase.url | `url`} property to specify the
168
159
  * URL of an existing object to replace. If no `url` is provided, one will be generated during object creation.
169
160
  *
170
- * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`} or {@link GraffitiObjectBase.tombstone | `tombstone`}
171
- * property since these are automatically generated by the Graffiti system.
161
+ * This object does not need a {@link GraffitiObjectBase.lastModified | `lastModified`}
162
+ * property since it will be automatically generated by the Graffiti system.
172
163
  */
173
164
  export type GraffitiPutObject<Schema extends JSONSchema> = Pick<
174
165
  GraffitiObjectBase,
@@ -266,8 +257,7 @@ export interface GraffitiPatch {
266
257
  }
267
258
 
268
259
  /**
269
- * This type represents a stream of data that are
270
- * returned by Graffiti's query-like operations such as
260
+ * A stream of data that are returned by Graffiti's query-like operations
271
261
  * {@link Graffiti.discover} and {@link Graffiti.recoverOrphans}.
272
262
  *
273
263
  * Errors are returned within the stream rather than as
@@ -275,26 +265,159 @@ export interface GraffitiPatch {
275
265
  * some implementations may pull data from multiple sources
276
266
  * including some that may be unreliable. In many cases,
277
267
  * these errors can be safely ignored.
278
- * The `origin` property of the error object indicates the
279
- * source of the error including its scheme and other
280
- * implementation-specific details (e.g. domain name).
268
+ * See {@link GraffitiStreamError}.
281
269
  *
282
270
  * The stream is an [`AsyncGenerator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
283
271
  * that can be iterated over using `for await` loops or calling `next` on the generator.
284
272
  * The stream can be terminated by breaking out of a loop calling `return` on the generator.
273
+ *
274
+ * The stream ends by returning a {@link GraffitiObjectStreamReturn.continue | `continue`}
275
+ * function and a {@link GraffitiObjectStreamReturn.cursor | `cursor`} string,
276
+ * each of which can be used to resume the stream from where it left off.
285
277
  */
286
- export type GraffitiStream<TValue, TReturn = void> = AsyncGenerator<
287
- | {
288
- error?: undefined;
289
- value: TValue;
290
- }
291
- | {
292
- error: Error;
293
- origin: string;
294
- },
295
- TReturn
278
+ export type GraffitiObjectStream<Schema extends JSONSchema> = AsyncGenerator<
279
+ GraffitiStreamError | GraffitiObjectStreamEntry<Schema>,
280
+ GraffitiObjectStreamReturn<Schema>
296
281
  >;
297
282
 
283
+ /**
284
+ * An error that can occur in either the
285
+ * {@link GraffitiObjectStream} or {@link GraffitiChannelStatsStream}.
286
+ *
287
+ * @internal
288
+ */
289
+ export interface GraffitiStreamError {
290
+ /**
291
+ * The error that occurred while streaming data.
292
+ */
293
+ error: Error;
294
+ /**
295
+ * The origin that the error occurred. It will include
296
+ * the scheme of the Graffiti implementation used and other
297
+ * implementation-specific information like a hostname.
298
+ */
299
+ origin: string;
300
+ }
301
+
302
+ /**
303
+ * A successful result from a {@link GraffitiObjectStream} or
304
+ * {@link GraffitiObjectStreamContinue} that includes an object.
305
+ *
306
+ * @internal
307
+ */
308
+ export interface GraffitiObjectStreamEntry<Schema extends JSONSchema> {
309
+ /**
310
+ * Empty property for compatibility with {@link GraffitiStreamError}
311
+ */
312
+ error?: undefined;
313
+ /**
314
+ * Empty property for compatibility with {@link GraffitiObjectStreamContinueTombstone}
315
+ */
316
+ tombstone?: undefined;
317
+ /**
318
+ * The object returned by the stream.
319
+ */
320
+ object: GraffitiObject<Schema>;
321
+ }
322
+
323
+ /**
324
+ * A result from a {@link GraffitiObjectStreamContinue} that indicated
325
+ * an object has been deleted since the original stream was run.
326
+ * Only sparse metadata about the deleted object is returned to respect
327
+ * the deleting user's privacy.
328
+ *
329
+ * @internal
330
+ */
331
+ export interface GraffitiObjectStreamContinueTombstone {
332
+ /**
333
+ * Empty property for compatibility with {@link GraffitiStreamError}
334
+ */
335
+ error?: undefined;
336
+ /**
337
+ * Use this property to differentiate a tombstone from a
338
+ * {@link GraffitiObjectStreamEntry}.
339
+ */
340
+ tombstone: true;
341
+ /**
342
+ * Sparse metadata about the deleted object. The full object is not returned
343
+ * to respect a user's privacy.
344
+ */
345
+ object: {
346
+ /**
347
+ * The {@link GraffitiObjectBase.url | `url`} of the deleted object.
348
+ */
349
+ url: string;
350
+ /**
351
+ * The time at which the object was deleted, comparable to
352
+ * {@link GraffitiObjectBase.lastModified | `lastModified`}.
353
+ *
354
+ * While it is not possible to re-{@link Graffiti.put | put} objects that have been
355
+ * {@link Graffiti.delete | deleted}, objects may appear deleted if
356
+ * an {@link GraffitiObjectBase.actor | `actor`} is no longer
357
+ * {@link GraffitiObjectBase.allowed | `allowed`} to access them.
358
+ * Therefore the {@link GraffitiObjectBase.lastModified | `lastModified`} property
359
+ * is necessary to compare object versions.
360
+ */
361
+ lastModified: number;
362
+ };
363
+ }
364
+
365
+ /**
366
+ * A continuation of the {@link GraffitiObjectStream} type can include
367
+ * both objects and tombstones of deleted objects.
368
+ *
369
+ * @internal
370
+ */
371
+ export type GraffitiObjectStreamContinueEntry<Schema extends JSONSchema> =
372
+ | GraffitiObjectStreamEntry<Schema>
373
+ | GraffitiObjectStreamContinueTombstone;
374
+
375
+ /**
376
+ * The output of a {@link GraffitiObjectStream} or a {@link GraffitiObjectStreamContinue}
377
+ * that allows the stream to be continued from where it left off.
378
+ *
379
+ * The {@link continue} function preserves the typing of the original stream,
380
+ * where as the {@link cursor} string can be serialized for use after a user
381
+ * has closed and reopened an application.
382
+ *
383
+ * The continued stream may include `tombstone`s of objects that have been
384
+ * deleted since the original stream was run. See {@link GraffitiObjectStreamContinueTombstone}.
385
+ * The continued stream may also return some objects that were already
386
+ * returned by the original stream, depending on how much state the
387
+ * underlying implementation is able to preserve.
388
+ *
389
+ * @internal
390
+ */
391
+ export interface GraffitiObjectStreamReturn<Schema extends JSONSchema> {
392
+ /**
393
+ * @returns A function that creates new stream that continues from where the original stream left off.
394
+ * It preserves the typing of the original stream.
395
+ */
396
+ continue: () => GraffitiObjectStreamContinue<Schema>;
397
+ /**
398
+ * A string that can be serialized and stored to resume the stream later.
399
+ * It must be passed to the {@link Graffiti.continueObjectStream} method
400
+ * to resume the stream.
401
+ */
402
+ cursor: string;
403
+ }
404
+
405
+ /**
406
+ * A continutation of the {@link GraffitiObjectStream} type, as returned by
407
+ * the {@link GraffitiObjectStreamReturn.continue} or by using
408
+ * {@link GraffitiObjectStreamReturn.cursor} with {@link Graffiti.continueObjectStream}.
409
+ *
410
+ * The continued stream may include `tombstone`s of objects that have been
411
+ * deleted since the original stream was run. See {@link GraffitiObjectStreamContinueTombstone}.
412
+ *
413
+ * @internal
414
+ */
415
+ export type GraffitiObjectStreamContinue<Schema extends JSONSchema> =
416
+ AsyncGenerator<
417
+ GraffitiStreamError | GraffitiObjectStreamContinueEntry<Schema>,
418
+ GraffitiObjectStreamReturn<Schema>
419
+ >;
420
+
298
421
  /**
299
422
  * Statistic about single channel returned by {@link Graffiti.channelStats}.
300
423
  * These statistics only account for contributions made by the
@@ -306,18 +429,30 @@ export type ChannelStats = {
306
429
  */
307
430
  channel: string;
308
431
  /**
309
- * The number of non-{@link GraffitiObjectBase.tombstone | `tombstone`}d objects
310
- * that the actor has posted to the channel.
432
+ * The number of objects that the actor has {@link Graffiti.put | put}
433
+ * and not {@link Graffiti.delete | deleted} in the channel.
311
434
  */
312
435
  count: number;
313
436
  /**
314
437
  * The time that the actor {@link GraffitiObjectBase.lastModified | last modified} an object in the channel,
315
438
  * measured in milliseconds since January 1, 1970.
316
- * {@link GraffitiObjectBase.tombstone | Tombstone}d objects do not effect this modification time.
439
+ * {@link Graffiti.delete | Deleted} objects do not effect this modification time.
317
440
  */
318
441
  lastModified: number;
319
442
  };
320
443
 
444
+ /**
445
+ * A stream of data that are returned by Graffiti's {@link Graffiti.channelStats} method.
446
+ * See {@link GraffitiObjectStream} for more information on streams.
447
+ */
448
+ export type GraffitiChannelStatsStream = AsyncGenerator<
449
+ | GraffitiStreamError
450
+ | {
451
+ error?: undefined;
452
+ value: ChannelStats;
453
+ }
454
+ >;
455
+
321
456
  /**
322
457
  * The event type produced in {@link Graffiti.sessionEvents}
323
458
  * when a user logs in manually from {@link Graffiti.login}
package/tests/crud.ts CHANGED
@@ -72,7 +72,6 @@ export const graffitiCRUDTests = (
72
72
  session,
73
73
  );
74
74
  expect(beforeReplaced.value).toEqual(value);
75
- expect(beforeReplaced.tombstone).toEqual(true);
76
75
  expect(beforeReplaced.url).toEqual(previous.url);
77
76
  expect(beforeReplaced.actor).toEqual(previous.actor);
78
77
  expect(beforeReplaced.lastModified).toBeGreaterThanOrEqual(
@@ -83,24 +82,31 @@ export const graffitiCRUDTests = (
83
82
  const afterReplaced = await graffiti.get(previous, {});
84
83
  expect(afterReplaced.value).toEqual(newValue);
85
84
  expect(afterReplaced.lastModified).toEqual(beforeReplaced.lastModified);
86
- expect(afterReplaced.tombstone).toEqual(false);
87
85
 
88
86
  // Delete it
89
87
  const beforeDeleted = await graffiti.delete(afterReplaced, session);
90
- expect(beforeDeleted.tombstone).toEqual(true);
91
88
  expect(beforeDeleted.value).toEqual(newValue);
92
89
  expect(beforeDeleted.lastModified).toBeGreaterThanOrEqual(
93
90
  beforeReplaced.lastModified,
94
91
  );
95
92
 
96
- // Get a tombstone
97
- const final = await graffiti.get(afterReplaced, {});
98
- expect(final).toEqual(beforeDeleted);
93
+ // Get is not found
94
+ await expect(graffiti.get(afterReplaced, {})).rejects.toBeInstanceOf(
95
+ GraffitiErrorNotFound,
96
+ );
99
97
 
100
98
  // Delete it again
101
- await expect(graffiti.delete(final, session)).rejects.toThrow(
99
+ await expect(graffiti.delete(beforeDeleted, session)).rejects.toThrow(
102
100
  GraffitiErrorNotFound,
103
101
  );
102
+
103
+ // Try to re-put it
104
+ await expect(
105
+ graffiti.put(
106
+ { url: beforeDeleted.url, value: {}, channels: [] },
107
+ session,
108
+ ),
109
+ ).rejects.toThrow(GraffitiErrorNotFound);
104
110
  });
105
111
 
106
112
  it("put, delete, patch with wrong actor", async () => {
@@ -339,7 +345,6 @@ export const graffitiCRUDTests = (
339
345
  };
340
346
  const beforePatched = await graffiti.patch(patch, putted, session);
341
347
  expect(beforePatched.value).toEqual(value);
342
- expect(beforePatched.tombstone).toBe(true);
343
348
  expect(beforePatched.lastModified).toBeGreaterThan(putted.lastModified);
344
349
 
345
350
  const gotten = await graffiti.get(putted, {});