/**
 * The synchronizer-ws-server-durable-object module of the TinyBase project lets
 * you create a server that facilitates synchronization between clients, running
 * as a Cloudflare Durable Object.
 * @see Cloudflare Durable Objects guide
 * @see Synchronization guide
 * @see Todo App v6 (collaboration) demo
 * @packageDocumentation
 * @module synchronizer-ws-server-durable-object
 * @since v5.4.0
 */
import type {
  Persister,
  Persists,
} from '../../../persisters/with-schemas/index.d.ts';
import type {
  Id,
  IdAddedOrRemoved,
  Ids,
  OptionalSchemas,
} from '../../../with-schemas/index.d.ts';
import {DurableObject} from 'cloudflare:workers';

/**
 * A WsServerDurableObject is the server component (running as a Cloudflare
 * Durable Object) for synchronization between clients that are using
 * WsSynchronizer instances.
 *
 * The WsServerDurableObject is an overridden implementation of the
 * DurableObject class, so you can have access to its members as well as the
 * TinyBase-specific methods. If you are using the storage for other data, you
 * may want to configure a `prefix` parameter to ensure you don't accidentally
 * collide with TinyBase data.
 *
 * Always remember to call the `super` implementations of the methods that
 * TinyBase uses (the constructor, `fetch`, `webSocketMessage`, and
 * `webSocketClose`) if you further override them.
 * @category Creation
 * @since v5.4.0
 */
export class WsServerDurableObject<
  Schemas extends OptionalSchemas,
  Env = unknown,
> extends DurableObject<Env> {
  /**
   * The constructor is used to create the Durable Object that will synchronize
   * the TinyBase clients.
   *
   * For basic TinyBase synchronization and persistence, you don't need to
   * override this method, but if you do, ensure you call the `super`
   * constructor
   * with the two parameters.
   * @param ctx The DurableObjectState context.
   * @param env The DurableObjectState environment.
   * @returns A new instance of the WsServerDurableObject.
   * @category Creation
   * @since v5.4.0
   */
  constructor(ctx: DurableObjectState, env: Env);

  /**
   * The createPersister method is used to return a persister for the Durable
   * Object to preserve Store data when clients are not connected.
   *
   * This has schema-based typing. The following is a simplified representation:
   *
   * ```ts override
   * createPersister():
   *   | Persister<Persists.MergeableStoreOnly>
   *   | Promise<Persister<Persists.MergeableStoreOnly>>
   *   | undefined;
   * ```
   *
   * In other words, override this method to enable persistence of the Store
   * data that the Durable Object is synchronizing between clients.
   *
   * This should almost certainly return a DurableObjectStoragePersister,
   * created with the createDurableObjectStoragePersister function. This will
   * ensure that the Store is serialized to the Durable Object KV-based storage.
   *
   * Returning `undefined` from this method will disable persistence.
   * @example
   * This example enables Durable Object persistence by creating a Persister
   * object within the createPersister method of a WsServerDurableObject.
   *
   * ```js yolo
   * import {createMergeableStore} from 'tinybase';
   * import {createDurableObjectStoragePersister} from 'tinybase/persisters/persister-durable-object-storage';
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   createPersister() {
   *     const store = createMergeableStore();
   *     const persister = createDurableObjectStoragePersister(
   *       store,
   *       this.ctx.storage,
   *     );
   *     return persister;
   *   }
   * }
   * ```
   * @returns A new instance of a DurableObjectStoragePersister (or a promise to
   * resolve one) that will be used to persist data to the Durable Object.
   * Return `undefined` if that functionality is not required.
   * @category Creation
   * @since v5.4.0
   */
  createPersister():
    | Persister<Schemas, Persists.MergeableStoreOnly>
    | Promise<Persister<Schemas, Persists.MergeableStoreOnly>>
    | undefined;

  /**
   * The getPathId method is used to get the Id of the path that is being
   * served.
   *
   * This is useful for when you want to know which path the current Durable
   * Object is serving - for the purposes of logging, for example.
   * @returns The Id of the path being served by the Durable Object.
   * @example
   * This example logs the path being served by the Durable Object every time a
   * synchronization method is handled.
   *
   * ```js yolo
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   onMessage() {
   *     console.info('Message received on path: ', this.getPathId());
   *   }
   * }
   * ```
   * @category Getter
   * @since v5.4.0
   */
  getPathId(): Id;

  /**
   * The getClientIds method is used to access a list of all the connected
   * clients on the path.
   *
   * Note that if you call this method from within the onClientId method as a
   * client is getting removed, it will still be returned in the list of client
   * Ids.
   * @returns The Ids of the clients being served by the Durable Object.
   * @example
   * This example logs the list of clients being served by the Durable Object
   * every time a synchronization method is handled.
   *
   * ```js yolo
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   onMessage() {
   *     console.info('Clients on path: ', this.getClientIds());
   *   }
   * }
   * ```
   * @category Getter
   * @since v5.4.0
   */
  getClientIds(): Ids;

  /**
   * The onPathId method is called when the first client connects to, or the
   * last client disconnects from, the server with a given path Id.
   *
   * This method is called with the path Id and an IdAddedOrRemoved flag
   * indicating whether it this is being triggered by the first client joining
   * (`1`) or the last client leaving (`-1`).
   * @param pathId The Id of the path being served by the Durable Object.
   * @param addedOrRemoved Whether the path had the first joiner, or the last
   * leaver.
   * @example
   * This example logs the Id of the path being served by the Durable Object
   * when the first client joins (the path Id is 'added'), and when the last
   * client leaves (the path Id is 'removed').
   *
   * ```js yolo
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   onPathId(pathId, addedOrRemoved) {
   *     console.info(
   *       (addedOrRemoved == 1 ? 'Added' : 'Removed') + ` path ${pathId}`,
   *     );
   *   }
   * }
   * ```
   * @category Event
   * @since v5.4.0
   */
  onPathId(pathId: Id, addedOrRemoved: IdAddedOrRemoved): void;

  /**
   * The onClientId method is called when a client connects to, or disconnects
   * from, the server.
   *
   * This method is called with the path Id, the client Id, and an
   * IdAddedOrRemoved flag indicating whether it this is being triggered by
   * the client joining (`1`) or the client leaving (`-1`).
   *
   * Note that if you call the getClientIds method from within this method as a
   * client is getting removed, it will still be returned in the list of client
   * Ids.
   * @param pathId The Id of the path being served by the Durable Object.
   * @param clientId The Id of the client joining or leaving.
   * @param addedOrRemoved Whether the client is joining or leaving.
   * @example
   * This example logs every client that joins (the client Id is 'added') or
   * leaves (the client Id is 'removed') on the path being served by the Durable
   * Object.
   *
   * ```js yolo
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   onClientId(pathId, clientId, addedOrRemoved) {
   *     console.info(
   *       (addedOrRemoved == 1 ? 'Added' : 'Removed') +
   *         ` client ${clientId} on path ${pathId}`,
   *     );
   *   }
   * }
   * ```
   * @category Event
   * @since v5.4.0
   */
  onClientId(pathId: Id, clientId: Id, addedOrRemoved: IdAddedOrRemoved): void;

  /**
   * The onMessage method is called when a message is handled by the server.
   *
   * This is useful if you want to debug the synchronization process, though be
   * aware that this method is called very frequently. It is called with the Id
   * of the client the message came _from_, the the Id of the client the message
   * is to be forwarded _to_, and the remainder of the message itself.
   *
   * Since this method is called often, it should be performant. The path Id is
   * not passed as an argument, since it has a small cost to provide by default.
   * You can use the getPathId method yourself if that information is needed.
   * @param fromClientId The Id of the client that send the message.
   * @param toClientId The Id of the client to receive the message (or empty for
   * a broadcast).
   * @param remainder The remainder of the body of the message.
   * @example
   * This example logs every message routed by the Durable Object between
   * clients.
   *
   * ```js yolo
   * import {WsServerDurableObject} from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
   *
   * export class MyDurableObject extends WsServerDurableObject {
   *   onMessage(fromClientId, toClientId, remainder) {
   *     console.info(
   *       `Message from '${fromClientId}' to '${toClientId}': ${remainder}`,
   *     );
   *   }
   * }
   * ```
   * @category Event
   * @since v5.4.0
   */
  onMessage(fromClientId: Id, toClientId: Id, remainder: string): void;
}

/**
 * The getWsServerDurableObjectFetch function returns a convenient handler for a
 * Cloudflare worker to route requests to the fetch handler of a
 * WsServerDurableObject for the given namespace.
 *
 * This has schema-based typing. The following is a simplified representation:
 *
 * ```ts override
 * getWsServerDurableObjectFetch<Namespace extends string>(
 *   namespace: Namespace,
 * ): (
 *   request: Request,
 *   env: {
 *     [namespace in Namespace]: DurableObjectNamespace<WsServerDurableObject>;
 *   },
 * ) => Response;
 * ```
 *
 * The implementation of the function that this returns requires the request to
 * be a WebSocket 'Upgrade' request, and for the client to have provided a
 * `sec-websocket-key` header that the server can use as a unique key for the
 * client.
 *
 * It then takes the path of the HTTP request and routes the upgrade request to
 * a Durable Object (in the given namespace) for that path. From then on, the
 * Durable Object handles all the WebSocket communication.
 *
 * Note that you'll need to have a Wrangler configuration that connects your
 * Durable Object class to the namespace. In other words, you'll have something
 * like this in your `wrangler.toml` file.
 *
 * ```toml
 * [[durable_objects.bindings]]
 * name = "MyDurableObjects"
 * class_name = "MyDurableObject"
 * ```
 *
 * Note that it is not required to use this handler to route TinyBase client
 * requests in your Cloudflare app. If you have your own custom routing logic,
 * path scheme, or authentication, for example, you can easily implement that in
 * the worker's fetch method yourself. See the [Durable Objects
 * documentation](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoking-the-fetch-handler)
 * for examples.
 *
 * You can also pass a newly created request to the Durable Object's `fetch`
 * method. For example, you can overwrite the 'path' that the Durable Object
 * thinks it is serving, perhaps to inject a unique authenticated user Id that
 * wasn't actually provided by the client WebSocket.
 * @param namespace A string for the namespace of the Durable Objects that you
 * want this worker to route requests to.
 * @returns A fetch handler that routes WebSocket upgrade requests to a Durable
 * Object.
 * @example
 * This example sets up default routing of the WebSocket upgrade request to a
 * Durable Object in the `MyDurableObjects` namespace. This would require the
 * `wrangler.toml` configuration shown above.
 *
 * ```js yolo
 * import {
 *   WsServerDurableObject,
 *   getWsServerDurableObjectFetch,
 * } from 'tinybase/synchronizers/synchronizer-ws-server-durable-object';
 *
 * export class MyDurableObject extends WsServerDurableObject {}
 *
 * export default {fetch: getWsServerDurableObjectFetch('MyDurableObjects')};
 * ```
 * @category Creation
 * @since v5.4.0
 */
export function getWsServerDurableObjectFetch<
  Schemas extends OptionalSchemas,
  Namespace extends string,
>(
  namespace: Namespace,
): (
  request: Request,
  env: {
    [namespace in Namespace]: DurableObjectNamespace<
      WsServerDurableObject<Schemas>
    >;
  },
) => Response;
