1 // 📧🏷️ Étiquette ∷ model.js
2 // ====================================================================
4 // Copyright © 2023 Lady [@ Lady’s Computer].
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/>.
10 import { identity
} from "./deps.js";
11 import { Storage
} from "./memory.js";
12 import { taggingDiscoveryContext
} from "./names.js";
13 import schema
from "./schema.js";
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
23 * `Tag`s are also not kept up‐to‐date, but persisting an outdated
24 * `Tag` will *not* undo subsequent changes.
26 * ※ This class is not itself directly exposed, although bound
27 * versions of it are via `TagSystem::Tag`.
30 /** The `TagSystem` this `Tag` belongs to. */
33 /** The `Storage` managed by this `Tag`’s `TagSystem`. */
36 /** The schema in use for this `Tag`. */
40 * The 30‐bit W·R·M·G base32 identifier with leading checksum which
41 * has been assigned to this `Tag`.
43 * Will be `null` if this `Tag` has not been persisted. Otherwise,
44 * the format is `cxx-xxxx` (`c` = checksum; `x` = digit).
48 /** The kind of this `Tag`. */
52 * The data which was attached to this `Tag` the last time it was
53 * persisted or retrieved from storage.
55 * Diffing with this will reveal changes.
57 #persistedData
= null;
59 /** The current (modified) data associated with this `Tag`. */
63 * Adds the provided label(s) to this `Tag` as the provided
64 * predicate, then returns this `Tag`.
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($);
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`.
80 * Arguments may be string identifiers or objects with an
81 * `.identifier` property.
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.
92 `Cannot state ${predicate} of Tag: Identifier must not be nullish.`,
94 } else if (values
.has(identifier
)) {
95 // Short‐circuit: The identifier has already been stated with
99 // The current tag has an identifier, but it hasn’t been stated
100 // with this predicate yet.
101 const tag
= storage
.get(identifier
);
103 // ☡ The current tag has not been persisted to this `Tag`’s
105 throw new RangeError(
106 `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`,
108 } else if (!this.#isTagInStorage(tag
)) {
109 // ☡ The current tag is not a tag in the correct tag system.
111 `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`,
114 !isObjectPredicateOK(
121 // ☡ This tag and the current tag form an invalid pair for
124 `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`,
127 // The current tag is a tag in the correct tag system; add
129 values
.add(identifier
);
137 * Removes the provided string label(s) from this `Tag` as the
138 * provided predicate, then returns this `Tag`.
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
);
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`.
154 * Arguments may be string identifiers or objects with an
155 * `.identifier` property.
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($));
167 * Returns whether or not the provided value is a tag which shares a
168 * storage with this tag.
170 * Sharing a storage also implies sharing a `TagSystem`.
174 // Try to compare the provided value’s internal store with
175 // the provided storage.
176 return $.#storage
== this.#storage
;
178 // The provided value was not a `Tag`.
184 * Yields the labels of this `Tag` according to the provided
187 *#yieldLabels(predicate
) {
188 yield* this.#data
[predicate
];
192 * Yields the tags that this `Tag` is related to by the provided
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
200 const tag
= storage
.get(identifier
);
202 !this.#isTagInStorage(tag
) || !isObjectPredicateOK(
209 // The tag no longer appears in storage or is not compatible;
210 // perhaps it was deleted.
213 // The tag exists and is constructable from storage.
220 * Yields the tags that this `Tag` is related to by the provided
221 * predicate, figured transitively.
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
;
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
);
238 !this.#isTagInStorage(tag
) || !isObjectPredicateOK(
245 // The tag no longer appears in storage or is not
246 // compatible; perhaps it was deleted.
249 // The tag exists and is constructable from storage.
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
);
258 // The nested tag has already been encountered.
264 // The tag has already been encountered.
272 * Constructs a new `Tag` of the provided kind and with the provided
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
280 * ☡ This constructor throws if the provided kind is not supported.
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}.`,
293 // The provided kind is one of the recognized tag kinds.
294 this.#kind
= kindString
;
295 this.#data
.prefLabel
= prefLabel
;
300 * Returns a new `Tag` constructor for the provided system, storage,
301 * schema, created with an appropriate prototype for the properties
304 * ※ This function is not exposed.
306 static For(system
, storage
, schema
) {
309 transitiveProperties
,
312 const constructor = function (...$s
) {
313 return Reflect
.construct(
315 [system
, storage
, schema
, ...$s
],
319 Object
.defineProperties(constructor, {
320 name
: { value
: "TagSystem::Tag" },
324 value
: Object
.create(
326 Object
.fromEntries(Array
.from(
328 for (const key
in objectProperties
) {
329 // Iterate over each object property and yield any
330 // necessary method definitions.
334 } = objectProperties
[key
];
335 if (key
in transitiveProperties
) {
336 // The current key indicates a transitive property.
338 // Transitive property methods are added by their
339 // nontransitive subproperties.
342 // The current key does not indicate a transitive
344 yield [`${key}Tags`, function* () {
345 yield* this.#yieldTags(key
);
347 if (inverseOf
== null) {
348 // The current key does not indicate an inverse
349 // property, so add and delete methods are also
351 const cased
= key
[0].toUpperCase() +
353 yield [`add${cased}Tag`, function (...tags
) {
354 return this.#addTag(key
, ...tags
);
356 yield [`delete${cased}Tag`, function (...tags
) {
357 return this.#deleteTag(key
, ...tags
);
360 // The current key indicates an inverse property,
361 // so no add and delete methods are necessary.
365 subPropertyOf
!= null &&
366 subPropertyOf
in transitiveProperties
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(
377 // The current key does not indicate a subproperty
378 // of a transitive property.
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() +
390 yield [`${key}s`, function* () {
391 yield* this.#yieldLabels(key
);
393 yield [`add${cased}`, function (...labels
) {
394 return this.#addLabel(key
, ...labels
);
396 yield [`delete${cased}`, function (...labels
) {
397 return this.#deleteLabel(key
, ...labels
);
400 // The current key is `"prefLabel"`. This is a
401 // special case which is not handled by the schema.
406 ([key
, value
]) => [key
, {
409 value
: Object
.defineProperty(value
, "name", {
419 return new TagConstructor(constructor, system
, storage
, schema
);
423 * Assigns the provided data and identifier to the provided tag.
425 * ☡ This function throws if the provided tag is not a `Tag`.
427 * ※ This function is not exposed.
429 static assignData(tag
, data
, identifier
) {
430 tag
.#identifier
= `${identifier}`;
431 tag
.#persistedData
= tagData(data
);
432 tag
.#data
= tagData(data
);
437 * Returns a new `Tag` with the provided identifier, kind, and
440 * ※ This function exists to enable `TagSystem`s to replay Create
441 * activities, maintaining the identifier of the original.
443 * ☡ This function throws if the provided identifier is already in
446 * ※ This function is not exposed.
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}.`,
455 const createdTag
= new system
.Tag(kind
, prefLabel
);
456 createdTag
.#identifier
= identifier
;
457 createdTag
.persist(true);
463 * Returns the `TagSystem` that the provided value belongs to.
465 * ※ This function can be used to check if the provided value has
466 * private tag features.
468 * ※ `Tag::system` is an overridable, publicly‐accessible means of
469 * accessing the system.
471 * ※ This function is not exposed.
473 static getSystem($) {
474 return !(#system
in Object($)) ? null : $.#system
;
478 // Overwrite the default `::constructor` method to instead give the
479 // actual (bound) constructor which was used to generate a given
481 Object
.defineProperties(this.prototype, {
486 // All `Tag`s are constructed via the `.Tag` constructor
487 // available in their `TagSystem`; return it.
488 return this.#system
.Tag
;
495 /** Returns the authority (domain) name for this `Tag`. */
496 get authorityName() {
497 return this.#system
.authorityName
;
500 /** Returns the identifier of this `Tag`. */
502 return this.#identifier
;
505 /** Returns the I·R·I for this `Tag`. */
507 const { identifier
, iriSpace
} = this;
508 return identifier
== null ? null : `${iriSpace}${identifier}`;
511 /** Returns the I·R·I space for this `Tag`. */
513 return this.#system
.iriSpace
;
516 /** Returns the kind of this `Tag`. */
522 * Returns the `TagSystem` for this `Tag`.
524 * ※ Internally, `Tag.getSystem` is preferred.
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.
535 * If the second argument is `true`, the `Tag` will be persisted but
536 * no serialization will be made. This is somewhat more efficient.
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.
543 * ※ Inverse object properties will never appear in the predicates
544 * of generated activities.
546 persist(silent
= false) {
547 const system
= this.#system
;
548 const storage
= this.#storage
;
551 transitiveProperties
,
554 const persistedData
= this.#persistedData
;
555 const data
= this.#data
;
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.
561 objectProperties
[key
]?.inverseOf
!= null ||
562 silent
&& key
in dataProperties
564 // The current property is one which is skipped in diffs.
566 // In a silent persist, this includes any literal terms.
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.
575 new: value
instanceof Set
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
587 if (value
.has(existing
)) {
588 // The value is in both the old and new version of the
590 oldValues
.delete(existing
);
591 newValues
.delete(existing
);
593 // The value is not shared.
597 diffs
[key
] = { old
: oldValues
, new: newValues
};
599 `${value}` != `${persisted}` ||
600 value
.language
!= persisted
.language
602 // The current property is (optionally language‐tagged)
603 // string‐valued and the value changed.
605 old
: new Set([persisted
]),
606 new: new Set([value
]),
609 // The current property did not change.
610 diffs
[key
] = { old
: new Set(), new: new Set() };
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);
620 // This `Tag` has not been persisted yet; save the new
621 // identifier after persisting.
622 this.#identifier
= storage
.add(this);
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
635 // The current property is the inverse of a non‐transitive
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
);
642 // Try removing this `Tag`.
643 referenced
.#data
[inverse
].delete(persistedIdentifier
);
644 storage
.set(referencedIdentifier
, referenced
);
646 // Removal failed, possibly because the other tag was
651 for (const referencedIdentifier
of diffs
[term
].new) {
652 const referenced
= storage
.get(referencedIdentifier
);
654 // Try adding this `Tag`.
655 referenced
.#data
[inverse
].add(persistedIdentifier
);
656 storage
.set(referencedIdentifier
, referenced
);
658 // Adding failed, possibly because the other tag was deleted.
665 // This is a silent persist.
668 // This is not a silent persist; an activity needs to be
669 // generated if a change was made.
671 "@context": taggingDiscoveryContext
,
674 identifier
== null ? "Create" : "Update",
676 context
: `${system.iri}`,
677 object
: `${this.iri}`,
678 endTime
: new Date().toISOString(),
684 const { unstates
, states
} = statements
;
685 if (identifier
== null) {
686 // This is a Create activity.
687 states
.push({ predicate
: "a", object
: `${this.kind}` });
689 // This is an Update activity.
696 }] of Object
.entries(diffs
)
698 // Iterate over the diffs of each term and state/unstate
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.
706 object
: { ...oldValue
},
709 // This is a named term; attempt to get its I·R·I and
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.
718 // The value resolved; push its I·R·I.
725 // Value resolution failed for some reason; perhaps the
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.
737 object
: { ...newValue
},
740 // This is a named term; attempt to get its I·R·I and
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.
749 // The value resolved; push its I·R·I.
756 // Value resolution failed for some reason; perhaps the
763 if (unstates
.length
== 0) {
764 // Nothing was unstated.
765 delete statements
.unstates
;
767 // Things were stated.
770 if (states
.length
== 0) {
771 // Nothing was stated.
772 delete statements
.states
;
774 // Things were stated.
781 !Object
.hasOwn(activity
, "states") &&
782 !Object
.hasOwn(activity
, "unstates")
784 // No meaningful changes were actually persisted.
787 // There were meaningful changes persisted regarding this `Tag`.
793 /** Returns the preferred label for this `Tag`. */
795 return this.#data
.prefLabel
;
798 /** Sets the preferred label of this `Tag` to the provided label. */
800 this.#data
.prefLabel
= langString($);
803 /** Returns the Tag U·R·I for this `Tag`. */
805 const { identifier
} = this;
806 return identifier
== null
808 : `tag:${this.taggingEntity}:${identifier}`;
811 /** Returns the tagging entity (domain and date) for this `Tag`. */
812 get taggingEntity() {
813 return this.#system
.taggingEntity
;
816 /** Returns the string form of the preferred label of this `Tag`. */
818 return `${this.#data.prefLabel}`;
822 * Returns a new object whose enumerable own properties contain the
823 * data from this object needed for storage.
825 * ※ This method is not really intended for public usage.
827 [Storage
.toObject
]() {
828 const data
= this.#data
;
829 return Object
.assign(Object
.create(null), {
838 * A `Tag` constructor function.
840 * This class extends the identity function, meaning that the object
841 * provided as the constructor is used verbatim (with new private
844 * ※ The instance methods of this class are provided as static
845 * methods on the superclass which all `Tag` constructors inherit
848 * ※ This class is not exposed.
853 * The exposed constructor function from which all `Tag` constructors
856 * ☡ This constructor always throws.
860 const tagConstructorBehaviours
= Object
.create(null);
862 TagConstructor
: class extends identity
{
864 * The `TagSystem` used for `Tag`s constructed by this
869 /** The `Storage` managed by this constructor’s `TagSystem`. */
872 /** The schema in use for this constructor. */
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.
880 * ※ This constructor does not modify the `name` or `prototype`
881 * properties of the provided constructor.
883 * ※ See `Tag.For`, where this constructor is used.
885 constructor(constructor, system
, storage
, schema
) {
887 Object
.setPrototypeOf(this, TagSuper
);
888 this.#system
= system
;
889 this.#storage
= storage
;
890 this.#schema
= schema
;
894 // Define the superclass constructor which all `Tag`
895 // constructors will inherit from.
896 const superclass
= tagConstructorBehaviours
.TagSuper
=
898 throw new TypeError("Tags must belong to a System.");
900 const { prototype: staticFeatures
} = this;
901 delete staticFeatures
.constructor;
902 Object
.defineProperty(superclass
, "prototype", {
905 value
: Tag
.prototype,
908 Object
.defineProperties(
910 Object
.getOwnPropertyDescriptors(staticFeatures
),
915 * Yields the tags in the `TagSystem` associated with this
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`.
928 // The current instance is not a `Tag` in this
936 * Returns a new `Tag` resolved from the provided I·R·I.
938 * ☡ This function throws if the I·R·I is not in the `.iriSpace`
939 * of the `TagSystem` associated with this constructor.
941 * ※ If the I·R·I is not recognized, this function returns
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}`,
955 // The I·R·I begins with the expected prefix.
956 const identifier
= name
.substring(prefix
.length
);
958 // Attempt to resolve the identifier.
959 const instance
= storage
.get(identifier
);
960 return Tag
.getSystem(instance
) == system
964 // Do not throw for bad identifiers.
971 * Returns a new `Tag` resolved from the provided identifier.
973 * ☡ This function throws if the identifier is invalid.
975 * ※ If the identifier is valid but not recognized, this
976 * function returns `undefined`.
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
988 * Returns a new `Tag` resolved from the provided Tag U·R·I.
990 * ☡ This function throws if the provided Tag U·R·I does not
991 * match the tagging entity of this constructor’s `TagSystem`.
993 * ※ If the specific component of the Tag U·R·I is not
994 * recognized, this function returns `undefined`.
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}`,
1007 // The I·R·I begins with the expected prefix.
1008 const identifier
= tagName
.substring(tagPrefix
.length
);
1010 // Attempt to resolve the identifier.
1011 const instance
= storage
.get(identifier
);
1012 return Tag
.getSystem(instance
) == system
1016 // Do not throw for bad identifiers.
1023 * Yields the tag identifiers in the `TagSystem` associated with
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`.
1036 // The current instance is not a `Tag` in this `TagSystem`.
1042 /** Returns the `TagSystem` for this `Tag` constructor. */
1044 return this.#system
;
1048 * Returns a new `Tag` constructed from the provided data and
1049 * with the provided identifier.
1051 * ※ This function is not really intended for public usage.
1053 [Storage
.toInstance
](data
, identifier
) {
1054 const tag
= new this(data
.kind
);
1055 return Tag
.assignData(tag
, data
, identifier
);
1058 TagSuper
: tagConstructorBehaviours
.TagSuper
,
1064 * Returns whether the provided schema, subject class, object
1065 * property, and object class are consistent.
1067 * This is hardly a full reasoner; it is tuned to the abilites and
1068 * needs of this module.
1070 isObjectPredicateOK
,
1072 const cachedClassAndSuperclasses
= new WeakMap();
1073 const cachedClassRestrictions
= new WeakMap();
1074 const cachedPredicateRestrictions
= new WeakMap();
1076 const classAndSuperclasses
= function* (
1079 touched
= new Set(),
1081 if (baseClass
== "Thing" || touched
.has(baseClass
)) {
1085 touched
.add(baseClass
);
1086 const subClassOf
= classes
[baseClass
]?.subClassOf
?? "Thing";
1088 const superclass
of (
1089 typeof subClassOf
== "string"
1091 : Array
.from(subClassOf
)
1092 ).filter(($) => typeof $ == "string")
1094 yield* classAndSuperclasses(classes
, superclass
, touched
);
1099 const getClassAndSuperclasses
= (schema
, baseClass
) => {
1100 const schemaCache
= cachedClassAndSuperclasses
.get(schema
);
1101 const cached
= schemaCache
?.[baseClass
];
1102 if (cached
!= null) {
1105 const { classes
} = schema
;
1106 const result
= [...classAndSuperclasses(classes
, baseClass
)];
1108 schemaCache
[baseClass
] = result
;
1110 cachedClassRestrictions
.set(
1112 Object
.assign(Object
.create(null), { [baseClass
]: result
}),
1119 const getClassRestrictions
= (schema
, domain
) => {
1120 const schemaCache
= cachedClassRestrictions
.get(schema
);
1121 const cached
= schemaCache
?.[domain
];
1122 if (cached
!= null) {
1125 const { classes
} = schema
;
1126 const restrictions
= Object
.create(null);
1127 const subClassOf
= classes
[domain
]?.subClassOf
?? "Thing";
1129 const superclass
of (
1130 typeof subClassOf
== "string"
1132 : Array
.from(subClassOf
)
1133 ).filter(($) => Object($) === $)
1135 const { onProperty
, allValuesFrom
} = superclass
;
1136 restrictions
[onProperty
] = processSpace(allValuesFrom
);
1139 schemaCache
[domain
] = restrictions
;
1141 cachedClassRestrictions
.set(
1143 Object
.assign(Object
.create(null), {
1144 [domain
]: restrictions
,
1148 return restrictions
;
1152 const getPredicateRestrictions
= (schema
, predicate
) => {
1153 const schemaCache
= cachedPredicateRestrictions
.get(schema
);
1154 const cached
= schemaCache
?.[predicate
];
1155 if (cached
!= null) {
1158 const { objectProperties
} = schema
;
1159 const restrictions
= [
1160 ...predicateRestrictions(objectProperties
, predicate
),
1162 (result
, { domainIntersection
, rangeIntersection
}) => {
1163 result
.domainIntersection
.push(...domainIntersection
);
1164 result
.rangeIntersection
.push(...rangeIntersection
);
1167 Object
.assign(Object
.create(null), {
1168 domainIntersection
: [],
1169 rangeIntersection
: [],
1173 schemaCache
[predicate
] = restrictions
;
1175 cachedPredicateRestrictions
.set(
1177 Object
.assign(Object
.create(null), {
1178 [predicate
]: restrictions
,
1182 return restrictions
;
1186 const processSpace
= (space
) =>
1187 Object(space
) === space
1192 Object(subspace
) === subspace
1193 ? Array
.from(subspace
.unionOf
)
1196 : [Array
.from(space
.unionOf
)]
1199 const predicateRestrictions
= function* (
1202 touched
= new Set(),
1204 if (predicate
== "Property" || touched
.has(predicate
)) {
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"),
1213 touched
.add(predicate
);
1215 const superproperty
of (
1216 subPropertyOf
== null
1218 : typeof subPropertyOf
== "string"
1220 : Array
.from(subPropertyOf
)
1223 yield* predicateRestrictions(
1233 isObjectPredicateOK
: (
1239 const { objectProperties
} = schema
;
1240 const predicateDefinition
= objectProperties
[predicate
];
1241 const isInverse
= "inverseOf" in predicateDefinition
;
1242 const usedPredicate
= isInverse
1243 ? predicateDefinition
.inverseOf
1245 const domain
= isInverse
? objectClass
: subjectClass
;
1246 const domains
= new Set(getClassAndSuperclasses(schema
, domain
));
1247 const ranges
= new Set(getClassAndSuperclasses(
1249 isInverse
? subjectClass
: objectClass
,
1251 const predicateRestrictions
= getPredicateRestrictions(
1255 const { domainIntersection
} = predicateRestrictions
;
1256 const rangeIntersection
= [
1257 ...predicateRestrictions
.rangeIntersection
,
1259 for (const domain
of domains
) {
1260 const classRestrictionOnPredicate
=
1261 getClassRestrictions(schema
, domain
)[usedPredicate
];
1262 if (classRestrictionOnPredicate
!= null) {
1263 yield* classRestrictionOnPredicate
;
1270 return domainIntersection
.every((domainUnion
) =>
1271 domainUnion
.some((domain
) =>
1272 domain
== "Thing" || domains
.has(domain
)
1275 rangeIntersection
.every((rangeUnion
) =>
1276 rangeUnion
.some((range
) =>
1277 range
== "Thing" || ranges
.has(range
)
1286 * Returns the provided value converted into a `String` object with
1287 * `.["@value"]` and `.["@language"]` properties.
1289 * The same object will be returned for every call with an equivalent
1292 * TODO: Ideally this would be extracted more fully into an R·D·F
1295 * ※ This function is not exposed.
1300 * Returns the language string object corresponding to the provided
1301 * value and language.
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
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, {
1320 enumerable
: !!language
,
1321 value
: language
|| null,
1323 language
: { enumerable
: false, get: getLanguage
},
1324 toString
: { enumerable
: false, value
: toString
},
1325 valueOf
: { enumerable
: false, value
: valueOf
},
1328 const ref
= new WeakRef(result
);
1329 langStringRegistry
.register(result
, { ref
, language
, value
});
1330 valueMap
[value
] = ref
;
1335 /** Returns the `.["@language"]` of this object. */
1336 const getLanguage
= Object
.defineProperty(
1338 return this["@language"] || null;
1341 { value
: "get language" },
1345 * A `FinalizationRegistry` for language string objects.
1347 * This simply cleans up the corresponding `WeakRef` in the language
1350 const langStringRegistry
= new FinalizationRegistry(
1351 ({ ref
, language
, value
}) => {
1352 const valueMap
= languageMap
[language
];
1353 if (valueMap
?.[value
] === ref
) {
1354 delete valueMap
[value
];
1362 * An object whose own values are an object mapping values to
1363 * language string objects for the language specified by the key.
1365 const languageMap
= Object
.create(null);
1367 /** Returns the `.["@value"]` of this object. */
1368 const toString = function () {
1369 return this["@value"];
1373 * Returns this object if it has a `.["@language"]`; otherwise, its
1376 const valueOf = function () {
1377 return this["@language"] ? this : this["@value"];
1387 `${$["@language"] ?? ""}`,
1389 : getLangString(`${$["@value"]}`)
1391 ? getLangString(`${$}`, `${$.language ?? ""}`)
1392 : getLangString(`${$}`)
1393 : getLangString(`${$ ?? ""}`),
1398 * Returns a normalized tag data object derived from the provided
1401 * ※ The properties of this function need to match the term names used
1402 * in the ActivityStreams serialization.
1404 * ※ This function is not exposed.
1406 const tagData
= ($) => {
1407 const data
= Object($);
1409 // prefLabel intentionally not set here
1419 let prefLabel
= langString(data
.prefLabel
);
1420 return Object
.preventExtensions(Object
.create(null, {
1423 get: () => prefLabel
,
1425 prefLabel
= langString($);
1432 ? Array
.from(altLabel
, langString
)
1440 ? Array
.from(hiddenLabel
, langString
)
1448 ? Array
.from(broader
, toIdentifier
)
1456 ? Array
.from(narrower
, toIdentifier
)
1464 ? Array
.from(inCanon
, toIdentifier
)
1472 ? Array
.from(hasInCanon
, toIdentifier
)
1480 ? Array
.from(involves
, toIdentifier
)
1488 ? Array
.from(involvedIn
, toIdentifier
)
1496 * Returns an identifier corresponding to the provided object.
1498 * This is either the value of its `.identifier` or its string value.
1500 * ※ This function is not exposed.
1502 const toIdentifier
= ($) =>
1505 : Object($) === $ && "identifier" in $
1510 * A tag system, with storage.
1512 * The `::Tag` constructor available on any `TagSystem` instance can be
1513 * used to create new `Tag`s within the system.
1515 export class TagSystem
{
1516 /** The cached bound `Tag` constructor for this `TagSystem`. */
1519 /** The domain of this `TagSystem`. */
1522 /** The date of this `TagSystem`. */
1525 /** The identifier of this `TagSystem`. */
1528 /** The schema used by this `TagSystem. */
1531 /** The internal `Storage` of this `TagSystem`. */
1532 #storage
= new Storage();
1535 * Constructs a new `TagSystem` with the provided domain and date.
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.
1541 * ☡ This constructor throws if provided with an invalid date.
1543 constructor(domain
, date
, identifier
= "") {
1544 const domainString
= `${domain}`;
1545 const dateString
= `${date}`;
1546 this.#identifier
= `${identifier}`;
1548 // If the identifier is a valid storage I·D, reserve it.
1549 this.#storage
.delete(this.#identifier
);
1551 // The identifier is not a valid storage I·D, so no worries.
1555 !/^[a-z](?:[\da-z-]*[\da-z])?(?:\.[a-z](?:[\da-z-]*[\da-z])?)*$/u
1558 // ☡ The domain is invalid.
1559 throw new RangeError(`Invalid domain: ${domain}.`);
1561 !/^\d{4}-\d{2}-\d{2}$/u.test(dateString
) ||
1562 dateString
!= new Date(dateString
).toISOString().split("T")[0]
1564 // ☡ The date is invalid.
1565 throw new RangeError(`Invalid date: ${date}.`);
1567 // The domain and date are 🆗.
1568 this.#domain
= domainString
;
1569 this.#date
= dateString
;
1574 * Returns a bound constructor for constructing `Tags` in this
1578 if (this.#Tag
!= null) {
1579 // A bound constructor has already been generated; return it.
1582 // No bound constructor has been created yet.
1583 const storage
= this.#storage
;
1584 return this.#Tag
= Tag
.For(this, storage
, this.#schema
);
1589 * Applies the provided activity to this `TagSystem` by replaying its
1590 * statement changes.
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.
1599 * ☡ This method throws an error if the provided activity cannot be
1603 const { Tag
: TagConstructor
} = this;
1607 transitiveProperties
,
1610 const { object
, states
, unstates
} = activity
;
1611 const activityTypes
= [].concat(activity
["@type"]);
1613 // ☡ The provided activity has no object.
1614 throw new TypeError(
1615 "Cannot apply activity: Activity lacks an object.",
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}`,
1630 // ☡ The object of the provided activity is in the I·R·I
1631 // space of this `TagSystem`.
1632 return iri
.substring(iriSpace
.length
);
1635 const tag
= (() => {
1636 // Either resolve the identifier to an existing tag or create
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
,
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.`,
1651 // There is a recognized tag class provided for the tag.
1652 return Tag
.new(this, identifier
, kind
);
1655 // The provided activity is not a Create activity.
1656 return TagConstructor
.fromIdentifier(identifier
);
1660 // ☡ Resolving the tag identifier failed.
1661 throw new RangeError(
1662 `Cannot apply activity: No tag for identifier: ${identifier}.`,
1665 // Resolving the identifier succeeded; apply the changes to the
1666 // tag and then silently persist it.
1668 const [statements
, mode
] of [
1669 [unstates
?? [], "delete"],
1670 [states
?? [], "add"],
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
1679 : predicate
in objectProperties
&&
1680 !(predicate
in transitiveProperties
||
1681 objectProperties
[predicate
].inverseOf
!= null)
1685 // The provided predicate is not recognized; ignore it.
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
1694 // Update the preflabel.
1695 tag
.prefLabel
= term
;
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
1707 // The predicate is an object property.
1709 // Attempt to resolve the object.
1710 return TagConstructor
.fromIRI(term
);
1712 // Resolving failed; return undefined.
1717 if (related
=== undefined) {
1718 // The predicate is an object property, but its object
1719 // was not resolvable.
1721 // ☡ This is a silent error to allow for selective
1722 // replay of activities while ignoring terms which are
1726 // The predicate is not an object property or has a
1727 // resolvable object.
1729 // Apply the statement.
1732 predicate
[0].toUpperCase(),
1733 predicate
.substring(1),
1734 predicate
in objectProperties
? "Tag" : "",
1747 /** Returns the authority name (domain) for this `TagSystem`. */
1748 get authorityName() {
1749 return this.#domain
;
1752 /** Returns the date of this `TagSystem`, as a string. */
1758 * Yields the entities in this `TagSystem`.
1760 * ※ Entities can hypothetically be anything. If you specifically
1761 * want the `Tag`s, use `::Tag.all` instead.
1764 yield* this.#storage
.values();
1768 * Returns the identifier of this `TagSystem`.
1770 * ※ Often this is just the empty string.
1773 return this.#identifier
;
1776 /** Yields the identifiers in use in this `TagSystem`. */
1778 yield* this.#storage
.keys();
1781 /** Returns the I·R·I for this `TagSystem`. */
1783 return `${this.iriSpace}${this.identifier}`;
1787 * Returns the prefix used for I·R·I’s of `Tag`s in this `TagSystem`.
1790 return `https://${this.authorityName}/tag:${this.taggingEntity}:`;
1793 /** Returns the Tag U·R·I for this `TagSystem`. */
1795 return `tag:${this.taggingEntity}:${this.identifier}`;
1799 * Returns the tagging entity (domain and date) for this `TagSystem`.
1801 get taggingEntity() {
1802 return `${this.authorityName},${this.date}`;