Skip to content
中文

Document candidate compilation, source checks, and CAS publication ​

The policy implementation follows.

ts
export function defineKnowledgePublication<App>(
  options: KnowledgePublicationOptions<App>,
): KnowledgePublication<App> {
  const load = options.store.load.bind(options.store),
    swap = options.store.compareAndSwap.bind(options.store);
  const authorize = options.authorize.bind(options),
    verify = options.verifySources.bind(options),
    now = options.now?.bind(options) ?? Date.now;
  // Defaults keep one record small: at most 32 pending candidates, each valid for seven days.
  const maxCandidates = options.maxCandidates ?? 32,
    retention = options.candidateRetentionMs ?? 7 * 86400_000;
  const maxChars = options.maxReadChars,
    reads = structuredClone(options.requiredReads ?? []);
  breakWhile(
    !Number.isSafeInteger(maxCandidates) ||
      maxCandidates < 1 ||
      !Number.isSafeInteger(retention) ||
      retention < 1,
    'Invalid publication bounds',
  );
  const compile = (documents: readonly KnowledgeDocument[]) => {
    const catalog = defineKnowledgeCatalog(documents, { maxChars });
    for (const read of reads) catalog.read(read);
    return catalog;
  };
  // Keep only the last successfully compiled record. Include its full document
  // content so a broken store reusing a revision cannot bypass validation.
  let compiledRecord: string | undefined;
  const sourceSnapshot = (
    sources: readonly KnowledgeSourceReference[],
  ): KnowledgeSourceReference[] => {
    const invalid = 'Invalid or expired knowledge provenance';
    breakWhile(!Array.isArray(sources) || !sources.length || sources.length > 32, invalid);
    const snapshot = Array.from(sources, (source) => ({
      id: source?.id,
      revision: source?.revision,
      expiresAtMs: source?.expiresAtMs,
    }));
    breakWhile(
      snapshot.some(
        (source) =>
          typeof source.id !== 'string' ||
          !source.id.trim() ||
          typeof source.revision !== 'string' ||
          !source.revision.trim() ||
          (source.expiresAtMs !== undefined &&
            (!Number.isFinite(source.expiresAtMs) || source.expiresAtMs <= now())),
      ),
      invalid,
    );
    return snapshot;
  };
  const read = async (context: OperationContext<App>): Promise<KnowledgePublicationRecord> => {
    context.signal.throwIfAborted();
    const record = structuredClone(await awaitWithSignal(load(), context.signal));
    breakWhile(
      !record ||
        !Number.isSafeInteger(record.revision) ||
        record.revision < 0 ||
        !Number.isSafeInteger(record.publishedRevision) ||
        record.publishedRevision < 0 ||
        record.publishedRevision > record.revision ||
        !Array.isArray(record.documents) ||
        !Array.isArray(record.candidates) ||
        record.candidates.length > maxCandidates,
      'Invalid publication record',
    );
    breakWhile(
      new Set(record.candidates.map((item) => item?.id)).size !== record.candidates.length ||
        record.candidates.some(
          (item) =>
            !item ||
            typeof item.id !== 'string' ||
            !item.id ||
            !Number.isSafeInteger(item.baseRevision) ||
            item.baseRevision < 0 ||
            item.baseRevision > record.publishedRevision ||
            !Number.isFinite(item.createdAtMs) ||
            !Number.isFinite(item.expiresAtMs) ||
            item.expiresAtMs <= item.createdAtMs ||
            !Array.isArray(item.sources) ||
            !item.sources.length ||
            item.sources.length > 32,
        ),
      'Invalid knowledge candidate',
    );
    const compilationKey = JSON.stringify([
      record.revision,
      record.publishedRevision,
      record.documents,
      record.candidates.map((item) => item.document),
    ]);
    if (compilationKey !== compiledRecord) {
      for (const item of record.candidates)
        compile([...record.documents.filter((doc) => doc.id !== item.document.id), item.document]);
      compile(record.documents);
      compiledRecord = compilationKey;
    }
    context.signal.throwIfAborted();
    return record;
  };
  const commit = async (
    record: KnowledgePublicationRecord,
    next: KnowledgePublicationRecord,
    context: OperationContext<App>,
    assertCurrent?: () => void,
  ) => {
    context.signal.throwIfAborted();
    const snapshot = structuredClone(next);
    assertCurrent?.();
    context.signal.throwIfAborted();
    const saved = await swap(record.revision, snapshot);
    breakWhile(saved !== true, 'Knowledge publication revision conflict');
    context.signal.throwIfAborted();
    return structuredClone(next);
  };
  const checked = async (revision: number, context: OperationContext<App>) => {
    const record = await read(context);
    breakWhile(record.revision !== revision, 'Knowledge publication revision conflict');
    return record;
  };
  const replacement = (record: KnowledgePublicationRecord, document: KnowledgeDocument) => {
    const previous = record.documents.find((item) => item.id === document.id);
    breakWhile(
      !!previous &&
        previous.revision === document.revision &&
        JSON.stringify(previous) !== JSON.stringify(document),
      'Changed knowledge needs a new document revision',
    );
    const documents = [
      ...record.documents.filter((item) => item.id !== document.id),
      structuredClone(document),
    ];
    compile(documents);
    return documents;
  };
  const candidate = (record: KnowledgePublicationRecord, id: string): KnowledgeCandidate => {
    const item = record.candidates.find((value) => value.id === id);
    breakWhile(!item || item.expiresAtMs <= now(), 'Knowledge candidate unavailable');
    return item!;
  };
  return Object.freeze<KnowledgePublication<App>>({
    read,
    async propose(request, context) {
      context.signal.throwIfAborted();
      const document = structuredClone(request.document),
        revision = request.revision,
        sources = sourceSnapshot(request.sources);
      await awaitAssertion(
        () => authorize('propose', context),
        context.signal,
        'Knowledge publication authorization must return void',
      );
      const record = await checked(revision, context);
      sourceSnapshot(sources);
      replacement(record, document);
      await awaitAssertion(
        () => verify(structuredClone(sources), context),
        context.signal,
        'Knowledge provenance validation must return void',
      );
      sourceSnapshot(sources);
      const candidates = record.candidates.filter((item) => item.expiresAtMs > now());
      breakWhile(candidates.length >= maxCandidates, 'Knowledge candidate capacity exhausted');
      const item: KnowledgeCandidate = {
        id: invocationId(),
        baseRevision: record.publishedRevision,
        document,
        sources,
        createdAtMs: now(),
        expiresAtMs: Math.min(
          now() + retention,
          ...sources.map((source) => source.expiresAtMs ?? Infinity),
        ),
      };
      return commit(
        record,
        { ...record, revision: record.revision + 1, candidates: [...candidates, item] },
        context,
        () => {
          sourceSnapshot(sources);
          breakWhile(item.expiresAtMs <= now(), 'Knowledge candidate expired');
        },
      );
    },
    async publish(request, context) {
      const { revision, candidateId } = request;
      await awaitAssertion(
        () => authorize('publish', context),
        context.signal,
        'Knowledge publication authorization must return void',
      );
      const record = await checked(revision, context),
        item = candidate(record, candidateId);
      breakWhile(
        item.baseRevision !== record.publishedRevision,
        'Knowledge candidate base changed; prepare it again',
      );
      const sources = sourceSnapshot(item.sources);
      await awaitAssertion(
        () => verify(structuredClone(sources), context),
        context.signal,
        'Knowledge provenance validation must return void',
      );
      sourceSnapshot(sources);
      const documents = replacement(record, item.document);
      return commit(
        record,
        {
          ...record,
          revision: record.revision + 1,
          publishedRevision: record.publishedRevision + 1,
          documents,
          candidates: record.candidates.filter((value) => value.id !== item.id),
        },
        context,
        () => {
          candidate(record, item.id);
          sourceSnapshot(sources);
        },
      );
    },
    async reject(request, context) {
      const { revision, candidateId } = request;
      await awaitAssertion(
        () => authorize('reject', context),
        context.signal,
        'Knowledge publication authorization must return void',
      );
      const record = await checked(revision, context);
      candidate(record, candidateId);
      return commit(
        record,
        {
          ...record,
          revision: record.revision + 1,
          candidates: record.candidates.filter((item) => item.id !== candidateId),
        },
        context,
      );
    },
  });
}

API reference, usage examples and developer guides.