]> Lady’s Gitweb - Etiquette/blob - model.js
Enable application of activities onto tag systems
[Etiquette] / model.js
1 // 📧🏷️ Étiquette ∷ model.js
2 // ====================================================================
3 //
4 // Copyright © 2023 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import { identity } from "./deps.js";
11 import { Storage } from "./memory.js";
12 import { taggingDiscoveryContext } from "./names.js";
13 import schema from "./schema.js";
14
15 /**
16 * A tag.
17 *
18 * `Tag`s are not assigned identifiers and do not have side·effects on
19 * other tags in the `TagSystem` until they are persisted with
20 * `::persist`, at which point changes to their relationships are
21 * applied.
22 *
23 * `Tag`s are also not kept up‐to‐date, but persisting an outdated
24 * `Tag` will *not* undo subsequent changes.
25 *
26 * ※ This class is not itself directly exposed, although bound
27 * versions of it are via `TagSystem::Tag`.
28 */
29 class Tag {
30 /** The `TagSystem` this `Tag` belongs to. */
31 #system;
32
33 /** The `Storage` managed by this `Tag`’s `TagSystem`. */
34 #storage;
35
36 /** The schema in use for this `Tag`. */
37 #schema;
38
39 /**
40 * The 30‐bit W·R·M·G base32 identifier with leading checksum which
41 * has been assigned to this `Tag`.
42 *
43 * Will be `null` if this `Tag` has not been persisted. Otherwise,
44 * the format is `cxx-xxxx` (`c` = checksum; `x` = digit).
45 */
46 #identifier = null;
47
48 /** The kind of this `Tag`. */
49 #kind = "Tag";
50
51 /**
52 * The data which was attached to this `Tag` the last time it was
53 * persisted or retrieved from storage.
54 *
55 * Diffing with this will reveal changes.
56 */
57 #persistedData = null;
58
59 /** The current (modified) data associated with this `Tag`. */
60 #data = tagData();
61
62 /**
63 * Adds the provided label(s) to this `Tag` as the provided
64 * predicate, then returns this `Tag`.
65 */
66 #addLabel(predicate, ...labels) {
67 const values = this.#data[predicate];
68 for (const $ of labels) {
69 // Iterate over each provided label and attempt to add it.
70 const literal = langString($);
71 values.add(literal);
72 }
73 return this;
74 }
75
76 /**
77 * Adds the provided tags to the list of tags that this `Tag` is
78 * related to by the provided predicate, then returns this `Tag`.
79 *
80 * Arguments may be string identifiers or objects with an
81 * `.identifier` property.
82 */
83 #addTag(predicate, ...tags) {
84 const storage = this.#storage;
85 const values = this.#data[predicate];
86 for (const $ of tags) {
87 // Iterate over each tag and attempt to state the predicate.
88 const identifier = toIdentifier($);
89 if (identifier == null) {
90 // ☡ The current tag has no identifier.
91 throw new TypeError(
92 `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
93 );
94 } else if (values.has(identifier)) {
95 // Short‐circuit: The identifier has already been stated with
96 // this predicate.
97 /* do nothing */
98 } else {
99 // The current tag has an identifier, but it hasn’t been stated
100 // with this predicate yet.
101 const tag = storage.get(identifier);
102 if (tag == null) {
103 // ☡ The current tag has not been persisted to this `Tag`’s
104 // storage.
105 throw new RangeError(
106 `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
107 );
108 } else if (!this.#isTagInStorage(tag)) {
109 // ☡ The current tag is not a tag in the correct tag system.
110 throw new TypeError(
111 `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
112 );
113 } else if (
114 !isObjectPredicateOK(
115 this.#schema,
116 this.#kind,
117 predicate,
118 tag.#kind,
119 )
120 ) {
121 // ☡ This tag and the current tag form an invalid pair for
122 // this predicate.
123 throw new TypeError(
124 `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
125 );
126 } else {
127 // The current tag is a tag in the correct tag system; add
128 // its identifier.
129 values.add(identifier);
130 }
131 }
132 }
133 return this;
134 }
135
136 /**
137 * Removes the provided string label(s) from this `Tag` as the
138 * provided predicate, then returns this `Tag`.
139 */
140 #deleteLabel(predicate, ...labels) {
141 const values = this.#data[predicate];
142 for (const $ of labels) {
143 // Iterate over each provided label and attempt to remove it.
144 const literal = langString($);
145 values.delete(literal);
146 }
147 return this;
148 }
149
150 /**
151 * Removes the provided tags from the list of tags that this `Tag` is
152 * related to by the provided predicate, then returns this `Tag`.
153 *
154 * Arguments may be string identifiers or objects with an
155 * `.identifier` property.
156 */
157 #deleteTag(predicate, ...tags) {
158 const values = this.#data[predicate];
159 for (const $ of tags) {
160 // Iterate over the provided tags and delete them.
161 values.delete(toIdentifier($));
162 }
163 return this;
164 }
165
166 /**
167 * Returns whether or not the provided value is a tag which shares a
168 * storage with this tag.
169 *
170 * Sharing a storage also implies sharing a `TagSystem`.
171 */
172 #isTagInStorage($) {
173 try {
174 // Try to compare the provided value’s internal store with
175 // the provided storage.
176 return $.#storage == this.#storage;
177 } catch {
178 // The provided value was not a `Tag`.
179 return false;
180 }
181 }
182
183 /**
184 * Yields the labels of this `Tag` according to the provided
185 * predicate.
186 */
187 *#yieldLabels(predicate) {
188 yield* this.#data[predicate];
189 }
190
191 /**
192 * Yields the tags that this `Tag` is related to by the provided
193 * predicate.
194 */
195 *#yieldTags(predicate) {
196 const storage = this.#storage;
197 for (const identifier of this.#data[predicate]) {
198 // Iterate over the tags in this predicate and yield them if
199 // possible.
200 const tag = storage.get(identifier);
201 if (
202 !this.#isTagInStorage(tag) || !isObjectPredicateOK(
203 this.#schema,
204 this.#kind,
205 predicate,
206 tag.#kind,
207 )
208 ) {
209 // The tag no longer appears in storage or is not compatible;
210 // perhaps it was deleted.
211 /* do nothing */
212 } else {
213 // The tag exists and is constructable from storage.
214 yield tag;
215 }
216 }
217 }
218
219 /**
220 * Yields the tags that this `Tag` is related to by the provided
221 * predicate, figured transitively.
222 */
223 *#yieldTransitiveTags(transitivePredicate, basePredicate) {
224 const storage = this.#storage;
225 const encountered = new Set();
226 let pending = new Set(this.#data[basePredicate]);
227 while (pending.size > 0) {
228 // Loop until all tags of the predicate have been encountered.
229 const processing = pending;
230 pending = new Set();
231 for (const identifier of processing) {
232 // Iterate over the tags and yield them if possible.
233 if (!encountered.has(identifier)) {
234 // The tag has not been encountered before.
235 encountered.add(identifier);
236 const tag = storage.get(identifier);
237 if (
238 !this.#isTagInStorage(tag) || !isObjectPredicateOK(
239 this.#schema,
240 this.#kind,
241 transitivePredicate,
242 tag.#kind,
243 )
244 ) {
245 // The tag no longer appears in storage or is not
246 // compatible; perhaps it was deleted.
247 /* do nothing */
248 } else {
249 // The tag exists and is constructable from storage.
250 yield tag;
251 for (const transitive of tag.#data[basePredicate]) {
252 // Iterate over the nested tags of the current tag and
253 // add them to pending as needed.
254 if (!encountered.has(transitive)) {
255 // The nested tag has not been encountered yet.
256 pending.add(transitive);
257 } else {
258 // The nested tag has already been encountered.
259 /* do nothing */
260 }
261 }
262 }
263 } else {
264 // The tag has already been encountered.
265 /* do nothing */
266 }
267 }
268 }
269 }
270
271 /**
272 * Constructs a new `Tag` of the provided kind and with the provided
273 * preferred label.
274 *
275 * ※ The first two arguments of this constructor are bound when
276 * generating the value of `TagSystem::Tag`. It isn’t possible to
277 * access this constructor in its unbound form from outside this
278 * module.
279 *
280 * ☡ This constructor throws if the provided kind is not supported.
281 */
282 constructor(system, storage, schema, kind = "Tag", prefLabel = "") {
283 this.#system = system;
284 this.#storage = storage;
285 this.#schema = schema;
286 const kindString = `${kind}`;
287 if (!(kindString in schema.classes)) {
288 // The provided kind is not supported.
289 throw new RangeError(
290 `Cannot construct Tag: Unrecognized kind: ${kind}.`,
291 );
292 } else {
293 // The provided kind is one of the recognized tag kinds.
294 this.#kind = kindString;
295 this.#data.prefLabel = prefLabel;
296 }
297 }
298
299 /**
300 * Returns a new `Tag` constructor for the provided system, storage,
301 * schema, created with an appropriate prototype for the properties
302 * so defined.
303 *
304 * ※ This function is not exposed.
305 */
306 static For(system, storage, schema) {
307 const {
308 objectProperties,
309 transitiveProperties,
310 dataProperties,
311 } = schema;
312 const constructor = function (...$s) {
313 return Reflect.construct(
314 Tag,
315 [system, storage, schema, ...$s],
316 new.target,
317 );
318 };
319 Object.defineProperties(constructor, {
320 name: { value: "TagSystem::Tag" },
321 prototype: {
322 configurable: false,
323 enumerable: false,
324 value: Object.create(
325 Tag.prototype,
326 Object.fromEntries(Array.from(
327 function* () {
328 for (const key in objectProperties) {
329 // Iterate over each object property and yield any
330 // necessary method definitions.
331 const {
332 inverseOf,
333 subPropertyOf,
334 } = objectProperties[key];
335 if (key in transitiveProperties) {
336 // The current key indicates a transitive property.
337 //
338 // Transitive property methods are added by their
339 // nontransitive subproperties.
340 /* do nothing */
341 } else {
342 // The current key does not indicate a transitive
343 // property.
344 yield [`${key}Tags`, function* () {
345 yield* this.#yieldTags(key);
346 }];
347 if (inverseOf == null) {
348 // The current key does not indicate an inverse
349 // property, so add and delete methods are also
350 // added.
351 const cased = key[0].toUpperCase() +
352 key.substring(1);
353 yield [`add${cased}Tag`, function (...tags) {
354 return this.#addTag(key, ...tags);
355 }];
356 yield [`delete${cased}Tag`, function (...tags) {
357 return this.#deleteTag(key, ...tags);
358 }];
359 } else {
360 // The current key indicates an inverse property,
361 // so no add and delete methods are necessary.
362 /* do nothing */
363 }
364 if (
365 subPropertyOf != null &&
366 subPropertyOf in transitiveProperties
367 ) {
368 // The current key indicates a subproperty of a
369 // transitive property; its method is also added.
370 yield [`${subPropertyOf}Tags`, function* () {
371 yield* this.#yieldTransitiveTags(
372 subPropertyOf,
373 key,
374 );
375 }];
376 } else {
377 // The current key does not indicate a subproperty
378 // of a transitive property.
379 /* do nothing */
380 }
381 }
382 }
383 for (const key in dataProperties) {
384 // Iterate over each data property and yield any
385 // necessary method definitions.
386 if (key != "prefLabel") {
387 // The current key is not `"prefLabel"`.
388 const cased = key[0].toUpperCase() +
389 key.substring(1);
390 yield [`${key}s`, function* () {
391 yield* this.#yieldLabels(key);
392 }];
393 yield [`add${cased}`, function (...labels) {
394 return this.#addLabel(key, ...labels);
395 }];
396 yield [`delete${cased}`, function (...labels) {
397 return this.#deleteLabel(key, ...labels);
398 }];
399 } else {
400 // The current key is `"prefLabel"`. This is a
401 // special case which is not handled by the schema.
402 /* do nothing */
403 }
404 }
405 }(),
406 ([key, value]) => [key, {
407 configurable: true,
408 enumerable: false,
409 value: Object.defineProperty(value, "name", {
410 value: key,
411 }),
412 writable: true,
413 }],
414 )),
415 ),
416 writable: false,
417 },
418 });
419 return new TagConstructor(constructor, system, storage, schema);
420 }
421
422 /**
423 * Assigns the provided data and identifier to the provided tag.
424 *
425 * ☡ This function throws if the provided tag is not a `Tag`.
426 *
427 * ※ This function is not exposed.
428 */
429 static assignData(tag, data, identifier) {
430 tag.#identifier = `${identifier}`;
431 tag.#persistedData = tagData(data);
432 tag.#data = tagData(data);
433 return tag;
434 }
435
436 /**
437 * Returns a new `Tag` with the provided identifier, kind, and
438 * prefLabel.
439 *
440 * ※ This function exists to enable `TagSystem`s to replay Create
441 * activities, maintaining the identifier of the original.
442 *
443 * ☡ This function throws if the provided identifier is already in
444 * use.
445 *
446 * ※ This function is not exposed.
447 */
448 static new(system, identifier, kind = "Tag", prefLabel = "") {
449 const storage = (new system.Tag()).#storage;
450 if (storage.has(identifier)) {
451 throw new RangeError(
452 `Cannot create Tag: Identifier already in use: ${identifier}.`,
453 );
454 } else {
455 const createdTag = new system.Tag(kind, prefLabel);
456 createdTag.#identifier = identifier;
457 createdTag.persist(true);
458 return createdTag;
459 }
460 }
461
462 /**
463 * Returns the `TagSystem` that the provided value belongs to.
464 *
465 * ※ This function can be used to check if the provided value has
466 * private tag features.
467 *
468 * ※ `Tag::system` is an overridable, publicly‐accessible means of
469 * accessing the system.
470 *
471 * ※ This function is not exposed.
472 */
473 static getSystem($) {
474 return !(#system in Object($)) ? null : $.#system;
475 }
476
477 static {
478 // Overwrite the default `::constructor` method to instead give the
479 // actual (bound) constructor which was used to generate a given
480 // `Tag`.
481 Object.defineProperties(this.prototype, {
482 constructor: {
483 configurable: true,
484 enumerable: false,
485 get() {
486 // All `Tag`s are constructed via the `.Tag` constructor
487 // available in their `TagSystem`; return it.
488 return this.#system.Tag;
489 },
490 set: undefined,
491 },
492 });
493 }
494
495 /** Returns the authority (domain) name for this `Tag`. */
496 get authorityName() {
497 return this.#system.authorityName;
498 }
499
500 /** Returns the identifier of this `Tag`. */
501 get identifier() {
502 return this.#identifier;
503 }
504
505 /** Returns the I·R·I for this `Tag`. */
506 get iri() {
507 const { identifier, iriSpace } = this;
508 return identifier == null ? null : `${iriSpace}${identifier}`;
509 }
510
511 /** Returns the I·R·I space for this `Tag`. */
512 get iriSpace() {
513 return this.#system.iriSpace;
514 }
515
516 /** Returns the kind of this `Tag`. */
517 get kind() {
518 return this.#kind;
519 }
520
521 /**
522 * Returns the `TagSystem` for this `Tag`.
523 *
524 * ※ Internally, `Tag.getSystem` is preferred.
525 */
526 get system() {
527 return this.#system;
528 }
529
530 /**
531 * Persist this `Tag` to storage and return an ActivityStreams
532 * serialization of a Tag Activity representing any changes, or
533 * `null` if no changes were made.
534 *
535 * If the second argument is `true`, the `Tag` will be persisted but
536 * no serialization will be made. This is somewhat more efficient.
537 *
538 * ※ Persistence can imply side‐effects on other objects, which are
539 * not noted explicitly in the activity. For example, marking a tag
540 * as broader than another causes the other tag to reciprocally be
541 * marked as narrower.
542 *
543 * ※ Inverse object properties will never appear in the predicates
544 * of generated activities.
545 */
546 persist(silent = false) {
547 const system = this.#system;
548 const storage = this.#storage;
549 const {
550 objectProperties,
551 transitiveProperties,
552 dataProperties,
553 } = this.#schema;
554 const persistedData = this.#persistedData;
555 const data = this.#data;
556 const diffs = {};
557 for (const [key, value] of Object.entries(data)) {
558 // Iterate over each entry of the tag data and create a diff
559 // with the last persisted information.
560 if (
561 objectProperties[key]?.inverseOf != null ||
562 silent && key in dataProperties
563 ) {
564 // The current property is one which is skipped in diffs.
565 //
566 // In a silent persist, this includes any literal terms.
567 /* do nothing */
568 } else {
569 // The current property should be diffed.
570 const persisted = persistedData?.[key] ?? null;
571 if (persisted == null) {
572 // There is no persisted data for the current property yet.
573 diffs[key] = {
574 old: new Set(),
575 new: value instanceof Set
576 ? new Set(value)
577 : new Set([value]),
578 };
579 } else if (value instanceof Set) {
580 // The current property is set‐valued.
581 const oldValues = new Set(persisted);
582 const newValues = new Set(value);
583 for (const existing of persisted) {
584 // Iterate over each persisted property and either remove
585 // it from the list of new values or add it to the list of
586 // removed ones.
587 if (value.has(existing)) {
588 // The value is in both the old and new version of the
589 // data.
590 oldValues.delete(existing);
591 newValues.delete(existing);
592 } else {
593 // The value is not shared.
594 /* do nothing */
595 }
596 }
597 diffs[key] = { old: oldValues, new: newValues };
598 } else if (
599 `${value}` != `${persisted}` ||
600 value.language != persisted.language
601 ) {
602 // The current property is (optionally language‐tagged)
603 // string‐valued and the value changed.
604 diffs[key] = {
605 old: new Set([persisted]),
606 new: new Set([value]),
607 };
608 } else {
609 // The current property did not change.
610 diffs[key] = { old: new Set(), new: new Set() };
611 }
612 }
613 }
614 const identifier = this.#identifier;
615 if (identifier != null) {
616 // This `Tag` has already been persisted; use its existing
617 // identifier and persist.
618 storage.set(identifier, this);
619 } else {
620 // This `Tag` has not been persisted yet; save the new
621 // identifier after persisting.
622 this.#identifier = storage.add(this);
623 }
624 const persistedIdentifier = this.#identifier;
625 this.#persistedData = tagData(data); // cloning here is necessary
626 for (const inverse in objectProperties) {
627 // Iterate over each non‐transitive inverse property and update
628 // it based on its inverse on the corresponding tags if possible.
629 const term = objectProperties[inverse].inverseOf;
630 if (term == null || term in transitiveProperties) {
631 // The current property is not the inverse of an non‐transitive
632 // property.
633 /* do nothing */
634 } else {
635 // The current property is the inverse of a non‐transitive
636 // property.
637 for (const referencedIdentifier of diffs[term].old) {
638 // Iterate over the removed tags and remove this `Tag` from
639 // their inverse property.
640 const referenced = storage.get(referencedIdentifier);
641 try {
642 // Try removing this `Tag`.
643 referenced.#data[inverse].delete(persistedIdentifier);
644 storage.set(referencedIdentifier, referenced);
645 } catch {
646 // Removal failed, possibly because the other tag was
647 // deleted.
648 /* do nothing */
649 }
650 }
651 for (const referencedIdentifier of diffs[term].new) {
652 const referenced = storage.get(referencedIdentifier);
653 try {
654 // Try adding this `Tag`.
655 referenced.#data[inverse].add(persistedIdentifier);
656 storage.set(referencedIdentifier, referenced);
657 } catch {
658 // Adding failed, possibly because the other tag was deleted.
659 /* do nothing */
660 }
661 }
662 }
663 }
664 if (silent) {
665 // This is a silent persist.
666 return undefined;
667 } else {
668 // This is not a silent persist; an activity needs to be
669 // generated if a change was made.
670 const activity = {
671 "@context": taggingDiscoveryContext,
672 "@type": [
673 "TagActivity",
674 identifier == null ? "Create" : "Update",
675 ],
676 context: `${system.iri}`,
677 object: `${this.iri}`,
678 endTime: new Date().toISOString(),
679 ...(() => {
680 const statements = {
681 unstates: [],
682 states: [],
683 };
684 const { unstates, states } = statements;
685 if (identifier == null) {
686 // This is a Create activity.
687 states.push({ predicate: "a", object: `${this.kind}` });
688 } else {
689 // This is an Update activity.
690 /* do nothing */
691 }
692 for (
693 const [term, {
694 old: oldValues,
695 new: newValues,
696 }] of Object.entries(diffs)
697 ) {
698 // Iterate over the diffs of each term and state/unstate
699 // things as needed.
700 for (const oldValue of oldValues) {
701 // Iterate over removals and unstate them.
702 if (term in dataProperties) {
703 // This is a literal term; push it.
704 unstates.push({
705 predicate: term,
706 object: { ...oldValue },
707 });
708 } else {
709 // This is a named term; attempt to get its I·R·I and
710 // push it.
711 try {
712 // Attempt to resolve the value and push the change.
713 const tag = storage.get(oldValue);
714 if (!this.#isTagInStorage(tag)) {
715 // The value did not resolve to a tag in storage.
716 /* do nothing */
717 } else {
718 // The value resolved; push its I·R·I.
719 unstates.push({
720 predicate: term,
721 object: tag.iri,
722 });
723 }
724 } catch {
725 // Value resolution failed for some reason; perhaps the
726 // tag was deleted.
727 /* do nothing */
728 }
729 }
730 }
731 for (const newValue of newValues) {
732 // Iterate over additions and state them.
733 if (term in dataProperties) {
734 // This is a literal term; push it.
735 states.push({
736 predicate: term,
737 object: { ...newValue },
738 });
739 } else {
740 // This is a named term; attempt to get its I·R·I and
741 // push it.
742 try {
743 // Attempt to resolve the value and push the change.
744 const tag = storage.get(newValue);
745 if (!this.#isTagInStorage(tag)) {
746 // The value did not resolve to a tag in storage.
747 /* do nothing */
748 } else {
749 // The value resolved; push its I·R·I.
750 states.push({
751 predicate: term,
752 object: tag.iri,
753 });
754 }
755 } catch {
756 // Value resolution failed for some reason; perhaps the
757 // tag was deleted.
758 /* do nothing */
759 }
760 }
761 }
762 }
763 if (unstates.length == 0) {
764 // Nothing was unstated.
765 delete statements.unstates;
766 } else {
767 // Things were stated.
768 /* do nothing */
769 }
770 if (states.length == 0) {
771 // Nothing was stated.
772 delete statements.states;
773 } else {
774 // Things were stated.
775 /* do nothing */
776 }
777 return statements;
778 })(),
779 };
780 if (
781 !Object.hasOwn(activity, "states") &&
782 !Object.hasOwn(activity, "unstates")
783 ) {
784 // No meaningful changes were actually persisted.
785 return null;
786 } else {
787 // There were meaningful changes persisted regarding this `Tag`.
788 return activity;
789 }
790 }
791 }
792
793 /** Returns the preferred label for this `Tag`. */
794 get prefLabel() {
795 return this.#data.prefLabel;
796 }
797
798 /** Sets the preferred label of this `Tag` to the provided label. */
799 set prefLabel($) {
800 this.#data.prefLabel = langString($);
801 }
802
803 /** Returns the Tag U·R·I for this `Tag`. */
804 get tagURI() {
805 const { identifier } = this;
806 return identifier == null
807 ? null
808 : `tag:${this.taggingEntity}:${identifier}`;
809 }
810
811 /** Returns the tagging entity (domain and date) for this `Tag`. */
812 get taggingEntity() {
813 return this.#system.taggingEntity;
814 }
815
816 /** Returns the string form of the preferred label of this `Tag`. */
817 toString() {
818 return `${this.#data.prefLabel}`;
819 }
820
821 /**
822 * Returns a new object whose enumerable own properties contain the
823 * data from this object needed for storage.
824 *
825 * ※ This method is not really intended for public usage.
826 */
827 [Storage.toObject]() {
828 const data = this.#data;
829 return Object.assign(Object.create(null), {
830 ...data,
831 kind: this.#kind,
832 });
833 }
834 }
835
836 const {
837 /**
838 * A `Tag` constructor function.
839 *
840 * This class extends the identity function, meaning that the object
841 * provided as the constructor is used verbatim (with new private
842 * fields added).
843 *
844 * ※ The instance methods of this class are provided as static
845 * methods on the superclass which all `Tag` constructors inherit
846 * from.
847 *
848 * ※ This class is not exposed.
849 */
850 TagConstructor,
851
852 /**
853 * The exposed constructor function from which all `Tag` constructors
854 * inherit.
855 *
856 * ☡ This constructor always throws.
857 */
858 TagSuper,
859 } = (() => {
860 const tagConstructorBehaviours = Object.create(null);
861 return {
862 TagConstructor: class extends identity {
863 /**
864 * The `TagSystem` used for `Tag`s constructed by this
865 * constructor.
866 */
867 #system;
868
869 /** The `Storage` managed by this constructor’s `TagSystem`. */
870 #storage;
871
872 /** The schema in use for this constructor. */
873 #schema;
874
875 /**
876 * Constructs a new `Tag` constructor by adding the appropriate
877 * private fields to the provided constructor, setting its
878 * prototype, and then returning it.
879 *
880 * ※ This constructor does not modify the `name` or `prototype`
881 * properties of the provided constructor.
882 *
883 * ※ See `Tag.For`, where this constructor is used.
884 */
885 constructor(constructor, system, storage, schema) {
886 super(constructor);
887 Object.setPrototypeOf(this, TagSuper);
888 this.#system = system;
889 this.#storage = storage;
890 this.#schema = schema;
891 }
892
893 static {
894 // Define the superclass constructor which all `Tag`
895 // constructors will inherit from.
896 const superclass = tagConstructorBehaviours.TagSuper =
897 function Tag() {
898 throw new TypeError("Tags must belong to a System.");
899 };
900 const { prototype: staticFeatures } = this;
901 delete staticFeatures.constructor;
902 Object.defineProperty(superclass, "prototype", {
903 configurable: false,
904 enumerable: false,
905 value: Tag.prototype,
906 writable: false,
907 });
908 Object.defineProperties(
909 superclass,
910 Object.getOwnPropertyDescriptors(staticFeatures),
911 );
912 }
913
914 /**
915 * Yields the tags in the `TagSystem` associated with this
916 * constructor.
917 */
918 *all() {
919 const system = this.#system;
920 const storage = this.#storage;
921 for (const instance of storage.values()) {
922 // Iterate over the entries and yield the ones which are
923 // `Tag`s in this `TagSystem`.
924 if (Tag.getSystem(instance) == system) {
925 // The current instance is a `Tag` in this `TagSystem`.
926 yield instance;
927 } else {
928 // The current instance is not a `Tag` in this
929 // `TagSystem`.
930 /* do nothing */
931 }
932 }
933 }
934
935 /**
936 * Returns a new `Tag` resolved from the provided I·R·I.
937 *
938 * ☡ This function throws if the I·R·I is not in the `.iriSpace`
939 * of the `TagSystem` associated with this constructor.
940 *
941 * ※ If the I·R·I is not recognized, this function returns
942 * `undefined`.
943 */
944 fromIRI(iri) {
945 const system = this.#system;
946 const storage = this.#storage;
947 const name = `${iri}`;
948 const prefix = `${system.iriSpace}`;
949 if (!name.startsWith(prefix)) {
950 // The I·R·I does not begin with the expected prefix.
951 throw new RangeError(
952 `I·R·I did not begin with the expected prefix: ${iri}`,
953 );
954 } else {
955 // The I·R·I begins with the expected prefix.
956 const identifier = name.substring(prefix.length);
957 try {
958 // Attempt to resolve the identifier.
959 const instance = storage.get(identifier);
960 return Tag.getSystem(instance) == system
961 ? instance
962 : undefined;
963 } catch {
964 // Do not throw for bad identifiers.
965 return undefined;
966 }
967 }
968 }
969
970 /**
971 * Returns a new `Tag` resolved from the provided identifier.
972 *
973 * ☡ This function throws if the identifier is invalid.
974 *
975 * ※ If the identifier is valid but not recognized, this
976 * function returns `undefined`.
977 */
978 fromIdentifier(identifier) {
979 const system = this.#system;
980 const storage = this.#storage;
981 const instance = storage.get(identifier);
982 return Tag.getSystem(instance) == system
983 ? instance
984 : undefined;
985 }
986
987 /**
988 * Returns a new `Tag` resolved from the provided Tag U·R·I.
989 *
990 * ☡ This function throws if the provided Tag U·R·I does not
991 * match the tagging entity of this constructor’s `TagSystem`.
992 *
993 * ※ If the specific component of the Tag U·R·I is not
994 * recognized, this function returns `undefined`.
995 */
996 fromTagURI(tagURI) {
997 const system = this.#system;
998 const storage = this.#storage;
999 const tagName = `${tagURI}`;
1000 const tagPrefix = `tag:${system.taggingEntity}:`;
1001 if (!tagName.startsWith(tagPrefix)) {
1002 // The Tag U·R·I does not begin with the expected prefix.
1003 throw new RangeError(
1004 `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
1005 );
1006 } else {
1007 // The I·R·I begins with the expected prefix.
1008 const identifier = tagName.substring(tagPrefix.length);
1009 try {
1010 // Attempt to resolve the identifier.
1011 const instance = storage.get(identifier);
1012 return Tag.getSystem(instance) == system
1013 ? instance
1014 : undefined;
1015 } catch {
1016 // Do not throw for bad identifiers.
1017 return undefined;
1018 }
1019 }
1020 }
1021
1022 /**
1023 * Yields the tag identifiers in the `TagSystem` associated with
1024 * this constructor.
1025 */
1026 *identifiers() {
1027 const system = this.#system;
1028 const storage = this.#storage;
1029 for (const [identifier, instance] of storage.entries()) {
1030 // Iterate over the entries and yield the ones which are
1031 // `Tag`s in this `TagSystem`.
1032 if (Tag.getSystem(instance) == system) {
1033 // The current instance is a `Tag` in this `TagSystem`.
1034 yield identifier;
1035 } else {
1036 // The current instance is not a `Tag` in this `TagSystem`.
1037 /* do nothing */
1038 }
1039 }
1040 }
1041
1042 /** Returns the `TagSystem` for this `Tag` constructor. */
1043 get system() {
1044 return this.#system;
1045 }
1046
1047 /**
1048 * Returns a new `Tag` constructed from the provided data and
1049 * with the provided identifier.
1050 *
1051 * ※ This function is not really intended for public usage.
1052 */
1053 [Storage.toInstance](data, identifier) {
1054 const tag = new this(data.kind);
1055 return Tag.assignData(tag, data, identifier);
1056 }
1057 },
1058 TagSuper: tagConstructorBehaviours.TagSuper,
1059 };
1060 })();
1061
1062 const {
1063 /**
1064 * Returns whether the provided schema, subject class, object
1065 * property, and object class are consistent.
1066 *
1067 * This is hardly a full reasoner; it is tuned to the abilites and
1068 * needs of this module.
1069 */
1070 isObjectPredicateOK,
1071 } = (() => {
1072 const cachedClassAndSuperclasses = new WeakMap();
1073 const cachedClassRestrictions = new WeakMap();
1074 const cachedPredicateRestrictions = new WeakMap();
1075
1076 const classAndSuperclasses = function* (
1077 classes,
1078 baseClass,
1079 touched = new Set(),
1080 ) {
1081 if (baseClass == "Thing" || touched.has(baseClass)) {
1082 /* do nothing */
1083 } else {
1084 yield baseClass;
1085 touched.add(baseClass);
1086 const subClassOf = classes[baseClass]?.subClassOf ?? "Thing";
1087 for (
1088 const superclass of (
1089 typeof subClassOf == "string"
1090 ? [subClassOf]
1091 : Array.from(subClassOf)
1092 ).filter(($) => typeof $ == "string")
1093 ) {
1094 yield* classAndSuperclasses(classes, superclass, touched);
1095 }
1096 }
1097 };
1098
1099 const getClassAndSuperclasses = (schema, baseClass) => {
1100 const schemaCache = cachedClassAndSuperclasses.get(schema);
1101 const cached = schemaCache?.[baseClass];
1102 if (cached != null) {
1103 return cached;
1104 } else {
1105 const { classes } = schema;
1106 const result = [...classAndSuperclasses(classes, baseClass)];
1107 if (schemaCache) {
1108 schemaCache[baseClass] = result;
1109 } else {
1110 cachedClassRestrictions.set(
1111 schema,
1112 Object.assign(Object.create(null), { [baseClass]: result }),
1113 );
1114 }
1115 return result;
1116 }
1117 };
1118
1119 const getClassRestrictions = (schema, domain) => {
1120 const schemaCache = cachedClassRestrictions.get(schema);
1121 const cached = schemaCache?.[domain];
1122 if (cached != null) {
1123 return cached;
1124 } else {
1125 const { classes } = schema;
1126 const restrictions = Object.create(null);
1127 const subClassOf = classes[domain]?.subClassOf ?? "Thing";
1128 for (
1129 const superclass of (
1130 typeof subClassOf == "string"
1131 ? [subClassOf]
1132 : Array.from(subClassOf)
1133 ).filter(($) => Object($) === $)
1134 ) {
1135 const { onProperty, allValuesFrom } = superclass;
1136 restrictions[onProperty] = processSpace(allValuesFrom);
1137 }
1138 if (schemaCache) {
1139 schemaCache[domain] = restrictions;
1140 } else {
1141 cachedClassRestrictions.set(
1142 schema,
1143 Object.assign(Object.create(null), {
1144 [domain]: restrictions,
1145 }),
1146 );
1147 }
1148 return restrictions;
1149 }
1150 };
1151
1152 const getPredicateRestrictions = (schema, predicate) => {
1153 const schemaCache = cachedPredicateRestrictions.get(schema);
1154 const cached = schemaCache?.[predicate];
1155 if (cached != null) {
1156 return cached;
1157 } else {
1158 const { objectProperties } = schema;
1159 const restrictions = [
1160 ...predicateRestrictions(objectProperties, predicate),
1161 ].reduce(
1162 (result, { domainIntersection, rangeIntersection }) => {
1163 result.domainIntersection.push(...domainIntersection);
1164 result.rangeIntersection.push(...rangeIntersection);
1165 return result;
1166 },
1167 Object.assign(Object.create(null), {
1168 domainIntersection: [],
1169 rangeIntersection: [],
1170 }),
1171 );
1172 if (schemaCache) {
1173 schemaCache[predicate] = restrictions;
1174 } else {
1175 cachedPredicateRestrictions.set(
1176 schema,
1177 Object.assign(Object.create(null), {
1178 [predicate]: restrictions,
1179 }),
1180 );
1181 }
1182 return restrictions;
1183 }
1184 };
1185
1186 const processSpace = (space) =>
1187 Object(space) === space
1188 ? "length" in space
1189 ? Array.from(
1190 space,
1191 (subspace) =>
1192 Object(subspace) === subspace
1193 ? Array.from(subspace.unionOf)
1194 : [subspace],
1195 )
1196 : [Array.from(space.unionOf)]
1197 : [[space]];
1198
1199 const predicateRestrictions = function* (
1200 objectProperties,
1201 predicate,
1202 touched = new Set(),
1203 ) {
1204 if (predicate == "Property" || touched.has(predicate)) {
1205 /* do nothing */
1206 } else {
1207 const { domain, range, subPropertyOf } =
1208 objectProperties[predicate];
1209 yield Object.assign(Object.create(null), {
1210 domainIntersection: processSpace(domain ?? "Thing"),
1211 rangeIntersection: processSpace(range ?? "Thing"),
1212 });
1213 touched.add(predicate);
1214 for (
1215 const superproperty of (
1216 subPropertyOf == null
1217 ? ["Property"]
1218 : typeof subPropertyOf == "string"
1219 ? [subPropertyOf]
1220 : Array.from(subPropertyOf)
1221 )
1222 ) {
1223 yield* predicateRestrictions(
1224 objectProperties,
1225 superproperty,
1226 touched,
1227 );
1228 }
1229 }
1230 };
1231
1232 return {
1233 isObjectPredicateOK: (
1234 schema,
1235 subjectClass,
1236 predicate,
1237 objectClass,
1238 ) => {
1239 const { objectProperties } = schema;
1240 const predicateDefinition = objectProperties[predicate];
1241 const isInverse = "inverseOf" in predicateDefinition;
1242 const usedPredicate = isInverse
1243 ? predicateDefinition.inverseOf
1244 : predicate;
1245 const domain = isInverse ? objectClass : subjectClass;
1246 const domains = new Set(getClassAndSuperclasses(schema, domain));
1247 const ranges = new Set(getClassAndSuperclasses(
1248 schema,
1249 isInverse ? subjectClass : objectClass,
1250 ));
1251 const predicateRestrictions = getPredicateRestrictions(
1252 schema,
1253 usedPredicate,
1254 );
1255 const { domainIntersection } = predicateRestrictions;
1256 const rangeIntersection = [
1257 ...predicateRestrictions.rangeIntersection,
1258 ...function* () {
1259 for (const domain of domains) {
1260 const classRestrictionOnPredicate =
1261 getClassRestrictions(schema, domain)[usedPredicate];
1262 if (classRestrictionOnPredicate != null) {
1263 yield* classRestrictionOnPredicate;
1264 } else {
1265 /* do nothing */
1266 }
1267 }
1268 }(),
1269 ];
1270 return domainIntersection.every((domainUnion) =>
1271 domainUnion.some((domain) =>
1272 domain == "Thing" || domains.has(domain)
1273 )
1274 ) &&
1275 rangeIntersection.every((rangeUnion) =>
1276 rangeUnion.some((range) =>
1277 range == "Thing" || ranges.has(range)
1278 )
1279 );
1280 },
1281 };
1282 })();
1283
1284 const {
1285 /**
1286 * Returns the provided value converted into a `String` object with
1287 * `.["@value"]` and `.["@language"]` properties.
1288 *
1289 * The same object will be returned for every call with an equivalent
1290 * value.
1291 *
1292 * TODO: Ideally this would be extracted more fully into an R·D·F
1293 * library.
1294 *
1295 * ※ This function is not exposed.
1296 */
1297 langString,
1298 } = (() => {
1299 /**
1300 * Returns the language string object corresponding to the provided
1301 * value and language.
1302 */
1303 const getLangString = (value, language = "") => {
1304 const valueMap = languageMap[language] ??= Object.create(null);
1305 const literal = valueMap[value]?.deref();
1306 if (literal != null) {
1307 // There is already an object corresponding to the provided value
1308 // and language.
1309 return literal;
1310 } else {
1311 // No object already exists corresponding to the provided value
1312 // and language; create one.
1313 const result = Object.preventExtensions(
1314 Object.create(String.prototype, {
1315 "@value": {
1316 enumerable: true,
1317 value,
1318 },
1319 "@language": {
1320 enumerable: !!language,
1321 value: language || null,
1322 },
1323 language: { enumerable: false, get: getLanguage },
1324 toString: { enumerable: false, value: toString },
1325 valueOf: { enumerable: false, value: valueOf },
1326 }),
1327 );
1328 const ref = new WeakRef(result);
1329 langStringRegistry.register(result, { ref, language, value });
1330 valueMap[value] = ref;
1331 return result;
1332 }
1333 };
1334
1335 /** Returns the `.["@language"]` of this object. */
1336 const getLanguage = Object.defineProperty(
1337 function () {
1338 return this["@language"] || null;
1339 },
1340 "name",
1341 { value: "get language" },
1342 );
1343
1344 /**
1345 * A `FinalizationRegistry` for language string objects.
1346 *
1347 * This simply cleans up the corresponding `WeakRef` in the language
1348 * map.
1349 */
1350 const langStringRegistry = new FinalizationRegistry(
1351 ({ ref, language, value }) => {
1352 const valueMap = languageMap[language];
1353 if (valueMap?.[value] === ref) {
1354 delete valueMap[value];
1355 } else {
1356 /* do nothing */
1357 }
1358 },
1359 );
1360
1361 /**
1362 * An object whose own values are an object mapping values to
1363 * language string objects for the language specified by the key.
1364 */
1365 const languageMap = Object.create(null);
1366
1367 /** Returns the `.["@value"]` of this object. */
1368 const toString = function () {
1369 return this["@value"];
1370 };
1371
1372 /**
1373 * Returns this object if it has a `.["@language"]`; otherwise, its
1374 * `.["@value"]`.
1375 */
1376 const valueOf = function () {
1377 return this["@language"] ? this : this["@value"];
1378 };
1379
1380 return {
1381 langString: ($) =>
1382 Object($) === $
1383 ? "@value" in $
1384 ? "@language" in $
1385 ? getLangString(
1386 `${$["@value"]}`,
1387 `${$["@language"] ?? ""}`,
1388 )
1389 : getLangString(`${$["@value"]}`)
1390 : "language" in $
1391 ? getLangString(`${$}`, `${$.language ?? ""}`)
1392 : getLangString(`${$}`)
1393 : getLangString(`${$ ?? ""}`),
1394 };
1395 })();
1396
1397 /**
1398 * Returns a normalized tag data object derived from the provided
1399 * object.
1400 *
1401 * ※ The properties of this function need to match the term names used
1402 * in the ActivityStreams serialization.
1403 *
1404 * ※ This function is not exposed.
1405 */
1406 const tagData = ($) => {
1407 const data = Object($);
1408 const {
1409 // prefLabel intentionally not set here
1410 altLabel,
1411 hiddenLabel,
1412 broader,
1413 narrower,
1414 inCanon,
1415 hasInCanon,
1416 involves,
1417 involvedIn,
1418 } = data;
1419 let prefLabel = langString(data.prefLabel);
1420 return Object.preventExtensions(Object.create(null, {
1421 prefLabel: {
1422 enumerable: true,
1423 get: () => prefLabel,
1424 set: ($) => {
1425 prefLabel = langString($);
1426 },
1427 },
1428 altLabel: {
1429 enumerable: true,
1430 value: new Set(
1431 altLabel != null
1432 ? Array.from(altLabel, langString)
1433 : undefined,
1434 ),
1435 },
1436 hiddenLabel: {
1437 enumerable: true,
1438 value: new Set(
1439 hiddenLabel != null
1440 ? Array.from(hiddenLabel, langString)
1441 : undefined,
1442 ),
1443 },
1444 broader: {
1445 enumerable: true,
1446 value: new Set(
1447 broader != null
1448 ? Array.from(broader, toIdentifier)
1449 : undefined,
1450 ),
1451 },
1452 narrower: {
1453 enumerable: true,
1454 value: new Set(
1455 narrower != null
1456 ? Array.from(narrower, toIdentifier)
1457 : undefined,
1458 ),
1459 },
1460 inCanon: {
1461 enumerable: true,
1462 value: new Set(
1463 inCanon != null
1464 ? Array.from(inCanon, toIdentifier)
1465 : undefined,
1466 ),
1467 },
1468 hasInCanon: {
1469 enumerable: true,
1470 value: new Set(
1471 hasInCanon != null
1472 ? Array.from(hasInCanon, toIdentifier)
1473 : undefined,
1474 ),
1475 },
1476 involves: {
1477 enumerable: true,
1478 value: new Set(
1479 involves != null
1480 ? Array.from(involves, toIdentifier)
1481 : undefined,
1482 ),
1483 },
1484 involvedIn: {
1485 enumerable: true,
1486 value: new Set(
1487 involvedIn != null
1488 ? Array.from(involvedIn, toIdentifier)
1489 : undefined,
1490 ),
1491 },
1492 }));
1493 };
1494
1495 /**
1496 * Returns an identifier corresponding to the provided object.
1497 *
1498 * This is either the value of its `.identifier` or its string value.
1499 *
1500 * ※ This function is not exposed.
1501 */
1502 const toIdentifier = ($) =>
1503 $ == null
1504 ? null
1505 : Object($) === $ && "identifier" in $
1506 ? $.identifier
1507 : `${$}`;
1508
1509 /**
1510 * A tag system, with storage.
1511 *
1512 * The `::Tag` constructor available on any `TagSystem` instance can be
1513 * used to create new `Tag`s within the system.
1514 */
1515 export class TagSystem {
1516 /** The cached bound `Tag` constructor for this `TagSystem`. */
1517 #Tag = null;
1518
1519 /** The domain of this `TagSystem`. */
1520 #domain;
1521
1522 /** The date of this `TagSystem`. */
1523 #date;
1524
1525 /** The identifier of this `TagSystem`. */
1526 #identifier;
1527
1528 /** The schema used by this `TagSystem. */
1529 #schema = schema;
1530
1531 /** The internal `Storage` of this `TagSystem`. */
1532 #storage = new Storage();
1533
1534 /**
1535 * Constructs a new `TagSystem` with the provided domain and date.
1536 *
1537 * Only actual, lowercased domain names are allowed for the domain,
1538 * and the date must be “full” (include month and day components).
1539 * This is for alignment with general best practices for Tag URI’s.
1540 *
1541 * ☡ This constructor throws if provided with an invalid date.
1542 */
1543 constructor(domain, date, identifier = "") {
1544 const domainString = `${domain}`;
1545 const dateString = `${date}`;
1546 this.#identifier = `${identifier}`;
1547 try {
1548 // If the identifier is a valid storage I·D, reserve it.
1549 this.#storage.delete(this.#identifier);
1550 } catch {
1551 // The identifier is not a valid storage I·D, so no worries.
1552 /* do nothing */
1553 }
1554 if (
1555 !/^[a-z](?:[\da-z-]*[\da-z])?(?:\.[a-z](?:[\da-z-]*[\da-z])?)*$/u
1556 .test(domainString)
1557 ) {
1558 // ☡ The domain is invalid.
1559 throw new RangeError(`Invalid domain: ${domain}.`);
1560 } else if (
1561 !/^\d{4}-\d{2}-\d{2}$/u.test(dateString) ||
1562 dateString != new Date(dateString).toISOString().split("T")[0]
1563 ) {
1564 // ☡ The date is invalid.
1565 throw new RangeError(`Invalid date: ${date}.`);
1566 } else {
1567 // The domain and date are 🆗.
1568 this.#domain = domainString;
1569 this.#date = dateString;
1570 }
1571 }
1572
1573 /**
1574 * Returns a bound constructor for constructing `Tags` in this
1575 * `TagSystem`.
1576 */
1577 get Tag() {
1578 if (this.#Tag != null) {
1579 // A bound constructor has already been generated; return it.
1580 return this.#Tag;
1581 } else {
1582 // No bound constructor has been created yet.
1583 const storage = this.#storage;
1584 return this.#Tag = Tag.For(this, storage, this.#schema);
1585 }
1586 }
1587
1588 /**
1589 * Applies the provided activity to this `TagSystem` by replaying its
1590 * statement changes.
1591 *
1592 * ※ This method assumes that the provided activity conforms to the
1593 * assumptions made by this module; i·e that its tags use the same
1594 * identifier format and its activities do not use statements with
1595 * inverse property predicates. It is not intended for the generic
1596 * playback of activities produced by other scripts or mechanisms,
1597 * for which a more sophisticated solution is required.
1598 *
1599 * ☡ This method throws an error if the provided activity cannot be
1600 * processed.
1601 */
1602 apply(activity) {
1603 const { Tag: TagConstructor } = this;
1604 const {
1605 classes,
1606 objectProperties,
1607 transitiveProperties,
1608 dataProperties,
1609 } = this.#schema;
1610 const { object, states, unstates } = activity;
1611 const activityTypes = [].concat(activity["@type"]);
1612 if (!object) {
1613 // ☡ The provided activity has no object.
1614 throw new TypeError(
1615 "Cannot apply activity: Activity lacks an object.",
1616 );
1617 } else {
1618 // The provided activity has an object.
1619 const iri = `${object}`;
1620 const iriSpace = `${this.iriSpace}`;
1621 const identifier = (() => {
1622 // Extract the identifier from the object I·R·I.
1623 if (!iri.startsWith(iriSpace)) {
1624 // ☡ The object of the provided activity is not in the I·R·I
1625 // space of this `TagSystem`.
1626 throw new RangeError(
1627 `Cannot apply activity: Object is not in I·R·I space: ${object}`,
1628 );
1629 } else {
1630 // ☡ The object of the provided activity is in the I·R·I
1631 // space of this `TagSystem`.
1632 return iri.substring(iriSpace.length);
1633 }
1634 })();
1635 const tag = (() => {
1636 // Either resolve the identifier to an existing tag or create
1637 // a new one.
1638 if (activityTypes.includes("Create")) {
1639 // The provided activity is a Create activity.
1640 const kind = states.findLast(
1641 ({ predicate, object }) =>
1642 predicate == "a" && `${object}` in classes,
1643 )?.object;
1644 if (kind == null) {
1645 // ☡ There is no recognized tag class provided for the tag;
1646 // it cannot be created.
1647 throw new RangeError(
1648 `Cannot apply activity: Tag type not recognized.`,
1649 );
1650 } else {
1651 // There is a recognized tag class provided for the tag.
1652 return Tag.new(this, identifier, kind);
1653 }
1654 } else {
1655 // The provided activity is not a Create activity.
1656 return TagConstructor.fromIdentifier(identifier);
1657 }
1658 })();
1659 if (!tag) {
1660 // ☡ Resolving the tag identifier failed.
1661 throw new RangeError(
1662 `Cannot apply activity: No tag for identifier: ${identifier}.`,
1663 );
1664 } else {
1665 // Resolving the identifier succeeded; apply the changes to the
1666 // tag and then silently persist it.
1667 for (
1668 const [statements, mode] of [
1669 [unstates ?? [], "delete"],
1670 [states ?? [], "add"],
1671 ]
1672 ) {
1673 // Delete unstatements, then add statements.
1674 for (const { predicate: $p, object: $o } of statements) {
1675 // Iterate over the statements and apply them.
1676 const predicate = `${$p}`;
1677 const term = predicate in dataProperties
1678 ? langString($o)
1679 : predicate in objectProperties &&
1680 !(predicate in transitiveProperties ||
1681 objectProperties[predicate].inverseOf != null)
1682 ? `${$o}`
1683 : null;
1684 if (term == null) {
1685 // The provided predicate is not recognized; ignore it.
1686 /* do nothing */
1687 } else if (predicate == "prefLabel") {
1688 // Preflabels are handled specially.
1689 if (mode == "delete") {
1690 // Unstating a preflabel has no effect unless a new one
1691 // is also stated.
1692 /* do nothing */
1693 } else {
1694 // Update the preflabel.
1695 tag.prefLabel = term;
1696 }
1697 } else {
1698 // The predicate is not `"prefLabel"`.
1699 const related = (() => {
1700 // If the predicate is an object property, attempt to
1701 // resolve the object.
1702 if (!(predicate in objectProperties)) {
1703 // The predicate is not an object property; return
1704 // null.
1705 return null;
1706 } else {
1707 // The predicate is an object property.
1708 try {
1709 // Attempt to resolve the object.
1710 return TagConstructor.fromIRI(term);
1711 } catch {
1712 // Resolving failed; return undefined.
1713 return undefined;
1714 }
1715 }
1716 })();
1717 if (related === undefined) {
1718 // The predicate is an object property, but its object
1719 // was not resolvable.
1720 //
1721 // ☡ This is a silent error to allow for selective
1722 // replay of activities while ignoring terms which are
1723 // not covered.
1724 /* do nothing */
1725 } else {
1726 // The predicate is not an object property or has a
1727 // resolvable object.
1728 //
1729 // Apply the statement.
1730 tag[
1731 mode.concat(
1732 predicate[0].toUpperCase(),
1733 predicate.substring(1),
1734 predicate in objectProperties ? "Tag" : "",
1735 )
1736 ](related ?? term);
1737 }
1738 }
1739 }
1740 }
1741 tag.persist(true);
1742 return tag;
1743 }
1744 }
1745 }
1746
1747 /** Returns the authority name (domain) for this `TagSystem`. */
1748 get authorityName() {
1749 return this.#domain;
1750 }
1751
1752 /** Returns the date of this `TagSystem`, as a string. */
1753 get date() {
1754 return this.#date;
1755 }
1756
1757 /**
1758 * Yields the entities in this `TagSystem`.
1759 *
1760 * ※ Entities can hypothetically be anything. If you specifically
1761 * want the `Tag`s, use `::Tag.all` instead.
1762 */
1763 *entities() {
1764 yield* this.#storage.values();
1765 }
1766
1767 /**
1768 * Returns the identifier of this `TagSystem`.
1769 *
1770 * ※ Often this is just the empty string.
1771 */
1772 get identifier() {
1773 return this.#identifier;
1774 }
1775
1776 /** Yields the identifiers in use in this `TagSystem`. */
1777 *identifiers() {
1778 yield* this.#storage.keys();
1779 }
1780
1781 /** Returns the I·R·I for this `TagSystem`. */
1782 get iri() {
1783 return `${this.iriSpace}${this.identifier}`;
1784 }
1785
1786 /**
1787 * Returns the prefix used for I·R·I’s of `Tag`s in this `TagSystem`.
1788 */
1789 get iriSpace() {
1790 return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
1791 }
1792
1793 /** Returns the Tag U·R·I for this `TagSystem`. */
1794 get tagURI() {
1795 return `tag:${this.taggingEntity}:${this.identifier}`;
1796 }
1797
1798 /**
1799 * Returns the tagging entity (domain and date) for this `TagSystem`.
1800 */
1801 get taggingEntity() {
1802 return `${this.authorityName},${this.date}`;
1803 }
1804 }
This page took 0.191087 seconds and 5 git commands to generate.