Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/SDKsPage/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,14 @@ export const data = {
text: 'LiveObjects plugin for JavaScript.',
image: { src: js, isWide: false },
githubRepoURL: 'https://github.com/ably/ably-js',
setupLink: 'liveobjects/quickstart',
setupLink: 'liveobjects/quickstart/javascript',
},
{
title: 'Swift',
text: 'LiveObjects plugin for Swift.',
image: { src: swift, isWide: false },
githubRepoURL: 'https://github.com/ably/ably-liveobjects-swift-plugin',
setupLink: 'liveobjects/quickstart/swift',
},
],
},
Expand Down
1 change: 1 addition & 0 deletions src/data/languages/languageData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default {
},
liveObjects: {
javascript: '2.11',
swift: '0.1',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little odd that we're tying JS/Java to their Pub/Sub version but having Swift at 0.1 just because of how they're imported (just a comment).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that in JS and Java, the LiveObjects plugin is part of the core SDK and hence must have the same version. In Swift, we are forced to have a separate repository for the plugin, which has the happy side effect of allowing us to choose a semantic version number that reflects the fact that the LiveObjects API is experimental (JS and Java instead have to rely on code comments that state this).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I totally understand the reasoning. Just from an external perspective it makes it look like JS and Java are far more mature than the Swift implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure — do you have any thoughts on what we could do to mitigate this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's something we need to look at holistically that we should include on some guidance for releasing plugins. Not worth blocking this from releasing right now.

},
liveSync: {
javascript: '0.4',
Expand Down
16 changes: 15 additions & 1 deletion src/data/nav/liveobjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ export default {
},
{
name: 'Getting started',
link: '/docs/liveobjects/quickstart',
pages: [
{
name: 'JavaScript',
link: '/docs/liveobjects/quickstart/javascript',
},
{
name: 'Swift',
link: '/docs/liveobjects/quickstart/swift',
},
],
},
],
},
Expand Down Expand Up @@ -92,6 +101,11 @@ export default {
name: 'JavaScript SDK',
external: true,
},
{
link: 'https://sdk.ably.com/builds/ably/ably-liveobjects-swift-plugin/main/AblyLiveObjects/documentation/ablyliveobjects/',
name: 'Swift plugin',
external: true,
},
{
link: '/docs/api/liveobjects-rest',
name: 'REST API',
Expand Down
5 changes: 4 additions & 1 deletion src/pages/docs/channels/options/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,10 @@ Occupancy events have a payload in the `data` property with a value of `occupanc
When using inband objects, the client receives messages with the special name `[meta]objects` that describe the current set of objects on a channel.

<Aside data-type="note">
This feature enables clients to subscribe to LiveObjects updates in realtime even on platforms that don't yet have a dedicated LiveObjects Realtime client implementation. If you're using LiveObjects from JavaScript/TypeScript, use the LiveObjects [plugin](/docs/liveobjects/quickstart?lang=javascript) which has dedicated support for all LiveObjects features.
This feature enables clients to subscribe to LiveObjects updates in realtime even on platforms that don't yet have a dedicated LiveObjects Realtime client implementation. If you're using LiveObjects from one of the the following languages, then use the LiveObjects plugin which has dedicated support for all LiveObjects features:

* [JavaScript/TypeScript](/docs/liveobjects/quickstart/javascript)
* [Swift](/docs/liveobjects/quickstart/swift)
</Aside>

For more information see the [inband objects](/docs/liveobjects/inband-objects) documentation.
Expand Down
131 changes: 128 additions & 3 deletions src/pages/docs/liveobjects/concepts/objects.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ LiveObjects provides specialized object types to model your application state. T

### LiveMap Object <a id="livemap"/>

[LiveMap](/docs/liveobjects/map) is a key/value data structure similar to a dictionary or JavaScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map):
[LiveMap](/docs/liveobjects/map) is a key/value data structure similar to a <If lang="javascript">dictionary or JavaScript [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)</If><If lang="swift">`Dictionary`</If>:

* Keys must be strings
* Values can be primitive types, JSON-serializable objects or arrays, or [references](#composability) to other objects
Expand All @@ -33,12 +33,22 @@ const userSettings = await channel.objects.createMap();
await userSettings.set('theme', 'dark');
await userSettings.set('notifications', true);
```

```swift
// Create a LiveMap
let userSettings = try await channel.objects.createMap()

// Set primitive values
try await userSettings.set(key: "theme", value: "dark")
try await userSettings.set(key: "notifications", value: true)
```
</Code>

#### Primitive Types <a id="primitive-types"/>

[LiveMap](/docs/liveobjects/map) supports the following primitive types as values:

<If lang="javascript">
* `string`
* `number`
* `boolean`
Expand All @@ -47,6 +57,15 @@ await userSettings.set('notifications', true);
<Aside data-type='note'>
`number` is a double-precision floating-point number, which is the same as JavaScript's [`number`](https://developer.mozilla.org/en-US/docs/Glossary/Number) type.
</Aside>
</If>

<If lang="swift">
* `String`
* `Double`
* `Bool`
* `Data`
* JSON arrays or objects
</If>

### LiveCounter Object <a id="livecounter"/>

Expand All @@ -63,6 +82,14 @@ const visitsCounter = await channel.objects.createCounter();
// Increment the counter
await visitsCounter.increment(1);
```

```swift
// Create a LiveCounter
let visitsCounter = try await channel.objects.createCounter();

// Increment the counter
try await visitsCounter.increment(amount: 1);
```
</Code>

### Root Object <a id="root-object"/>
Expand All @@ -84,6 +111,14 @@ const root = await channel.objects.getRoot();
// Use it like any other LiveMap
await root.set('app-version', '1.0.0');
```

```swift
// Get the Root Object
let root = try await channel.objects.getRoot()

// Use it like any other LiveMap
try await root.set(key: "app-version", value: "1.0.0")
```
</Code>

## Reachability <a id="reachability"/>
Expand All @@ -109,6 +144,21 @@ counterOld.on('deleted', () => {
const counterNew = await channel.objects.createCounter();
await root.set('myCounter', counterNew);
```

```swift
// Create a counter and reference it from the root
let counterOld = try await channel.objects.createCounter()
try await root.set(key: "myCounter", value: .liveCounter(counterOld))

// counterOld will eventually be deleted
counterOld.on(event: .deleted) { _ in
print("counterOld has been deleted and can no longer be used")
}

// Create a new counter and replace the old one referenced from the root
let counterNew = try await channel.objects.createCounter()
try await root.set(key: "myCounter", value: .liveCounter(counterNew))
```
</Code>

<Aside data-type='note'>
Expand All @@ -134,6 +184,26 @@ await profileMap.set('preferences', preferencesMap);
await profileMap.set('activity', activityCounter);
await root.set('profile', profileMap);

// Resulting structure:
// root (LiveMap)
// └── profile (LiveMap)
// ├── preferences (LiveMap)
// │ └── theme: "dark" (string)
// └── activity (LiveCounter)
```

```swift
// Create LiveObjects
let profileMap = try await channel.objects.createMap()
let preferencesMap = try await channel.objects.createMap()
let activityCounter = try await channel.objects.createCounter()

// Build a composite structure
try await preferencesMap.set(key: "theme", value: "dark")
try await profileMap.set(key: "preferences", value: .liveMap(preferencesMap))
try await profileMap.set(key: "activity", value: .liveCounter(activityCounter))
try await root.set(key: "profile", value: .liveMap(profileMap))

// Resulting structure:
// root (LiveMap)
// └── profile (LiveMap)
Expand Down Expand Up @@ -176,6 +246,43 @@ mapB.get('count').subscribe(() => {
// Increment the counter
await counter.increment(1);
```

```swift
// Create a counter
let counter = try await channel.objects.createCounter()

// Create two different maps
let mapA = try await channel.objects.createMap()
let mapB = try await channel.objects.createMap()
try await root.set(key: "a", value: .liveMap(mapA))
try await root.set(key: "b", value: .liveMap(mapB))

// Reference the same counter from both maps
try await mapA.set(key: "count", value: .liveCounter(counter))
try await mapB.set(key: "count", value: .liveCounter(counter))

// The counter referenced from each location shows the same
// value, since they refer to the same underlying counter
try mapA.get(key: "count")?.liveCounterValue?.subscribe { _, _ in
do {
let value = try mapA.get(key: "count")?.liveCounterValue?.value
print(String(describing: value)) // 1
} catch {
// Error not relevant here
}
}
try mapB.get(key: "count")?.liveCounterValue?.subscribe { _, _ in
do {
let value = try mapB.get(key: "count")?.liveCounterValue?.value
print(String(describing: value)) // 1
} catch {
// Error not relevant here
}
}

// Increment the counter
try await counter.increment(amount: 1)
```
</Code>

It is also possible that object references form a cycle:
Expand All @@ -198,6 +305,24 @@ root.get('a') // mapA
.get('ref') // mapB
.get('ref'); // mapA
```

```swift
// Create two different maps
let mapA = try await channel.objects.createMap()
let mapB = try await channel.objects.createMap()

// Set up a circular reference
try await mapA.set(key: "ref", value: .liveMap(mapB))
try await mapB.set(key: "ref", value: .liveMap(mapA))

// Add one map to root (both are now reachable)
try await root.set(key: "a", value: .liveMap(mapA))

// We can traverse the cycle
_ = try root.get(key: "a")? // mapA
.liveMapValue?.get(key: "ref")? // mapB
.liveMapValue?.get(key: "ref") // mapA
```
</Code>

## Metadata <a id="metadata"/>
Expand All @@ -215,15 +340,15 @@ Every object has a unique identifier that distinguishes it from all other object
Object IDs follow a specific format:

<Code fixed="true">
```javascript
```text
type:hash@timestamp
```
</Code>

For example:

<Code fixed="true">
```javascript
```text
counter:J7x6mAF8X5Ha60VBZb6GtXSgnKJQagNLgadUlgICjkk@1734628392000
```
</Code>
Expand Down
68 changes: 68 additions & 0 deletions src/pages/docs/liveobjects/concepts/operations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ await map.set('username', 'alice');
// Remove a key
await map.remove('username');
```

```swift
// Set a value for a key
try await map.set(key: "username", value: "alice")

// Remove a key
try await map.remove(key: "username")
```
</Code>

### LiveCounter Operations <a id="livecounter"/>
Expand All @@ -59,6 +67,14 @@ await counter.increment(5);
// Decrement counter by 2
await counter.decrement(2);
```

```swift
// Increment counter by 5
try await counter.increment(amount: 5)

// Decrement counter by 2
try await counter.decrement(amount: 2)
```
</Code>

### Create Operations <a id="create-operations"/>
Expand All @@ -78,6 +94,17 @@ const userMap = await channel.objects.createMap({
// Create a counter with initial value
const scoreCounter = await channel.objects.createCounter(100);
```

```swift
// Create a map with initial values
let userMap = try await channel.objects.createMap(entries: [
"username": "alice",
"status": "online",
])

// Create a counter with initial value
let scoreCounter = try await channel.objects.createCounter(count: 100)
```
</Code>

When a create operation is processed, an [object ID](/docs/liveobjects/concepts/objects#object-ids) for the new object instance is automatically generated for the object.
Expand All @@ -97,6 +124,11 @@ When using a client library object IDs are handled automatically, allowing you w
// The published operation targets the object ID of the `userMap` object instance
await userMap.set('username', 'alice');
```

```swift
// The published operation targets the object ID of the `userMap` object instance
try await userMap.set(key: "username", "alice")
```
</Code>

Therefore it is important that you obtain an up-to-date object instance before performing operations on an object. For example, you can [subscribe](/docs/liveobjects/map#subscribe-data) to a `LiveMap` instance to ensure you always have an up-to-date reference to any child objects in the map:
Expand All @@ -112,6 +144,42 @@ root.subscribe(() => { myCounter = root.get('myCounter'); });
// the counter instance at the 'myCounter' key in the root map changes
await myCounter.increment(1);
```

{ /* We can't map the JS example directly because Swift concurrency prevents us from mutating local variables in the way that the JS example does, so I tried to show how we might need to handle this scenario in a real-world app where things are isolated to the main actor. But it's long and ugly. */ }
```swift
struct MyView: View {
var root: any LiveMap
@State private var myCounter: (any LiveCounter)?

var body: some View {
Button("Increment the counter") {
Task {
try await myCounter?.increment(amount: 1)
}
}.onAppear {
do {
myCounter = try root.get(key: "myCounter")?.liveCounterValue

// We keep a reference to the latest value that the root map
// stores at the "myCounter" key, to ensure that upon tapping
// the button, we increment the correct counter.

try root.subscribe { _, _ in
MainActor.assumeIsolated {
do {
myCounter = try root.get(key: "myCounter")?.liveCounterValue
} catch {
// Error handling of root.get(key:) omitted for brevity
}
}
}
} catch {
// Error handling of root.get(key:) omitted for brevity
}
}
}
}
```
</Code>

In the [REST API](/docs/liveobjects/rest-api-usage#updating-objects-by-id), this relationship is made explicit:
Expand Down
Loading