Vocabulary
One of the key features of Fedify library is that it provides a collection of type-safe objects that represent the Activity Vocabulary and the vendor-specific extensions.
There are tons of objects in the Activity Vocabulary, and it's not practical to list all of them here. Instead, we'll show a few examples of the objects that are available in the library: Create
, Note
, and Person
. For the full list of the objects, please refer to the API reference.
Instantiation
You can instantiate an object by calling the constructor function with an object that contains the properties of the object. The following shows an example of instantiating a Create
object:
import { Create, Note } from "@fedify/fedify";
const create = new Create({
id: new URL("https://example.com/activities/123"),
actor: new URL("https://example.com/users/alice"),
object: new Note({
id: new URL("https://example.com/notes/456"),
content: "Hello, world!",
published: Temporal.Instant.from("2024-01-01T00:00:00Z"),
}),
});
Note that every URI is represented as a URL
object. This is for distinguishing the URIs from the other strings.
TIP
You can instantiate an object from a JSON-LD document by calling the fromJsonLd()
method of the object. See the JSON-LD section for details.
Properties
Every object in the Activity Vocabulary has a set of properties. The properties are categorized into the following types:
- Functional or non-functional
- Scalar or non-scalar
Functional properties are the properties that contain zero or a single value, while non-functional properties are the properties that contain zero or multiple values.
Scalar properties can contain only scalar values (e.g., string, number, boolean, URI), while non-scalar properties can contain both scalar and non-scalar values. Objects like Create
, Note
, and Person
are non-scalar values. Non-scalar properties can contain either objects or URIs (object ID) of the objects.
Depending on the category of the property, the accessors of the property are different. The following table shows examples of the accessors:
Functional | Non-functional | |
---|---|---|
Scalar | Object.published | Object.name /names |
Non-scalar | Person.inboxId /getInbox() | Activity.actorId /actorIds /getActor() /getActors() |
Some non-functional properties have both singular and plural accessors for the sake of convenience. In such cases, the singular accessors return the first value of the property, while the plural accessors return all values of the property.
NOTE
Some of the properties in Activity Vocabulary have been renamed in Fedify:
Object IDs and remote objects
Every object in the Activity Vocabulary has an id
property, which is the URI of the object. It is used to identify and dereference the object.
For example, the following two objects are equivalent (where dereferencing URI https://example.com/notes/456 returns the Note
object):
const a = new Create({
id: new URL("https://example.com/activities/123"),
actor: new URL("https://example.com/users/alice"),
object: new Note({
id: new URL("https://example.com/notes/456"),
content: "Hello, world!",
published: Temporal.Instant.from("2024-01-01T00:00:00Z"),
}),
});
const b = new Create({
actor: new URL("https://example.com/users/alice"),
object: new URL("https://example.com/notes/456"),
});
How are the two objects equivalent? Because for the both objects, getObject()
returns the equivalent Note
object. Such get*()
methods for non-scalar properties are called dereferencing accessors. Under the hood, the get*()
methods fetch the remote object from the URI and return the object if no cache hit. In the above example, the await a.getObject()
immediately returns the Note
object because it's already instantiated, while the await b.getObject()
fetches the remote object from the URI and returns the Note
object.
If you only need the object ID without fetching the remote object, you can use the *Id
/*Ids
accessors instead of dereferencing accessors. In the same manner, both a.objectId
and b.objectId
return the equivalent URI.
TIP
Dereferencing accessors take option documentLoader
to specify the method to fetch the remote object. By default, it uses the default document loader which utilizes th fetch()
API.
If you want to implement your own document loader, see the DocumentLoader
interface in the API reference.
See the Getting a DocumentLoader
section for details.
Property hydration
The get*()
accessor methods for non-scalar properties automatically populate the property with the remote object when the methods are called even if the property internally contains only the URI of the object. This is called property hydration.
For example, the following code hydrates the object
property of the Create
object:
const create = new Create({
object: new URL(
"https://hollo.social/@fedify/0191e4f3-6b08-7003-9d33-f07d1e33d7b4",
),
});
// Hydrates the `object` property:
const note = await create.getObject();
// Returns the hydrated `object` property; therefore, the following code does
// not make any HTTP request:
const note2 = await create.getObject();
Hydrating the property also affects the JSON-LD representation of the object.
For example, since the following code does not hydrate the object
property, the JSON-LD representation of the Create
object has the object
property with the URI of the object:
const create = new Create({
object: new URL(
"https://hollo.social/@fedify/0191e4f3-6b08-7003-9d33-f07d1e33d7b4",
),
});
const jsonLd = await create.toJsonLd();
console.log(JSON.stringify(jsonLd));
The above code outputs the following JSON-LD document ("@context"
is simplified for readability):
{
"@context": ["https://www.w3.org/ns/activitystreams"],
"type": "Create",
"object": "https://hollo.social/@fedify/0191e4f3-6b08-7003-9d33-f07d1e33d7b4"
}
However, if the property is once hydrated, the JSON-LD representation of the object has the object
property with the full object:
// Hydrates the `object` property:
await create.getObject();
const jsonLd = await create.toJsonLd();
console.log(JSON.stringify(jsonLd));
The above code outputs the following JSON-LD document ("@context"
and some attributes are simplified or omitted for readability):
{
"type": "Create",
"@context": ["https://www.w3.org/ns/activitystreams"],
"object": {
"id": "https://hollo.social/@fedify/0191e4f3-6b08-7003-9d33-f07d1e33d7b4",
"type": "Note",
"attributedTo": "https://hollo.social/@fedify",
"content": "<p>...</p>\n",
"published": "2024-09-12T06:37:23.593Z",
"sensitive": false,
"tag": [],
"to": "as:Public",
"url": "https://hollo.social/@fedify/0191e4f3-6b08-7003-9d33-f07d1e33d7b4"
}
}
Immutability
Every object in the Activity Vocabulary is represented as an immutable object. This means that you cannot change the properties of the object after the object is instantiated. This is for ensuring the consistency of the objects and the safety of the objects in the concurrent environment.
In order to change the properties of the object, you need to clone the object with the new properties. Fortunately, the objects have a clone()
method that takes an object with the new properties and returns a new object with the new properties. The following shows an example of changing the content
property of a Note
object:
import { LanguageString, Note } from "@fedify/fedify";
const noteInEnglish = new Note({
id: new URL("https://example.com/notes/123"),
content: new LanguageString("Hello, world!", "en"),
published: Temporal.Now.instant(),
});
const noteInChinese = noteInEnglish.clone({
content: new LanguageString("你好,世界!", "zh"),
});
Parameters of the clone()
method share the same type with parameters of the constructor.
Looking up remote objects
See the Looking up remote objects section in the Context docs.
Traversing remote collections
See the Traversing remote collections section in the Context docs.
JSON-LD
Under the hood, every object in the Activity Vocabulary is represented as a JSON-LD document. The JSON-LD document is a JSON object that contains the properties of the object. The following shows an example of the JSON-LD representation of the Create
object:
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://example.com/activities/123",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"id": "https://example.com/notes/456",
"content": "Hello, world!",
"published": "2024-01-01T00:00:00Z"
}
}
If you want to instantiate an object from a JSON-LD document, you can use the fromJsonLd()
method of the object. The following shows an example of instantiating a Create
object from the JSON-LD document:
import { Create } from "@fedify/fedify";
const create = await Create.fromJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://example.com/activities/123",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"id": "https://example.com/notes/456",
"content": "Hello, world!",
"published": "2024-01-01T00:00:00Z"
}
});
Note that the fromJsonLd()
method can parse a subtype as well. For example, since Create
is a subtype of Activity
, the Activity.fromJsonLd()
method can parse a Create
object as well:
import { Activity } from "@fedify/fedify";
const create = await Activity.fromJsonLd({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://example.com/activities/123",
"actor": "https://example.com/users/alice",
"object": {
"type": "Note",
"id": "https://example.com/notes/456",
"content": "Hello, world!",
"published": "2024-01-01T00:00:00Z"
}
});
On the other way around, you can use the toJsonLd()
method to get the JSON-LD representation of the object:
const jsonLd = await create.toJsonLd();
By default, the toJsonLd()
method returns the JSON-LD document which is neither compacted nor expanded. Instead, it processes the JSON-LD document without the proper JSON-LD processor for efficiency.
The toJsonLd()
method takes some options to customize the JSON-LD document. For example, you can compact the JSON-LD document with a custom context. In this case, the toJsonLd()
method returns the compacted JSON-LD document which is processed by the proper JSON-LD processor:
const jsonLd = await create.toJsonLd({
format: "compact",
context: "https://example.com/context",
});
TIP
Why are the fromJsonLd()
and toJsonLd()
methods asynchronous? Because both methods may fetch remote documents under the hood in order to compact/expand a JSON-LD document. In fact, like the dereferencing accessors, both fromJsonLd()
and toJsonLd()
methods take option documentLoader
to specify the method to fetch the remote document.
See the Getting a DocumentLoader
section for details.
Scalar types
The Activity Vocabulary has a few scalar types that are used as the values of the properties. The following table shows the scalar types and their corresponding TypeScript types:
Scalar type | TypeScript type |
---|---|
xsd:boolean | boolean |
xsd:integer | number |
xsd:nonNegativeInteger | number |
xsd:float | number |
xsd:string | string |
xsd:anyURI | URL |
xsd:dateTime | Temporal.Instant |
xsd:duration | Temporal.Duration |
rdf:langString | LanguageString |
w3id:cryptosuiteString | "eddsa-jcs-2022" |
w3id:multibase | Uint8Array |
Language tag (BCP 47) | LanguageTag |
Public key PEM | CryptoKey |
Public key Multibase | CryptoKey |
Proof purpose | "assertionMethod" | "authentication" | "capabilityInvocation" | "capabilityDelegation" | "keyAgreement" |
Units | "cm" | "feet" | "inches" | "km" | "m" | "miles" | URL |