All files / json-crdt-repo/src/session EditSessionFactory.ts

100% Statements 52/52
85% Branches 34/40
71.42% Functions 5/7
100% Lines 48/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 1789x   9x 9x             9x 87x               80x 80x 80x 80x 80x 51x 51x 51x     80x 29x         80x 80x                       51x 51x 51x 51x 32x 32x 32x 2x     2x 2x       32x     19x     19x 19x 19x 13x 13x 13x     5x 4x 4x 4x     9x     9x   2x 1x     6x 5x     1x         12x   2x                                                                                                                                                    
import {Model, type NodeBuilder} from 'json-joy/lib/json-crdt';
import type {BlockId, LocalRepo} from '../local/types';
import {EditSession} from './EditSession';
import {timeout} from 'thingies/lib/timeout';
 
export interface EditSessionFactoryOpts {
  readonly sid: number;
  readonly repo: LocalRepo;
}
 
export class EditSessionFactory {
  constructor(protected readonly opts: EditSessionFactoryOpts) {}
 
  /**
   * Creates a new editing session synchronously (immediately). If the block
   * with a given ID already exists, it asynchronously synchronizes the local
   * and remote state.
   */
  public make(opts: EditSessionMakeOpts): {session: EditSession; sync?: Promise<void>} {
    const {id, schema, pull = true} = opts;
    const factoryOpts = this.opts;
    const model = Model.create(void 0, factoryOpts.sid);
    const session = new EditSession(factoryOpts.repo, id, model, undefined, opts.session);
    if (schema) {
      const sessionModel = session.model;
      sessionModel.setSchema(schema);
      sessionModel.api.flush();
    }
    let sync: Promise<void> | undefined;
    if (pull && !session.log.patches.size()) {
      sync = session
        .sync()
        .then(() => {})
        .catch(() => {});
    }
    session.log.end.api.autoFlush();
    return {session, sync};
  }
 
  /**
   * Load block from the local repo. Creates a new editing session
   * asynchronously from an existing local block.
   *
   * It is also possible to block on remote state check in case the block does
   * not exist locally. When `pull` is set, it will also refresh the latest
   * state from the remote in the background after returning local state.
   */
  public async load(opts: EditSessionLoadOpts): Promise<EditSession> {
    const id = opts.id;
    const repo = this.opts.repo;
    try {
      const {model, cursor} = await repo.get({id});
      const session = new EditSession(repo, id, model, cursor, opts.session);
      session.log.end.api.autoFlush();
      if (opts.pull) {
        void repo
          .pull(id)
          .then(async ({cursor}) => {
            session.cursor = cursor;
            await session.load();
          })
          .catch(() => {});
      }
      return session;
    } catch (error) {
      const errorCode =
        !!error && typeof error === 'object'
          ? (error as Record<string, unknown>).code || (error as Record<string, unknown>).message || ''
          : '';
      Eif (errorCode === 'NOT_FOUND') {
        const remote = opts.remote;
        if (remote) {
          const timeoutMs = remote.timeout;
          try {
            const {model, cursor} = await (typeof timeoutMs === 'number'
              ? timeout(timeoutMs, repo.pull(id))
              : repo.pull(id));
            if (remote.throwIf === 'exists') throw new Error('EXISTS');
            const session = new EditSession(repo, id, model, cursor, opts.session);
            session.log.end.api.autoFlush();
            return session;
          } catch (error) {
            const errorCode =
              !!error && typeof error === 'object'
                ? (error as Record<string, unknown>).code || (error as Record<string, unknown>).message || ''
                : '';
            switch (errorCode) {
              case 'TIMEOUT': {
                if (!opts.make) throw error;
                break;
              }
              case 'NOT_FOUND': {
                if (remote.throwIf === 'missing') throw error;
                break;
              }
              default: {
                throw error;
              }
            }
          }
        }
        if (opts.make) return this.make({session: opts.session, ...opts.make, id}).session;
      }
      throw error;
    }
  }
}
 
/**
 * Constructs a new editing session synchronously.
 */
export interface EditSessionMakeOpts {
  /** Block ID. */
  id: BlockId;
 
  /** The new block schema, if any. */
  schema?: NodeBuilder;
 
  /**
   * Whether to asynchronously pull for any existing local block state, if a
   * block with the same ID already exists. Defaults to `true`.
   */
  pull?: boolean;
 
  /**
   * Internal unique session ID.
   */
  session?: number;
}
 
/**
 * Constructs and editing session asynchronously from an existing block. In
 * case the block does not exist, it is possible to create one or throw an
 * error.
 */
export interface EditSessionLoadOpts {
  /** Block ID. */
  id: BlockId;
 
  /**
   * If specified, will create a new block, if one does not already exist. Will
   * use these `make` options and provide them to the `make()` call.
   */
  make?: Omit<EditSessionMakeOpts, 'id'>;
 
  // /** The new block schema, if any. */
  // schema?: NodeBuilder;
 
  /**
   * Internal unique session ID.
   */
  session?: number;
 
  /**
   * Whether to refresh the latest state from the remote in the background
   * after loading an existing local block.
   */
  pull?: boolean;
 
  remote?: {
    /**
     * Time in milliseconds to wait for the remote to respond. If the remote
     * does not respond in time, the call will proceed with the local state.
     *
     * If upsert `make` option is not provided, the call will throw a "TIMEOUT"
     * error.
     */
    timeout?: number;
 
    /**
     * Defaults to an empty string. Otherwise, if "missing", will throw a
     * "NOT_FOUND" error if the block does not exist remotely. If "exists", will
     * a "CONFLICT" error if the block exists remotely.
     */
    throwIf?: '' | 'missing' | 'exists';
  };
}