{"version":3,"file":"ResourceLinkQuery.mjs","names":[],"sources":["../../src/linkTraversal/ResourceLinkQuery.ts"],"sourcesContent":["import type { LdoBase, ShapeType } from \"@ldo/ldo\";\nimport type {\n  ExpandDeep,\n  ILinkQuery,\n  LQInput,\n  LQReturn,\n} from \"../types/ILinkQuery\";\nimport type { ConnectedPlugin } from \"../types/ConnectedPlugin\";\nimport type { SubjectNode } from \"@ldo/rdf-utils\";\nimport { exploreLinks } from \"./exploreLinks\";\nimport type { IConnectedLdoDataset } from \"../types/IConnectedLdoDataset\";\nimport type { IConnectedLdoBuilder } from \"../types/IConnectedLdoBuilder\";\nimport { v4 } from \"uuid\";\nimport type { nodeEventListener } from \"@ldo/subscribable-dataset\";\nimport type { Quad } from \"@rdfjs/types\";\n\n/**\n * Represents a query over multiple datasources and constituting muliple\n * resources.\n *\n * @example\n * ```typescript\n * import { ProfileShapeType } from \"./_ldo/Profile.shapeType.ts\";\n *\n * // Create a link query\n * const linkQuery = ldoDataset\n *   .usingType(ProfileShapeType)\n *   .startLinkQuery(\n *     \"http://example.com/profile/card\",\n *     \"http://example.com/profile/card#me\",\n *     {\n *       name: true,\n *         knows: {\n *           name: true,\n *         },\n *       },\n *     }\n *   );\n * // Susbscribe to this link query, automaticically updating the dataset when\n * // something from the link query is changed.\n * await linkQuery.subscribe();\n * ```\n */\nexport class ResourceLinkQuery<\n  Type extends LdoBase,\n  QueryInput extends LQInput<Type>,\n  Plugins extends ConnectedPlugin[],\n> implements ILinkQuery<Type, QueryInput>\n{\n  protected previousTransactionId: string = \"INIT\";\n\n  // Resource Subscriptions uri -> unsubscribeId\n  protected activeResourceSubscriptions: Record<string, string> = {};\n  // Unsubscribe IDs for this ResourceLinkQuery\n  protected thisUnsubscribeIds = new Set<string>();\n\n  protected curOnDataChanged: nodeEventListener<Quad> | undefined;\n\n  protected resourcesWithSubscriptionInProgress: Record<\n    string,\n    Promise<void> | undefined\n  > = {};\n\n  /**\n   * @internal\n   * @param parentDataset The dataset for which this link query is a part\n   * @param shapeType A ShapeType for the link query to follow\n   * @param ldoBuilder An LdoBuilder associated with the dataset\n   * @param startingResource The resource to explore first in the link query\n   * @param startingSubject The starting point of the link query\n   * @param linkQueryInput A definition of the link query\n   */\n  constructor(\n    protected parentDataset: IConnectedLdoDataset<Plugins>,\n    protected shapeType: ShapeType<Type>,\n    protected ldoBuilder: IConnectedLdoBuilder<Type, Plugins>,\n    protected startingResource: Plugins[number][\"types\"][\"resource\"],\n    protected startingSubject: SubjectNode | string,\n    protected linkQueryInput: QueryInput,\n  ) {}\n\n  /**\n   * Runs this link query, returning the result\n   * @param options Options for how to run the link query\n   * @returns A subset of the ShapeType as defined by the LinkQuery\n   *\n   * @example\n   * ```\n   * import { ProfileShapeType } from \"./_ldo/Profile.shapeType.ts\";\n   *\n   * // Create a link query\n   * const linkQuery = ldoDataset\n   *   .usingType(ProfileShapeType)\n   *   .startLinkQuery(\n   *     \"http://example.com/profile/card\",\n   *     \"http://example.com/profile/card#me\",\n   *     {\n   *       name: true,\n   *         knows: {\n   *           name: true,\n   *         },\n   *       },\n   *     }\n   *   );\n   * // Susbscribe to this link query, automaticically updating the dataset when\n   * // something from the link query is changed.\n   * const result = await linkQuery.read();\n   * console.log(result.name);\n   * result.knows.forEach((person) => console.log(person.name));\n   * // The following will type-error. Despite \"phone\" existing on a Profile,\n   * // it was not covered by the link query.\n   * console.log(result.phone);\n   * ```\n   */\n  async run(options?: {\n    reload?: boolean;\n  }): Promise<ExpandDeep<LQReturn<Type, QueryInput>>> {\n    await exploreLinks(\n      this.parentDataset,\n      this.shapeType,\n      this.startingResource,\n      this.startingSubject,\n      this.linkQueryInput,\n      { shouldRefreshResources: options?.reload },\n    );\n    return this.fromSubject();\n  }\n\n  /**\n   * Subscribes to the data defined by the link query, updating the dataset if\n   * any changes are made.\n   * @returns An unsubscribeId\n   *\n   * @example\n   * ```\n   * import { ProfileShapeType } from \"./_ldo/Profile.shapeType.ts\";\n   *\n   * // Create a link query\n   * const linkQuery = ldoDataset\n   *   .usingType(ProfileShapeType)\n   *   .startLinkQuery(\n   *     \"http://example.com/profile/card\",\n   *     \"http://example.com/profile/card#me\",\n   *     {\n   *       name: true,\n   *         knows: {\n   *           name: true,\n   *         },\n   *       },\n   *     }\n   *   );\n   * // Susbscribe to this link query, automaticically updating the dataset when\n   * // something from the link query is changed.\n   * const unsubscribeId = await linkQuery.subscribe();\n   *\n   * // Now, let's imagine the following triple was added to\n   * \"http://example.com/profile/card\":\n   * <http://example.com/profile/card#me> <http://xmlns.com/foaf/0.1/knows> <http://example2.com/profile/card#me>\n   * Because you're subscribed, the dataset will automatically be updated.\n   *\n   * // End subscription\n   * linkQuery.unsubscribe(unsubscribeId);\n   * ```\n   */\n  async subscribe(): Promise<string> {\n    const subscriptionId = v4();\n    this.thisUnsubscribeIds.add(subscriptionId);\n    // If there's already a registered onDataChange, we don't need to make a new\n    // on for this new subscription\n    if (this.curOnDataChanged) {\n      return subscriptionId;\n    }\n    this.curOnDataChanged = async (\n      _changes,\n      transactionId: string,\n      _triggering,\n    ) => {\n      // Set a transaction Id, so that we only trigger one re-render\n      if (transactionId === this.previousTransactionId) return;\n      this.previousTransactionId = transactionId;\n      // Remove previous registration\n      this.parentDataset.removeListenerFromAllEvents(this.curOnDataChanged!);\n\n      // Explore the links, with a subscription to re-explore the links if any\n      // covered information changes\n      const resourcesToUnsubscribeFrom = new Set(\n        Object.keys(this.activeResourceSubscriptions),\n      );\n\n      // Only add the listeners if we're currently subscribed\n      const exploreOptions = this.curOnDataChanged\n        ? {\n            onCoveredDataChanged: this.curOnDataChanged,\n            onResourceEncountered: async (resource) => {\n              // Wait for the the in progress registration to complete. Once it\n              // is complete, you're subscribed, so we can remove this from the\n              // resources to unsubscribe from.\n              if (this.resourcesWithSubscriptionInProgress[resource.uri]) {\n                await this.resourcesWithSubscriptionInProgress[resource.uri];\n                resourcesToUnsubscribeFrom.delete(resource.uri);\n                return;\n              }\n              // No need to do anything if we're already subscribed\n              if (resourcesToUnsubscribeFrom.has(resource.uri)) {\n                resourcesToUnsubscribeFrom.delete(resource.uri);\n                return;\n              }\n              // Otherwise begin the subscription\n              let resolve;\n              this.resourcesWithSubscriptionInProgress[resource.uri] =\n                new Promise<void>((res) => {\n                  resolve = res;\n                });\n              const unsubscribeId = await resource.subscribeToNotifications();\n              this.activeResourceSubscriptions[resource.uri] = unsubscribeId;\n              // Unsubscribe in case unsubscribe call came in mid subscription\n              if (!this.curOnDataChanged) {\n                await this.unsubscribeFromResource(resource.uri);\n              }\n              resolve();\n              this.resourcesWithSubscriptionInProgress[resource.uri] =\n                undefined;\n            },\n          }\n        : {};\n      await exploreLinks(\n        this.parentDataset,\n        this.shapeType,\n        this.startingResource,\n        this.startingSubject,\n        this.linkQueryInput,\n        exploreOptions,\n      );\n      // Clean up unused subscriptions\n      await Promise.all(\n        Array.from(resourcesToUnsubscribeFrom).map(async (uri) =>\n          this.unsubscribeFromResource(uri),\n        ),\n      );\n    };\n    await this.curOnDataChanged({}, \"BEGIN_SUB\", [null, null, null, null]);\n    return subscriptionId;\n  }\n\n  private async unsubscribeFromResource(uri) {\n    const resource = this.parentDataset.getResource(uri);\n    const unsubscribeId = this.activeResourceSubscriptions[uri];\n    delete this.activeResourceSubscriptions[uri];\n    await resource.unsubscribeFromNotifications(unsubscribeId);\n  }\n\n  private async fullUnsubscribe(): Promise<void> {\n    if (this.curOnDataChanged) {\n      this.parentDataset.removeListenerFromAllEvents(this.curOnDataChanged);\n      this.curOnDataChanged = undefined;\n    }\n    await Promise.all(\n      Object.keys(this.activeResourceSubscriptions).map(async (uri) => {\n        this.unsubscribeFromResource(uri);\n      }),\n    );\n  }\n\n  async unsubscribe(unsubscribeId: string): Promise<void> {\n    this.thisUnsubscribeIds.delete(unsubscribeId);\n    if (this.thisUnsubscribeIds.size === 0) {\n      await this.fullUnsubscribe();\n    }\n  }\n\n  async unsubscribeAll() {\n    this.thisUnsubscribeIds.clear();\n    await this.fullUnsubscribe();\n  }\n\n  fromSubject(): ExpandDeep<LQReturn<Type, QueryInput>> {\n    return this.ldoBuilder.fromSubject(\n      this.startingSubject,\n    ) as unknown as ExpandDeep<LQReturn<Type, QueryInput>>;\n  }\n\n  getSubscribedResources(): Plugins[number][\"types\"][\"resource\"][] {\n    return Object.keys(this.activeResourceSubscriptions).map((uri) =>\n      this.parentDataset.getResource(uri),\n    );\n  }\n}\n"],"mappings":"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"}